@heroku/js-blanket 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/.tsbuildinfo +1 -0
- package/dist/cjs/.tsbuildinfo +1 -0
- package/dist/cjs/adapters/logging/generic.js +23 -0
- package/dist/cjs/adapters/logging/generic.js.map +1 -0
- package/dist/cjs/adapters/logging/generic.test.js +432 -0
- package/dist/cjs/adapters/logging/generic.test.js.map +1 -0
- package/dist/cjs/core/patterns.js +17 -0
- package/dist/cjs/core/patterns.js.map +1 -0
- package/dist/cjs/core/presets.js +116 -0
- package/dist/cjs/core/presets.js.map +1 -0
- package/dist/cjs/core/scrubber.js +260 -0
- package/dist/cjs/core/scrubber.js.map +1 -0
- package/dist/cjs/core/scrubber.test.js +392 -0
- package/dist/cjs/core/scrubber.test.js.map +1 -0
- package/dist/cjs/core/types.js +3 -0
- package/dist/cjs/core/types.js.map +1 -0
- package/dist/cjs/core/types.test.js +326 -0
- package/dist/cjs/core/types.test.js.map +1 -0
- package/dist/cjs/index.js +16 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/index.test.js +31 -0
- package/dist/cjs/index.test.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/.tsbuildinfo +1 -0
- package/{src/adapters/logging/generic.ts → dist/esm/adapters/logging/generic.d.ts} +1 -4
- package/dist/esm/adapters/logging/generic.js +20 -0
- package/dist/esm/adapters/logging/generic.js.map +1 -0
- package/dist/esm/adapters/logging/generic.test.d.ts +7 -0
- package/dist/esm/adapters/logging/generic.test.js +430 -0
- package/dist/esm/adapters/logging/generic.test.js.map +1 -0
- package/dist/esm/core/patterns.d.ts +4 -0
- package/dist/esm/core/patterns.js +14 -0
- package/dist/esm/core/patterns.js.map +1 -0
- package/dist/esm/core/presets.d.ts +64 -0
- package/{src/core/presets.ts → dist/esm/core/presets.js} +46 -55
- package/dist/esm/core/presets.js.map +1 -0
- package/dist/esm/core/scrubber.d.ts +131 -0
- package/dist/esm/core/scrubber.js +256 -0
- package/dist/esm/core/scrubber.js.map +1 -0
- package/dist/esm/core/scrubber.test.d.ts +1 -0
- package/dist/esm/core/scrubber.test.js +390 -0
- package/dist/esm/core/scrubber.test.js.map +1 -0
- package/dist/esm/core/types.d.ts +169 -0
- package/dist/esm/core/types.js +2 -0
- package/dist/esm/core/types.js.map +1 -0
- package/dist/esm/core/types.test.d.ts +9 -0
- package/dist/esm/core/types.test.js +324 -0
- package/dist/esm/core/types.test.js.map +1 -0
- package/{src/index.ts → dist/esm/index.d.ts} +0 -3
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/index.test.d.ts +1 -0
- package/dist/esm/index.test.js +29 -0
- package/dist/esm/index.test.js.map +1 -0
- package/package.json +45 -47
- package/.c8rc.json +0 -11
- package/.editorconfig +0 -11
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -41
- package/.github/copilot-instructions.md +0 -117
- package/.github/workflows/ci.yml +0 -25
- package/.husky/pre-commit +0 -1
- package/.lintstagedrc.json +0 -4
- package/.tool-versions +0 -1
- package/CODEOWNERS +0 -8
- package/CODE_OF_CONDUCT.md +0 -111
- package/CONTRIBUTING.md +0 -123
- package/SECURITY.md +0 -8
- package/docs/examples/logging-integration.md +0 -736
- package/eslint.config.mjs +0 -108
- package/prettier.config.mjs +0 -10
- package/scripts/test-setup.mjs +0 -24
- package/src/adapters/logging/generic.test.ts +0 -531
- package/src/core/patterns.ts +0 -22
- package/src/core/scrubber.test.ts +0 -465
- package/src/core/scrubber.ts +0 -284
- package/src/core/types.test.ts +0 -516
- package/src/core/types.ts +0 -176
- package/src/index.test.ts +0 -41
- package/tsconfig.cjs.json +0 -12
- package/tsconfig.esm.json +0 -12
- package/tsconfig.json +0 -32
- package/tsconfig.test.json +0 -9
package/eslint.config.mjs
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'eslint/config';
|
|
2
|
-
import js from '@eslint/js';
|
|
3
|
-
import tsParser from '@typescript-eslint/parser';
|
|
4
|
-
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
8
|
-
// ESLint flat config for a TypeScript project with dual-build (CJS/ESM) outputs.
|
|
9
|
-
// - Ignores built artifacts in `dist/`
|
|
10
|
-
// - Applies base JS recommended rules to JS files
|
|
11
|
-
// - Applies TypeScript parsing and TS-specific rules to TS files
|
|
12
|
-
// - Enables common Mocha globals in test files
|
|
13
|
-
|
|
14
|
-
export default defineConfig([
|
|
15
|
-
// Global ignores for generated content and dependencies
|
|
16
|
-
{
|
|
17
|
-
ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
|
|
18
|
-
},
|
|
19
|
-
|
|
20
|
-
// JavaScript files
|
|
21
|
-
{
|
|
22
|
-
files: ['**/*.js', '**/*.mjs', '**/*.cjs'],
|
|
23
|
-
...js.configs.recommended,
|
|
24
|
-
languageOptions: {
|
|
25
|
-
ecmaVersion: 2022,
|
|
26
|
-
sourceType: 'module',
|
|
27
|
-
globals: {
|
|
28
|
-
console: 'readonly',
|
|
29
|
-
process: 'readonly',
|
|
30
|
-
Buffer: 'readonly',
|
|
31
|
-
__dirname: 'readonly',
|
|
32
|
-
__filename: 'readonly',
|
|
33
|
-
module: 'readonly',
|
|
34
|
-
require: 'readonly',
|
|
35
|
-
exports: 'readonly',
|
|
36
|
-
global: 'readonly',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
// TypeScript source files
|
|
42
|
-
{
|
|
43
|
-
files: ['src/**/*.ts', 'src/**/*.mts', 'src/**/*.cts'],
|
|
44
|
-
languageOptions: {
|
|
45
|
-
parser: tsParser,
|
|
46
|
-
parserOptions: {
|
|
47
|
-
// Use the project tsconfig for type-aware linting
|
|
48
|
-
project: ['./tsconfig.json'],
|
|
49
|
-
tsconfigRootDir: path.dirname(fileURLToPath(import.meta.url)),
|
|
50
|
-
ecmaVersion: 2022,
|
|
51
|
-
sourceType: 'module',
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
plugins: {
|
|
55
|
-
'@typescript-eslint': tsPlugin,
|
|
56
|
-
},
|
|
57
|
-
rules: {
|
|
58
|
-
// Align with config behavior
|
|
59
|
-
'@typescript-eslint/no-unused-vars': [
|
|
60
|
-
'warn',
|
|
61
|
-
{
|
|
62
|
-
argsIgnorePattern: '^_',
|
|
63
|
-
varsIgnorePattern: '^_',
|
|
64
|
-
},
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
// Test files (Mocha) - Updated for colocation
|
|
70
|
-
{
|
|
71
|
-
files: [
|
|
72
|
-
'src/**/*.test.ts',
|
|
73
|
-
'src/**/*.spec.ts',
|
|
74
|
-
'src/testUtils/**/*.test.ts',
|
|
75
|
-
],
|
|
76
|
-
languageOptions: {
|
|
77
|
-
parser: tsParser,
|
|
78
|
-
parserOptions: {
|
|
79
|
-
project: ['./tsconfig.test.json'],
|
|
80
|
-
tsconfigRootDir: path.dirname(fileURLToPath(import.meta.url)),
|
|
81
|
-
ecmaVersion: 2022,
|
|
82
|
-
sourceType: 'module',
|
|
83
|
-
},
|
|
84
|
-
globals: {
|
|
85
|
-
describe: 'readonly',
|
|
86
|
-
it: 'readonly',
|
|
87
|
-
before: 'readonly',
|
|
88
|
-
beforeEach: 'readonly',
|
|
89
|
-
after: 'readonly',
|
|
90
|
-
afterEach: 'readonly',
|
|
91
|
-
console: 'readonly',
|
|
92
|
-
global: 'readonly',
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
plugins: {
|
|
96
|
-
'@typescript-eslint': tsPlugin,
|
|
97
|
-
},
|
|
98
|
-
rules: {
|
|
99
|
-
'@typescript-eslint/no-unused-vars': [
|
|
100
|
-
'warn',
|
|
101
|
-
{
|
|
102
|
-
argsIgnorePattern: '^_',
|
|
103
|
-
varsIgnorePattern: '^_',
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
]);
|
package/prettier.config.mjs
DELETED
package/scripts/test-setup.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Suppress structured logger output objects while preserving Mocha output
|
|
2
|
-
const originalLog = console.log.bind(console);
|
|
3
|
-
const originalError = console.error.bind(console);
|
|
4
|
-
|
|
5
|
-
function shouldSuppress(args) {
|
|
6
|
-
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
|
|
7
|
-
const msg = args[0];
|
|
8
|
-
const hasStructuredShape =
|
|
9
|
-
Object.prototype.hasOwnProperty.call(msg, 'message') &&
|
|
10
|
-
Object.prototype.hasOwnProperty.call(msg, 'level');
|
|
11
|
-
return hasStructuredShape;
|
|
12
|
-
}
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
console.log = (...args) => {
|
|
17
|
-
if (shouldSuppress(args)) return;
|
|
18
|
-
originalLog(...args);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
console.error = (...args) => {
|
|
22
|
-
if (shouldSuppress(args)) return;
|
|
23
|
-
originalError(...args);
|
|
24
|
-
};
|
|
@@ -1,531 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Generic Logging Adapter
|
|
3
|
-
*
|
|
4
|
-
* The generic adapter is a thin wrapper around the core Scrubber,
|
|
5
|
-
* designed for use with any logging library (Winston, Pino, Bunyan, etc.)
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { expect } from 'chai';
|
|
9
|
-
import { createRedactor } from './generic.js';
|
|
10
|
-
import { Scrubber } from '../../core/scrubber.js';
|
|
11
|
-
import { HEROKU_FIELDS, GDPR_FIELDS, PCI_FIELDS } from '../../core/presets.js';
|
|
12
|
-
|
|
13
|
-
describe('Generic Logging Adapter', () => {
|
|
14
|
-
describe('createRedactor()', () => {
|
|
15
|
-
it('returns a Scrubber instance', () => {
|
|
16
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
17
|
-
expect(redactor).to.be.instanceOf(Scrubber);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('creates a functional scrubber', () => {
|
|
21
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
22
|
-
const result = redactor.scrub({ user: 'john', password: 'secret' });
|
|
23
|
-
|
|
24
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
25
|
-
expect(result.scrubbed).to.be.true;
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('accepts empty configuration', () => {
|
|
29
|
-
const redactor = createRedactor({});
|
|
30
|
-
const result = redactor.scrub({ data: 'test' });
|
|
31
|
-
|
|
32
|
-
expect(result.data).to.deep.equal({ data: 'test' });
|
|
33
|
-
expect(result.scrubbed).to.be.false;
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('Configuration Options', () => {
|
|
38
|
-
it('supports field-based scrubbing', () => {
|
|
39
|
-
const redactor = createRedactor({
|
|
40
|
-
fields: ['password', 'apiToken', 'secret'],
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const result = redactor.scrub({
|
|
44
|
-
user: 'john',
|
|
45
|
-
password: 'secret123',
|
|
46
|
-
apiToken: 'token123',
|
|
47
|
-
secret: 'hidden',
|
|
48
|
-
public: 'visible',
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
52
|
-
expect(result.data.apiToken).to.equal('[SCRUBBED]');
|
|
53
|
-
expect(result.data.secret).to.equal('[SCRUBBED]');
|
|
54
|
-
expect(result.data.public).to.equal('visible');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('supports regex field patterns', () => {
|
|
58
|
-
const redactor = createRedactor({
|
|
59
|
-
fields: [/api[-_]?key/i, /^secret/i],
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const result = redactor.scrub({
|
|
63
|
-
api_key: 'key1',
|
|
64
|
-
'api-key': 'key2',
|
|
65
|
-
apikey: 'key3',
|
|
66
|
-
secret_token: 'token',
|
|
67
|
-
SECRET: 'hidden',
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(result.data.api_key).to.equal('[SCRUBBED]');
|
|
71
|
-
expect(result.data['api-key']).to.equal('[SCRUBBED]');
|
|
72
|
-
expect(result.data.apikey).to.equal('[SCRUBBED]');
|
|
73
|
-
expect(result.data.secret_token).to.equal('[SCRUBBED]');
|
|
74
|
-
expect(result.data.SECRET).to.equal('[SCRUBBED]');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('supports path-based scrubbing', () => {
|
|
78
|
-
const redactor = createRedactor({
|
|
79
|
-
paths: ['user.email', 'request.headers.authorization'],
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const result = redactor.scrub({
|
|
83
|
-
user: {
|
|
84
|
-
name: 'John',
|
|
85
|
-
email: 'john@example.com',
|
|
86
|
-
},
|
|
87
|
-
request: {
|
|
88
|
-
method: 'POST',
|
|
89
|
-
headers: {
|
|
90
|
-
authorization: 'Bearer token123',
|
|
91
|
-
'content-type': 'application/json',
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(result.data.user.email).to.equal('[SCRUBBED]');
|
|
97
|
-
expect(result.data.request.headers.authorization).to.equal('[SCRUBBED]');
|
|
98
|
-
expect(result.data.user.name).to.equal('John');
|
|
99
|
-
expect(result.data.request.headers['content-type']).to.equal(
|
|
100
|
-
'application/json'
|
|
101
|
-
);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('supports pattern-based scrubbing', () => {
|
|
105
|
-
const redactor = createRedactor({
|
|
106
|
-
patterns: [
|
|
107
|
-
/\b\d{3}-\d{2}-\d{4}\b/g, // SSN
|
|
108
|
-
/\d{4}-\d{4}-\d{4}-\d{4}/g, // Credit card
|
|
109
|
-
],
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const result = redactor.scrub({
|
|
113
|
-
message: 'User SSN is 123-45-6789 and card is 4532-1234-5678-9010',
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
expect(result.data.message).to.equal(
|
|
117
|
-
'User SSN is [SCRUBBED] and card is [SCRUBBED]'
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('supports combined scrubbing modes', () => {
|
|
122
|
-
const redactor = createRedactor({
|
|
123
|
-
fields: ['password'],
|
|
124
|
-
paths: ['user.email'],
|
|
125
|
-
patterns: [/\b\d{3}-\d{2}-\d{4}\b/g],
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const result = redactor.scrub({
|
|
129
|
-
user: {
|
|
130
|
-
name: 'John',
|
|
131
|
-
email: 'john@example.com',
|
|
132
|
-
password: 'secret',
|
|
133
|
-
},
|
|
134
|
-
message: 'SSN: 123-45-6789',
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
expect(result.data.user.password).to.equal('[SCRUBBED]');
|
|
138
|
-
expect(result.data.user.email).to.equal('[SCRUBBED]');
|
|
139
|
-
expect(result.data.message).to.equal('SSN: [SCRUBBED]');
|
|
140
|
-
expect(result.data.user.name).to.equal('John');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('supports custom replacement text', () => {
|
|
144
|
-
const redactor = createRedactor({
|
|
145
|
-
fields: ['password'],
|
|
146
|
-
replacement: '[REDACTED]',
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
const result = redactor.scrub({ password: 'secret' });
|
|
150
|
-
expect(result.data.password).to.equal('[REDACTED]');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('supports recursive scrubbing control', () => {
|
|
154
|
-
const redactorRecursive = createRedactor({
|
|
155
|
-
fields: ['password'],
|
|
156
|
-
recursive: true,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const redactorNonRecursive = createRedactor({
|
|
160
|
-
fields: ['password'],
|
|
161
|
-
recursive: false,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const data = {
|
|
165
|
-
password: 'level1',
|
|
166
|
-
nested: {
|
|
167
|
-
password: 'level2',
|
|
168
|
-
},
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const recursiveResult = redactorRecursive.scrub(data);
|
|
172
|
-
expect(recursiveResult.data.password).to.equal('[SCRUBBED]');
|
|
173
|
-
expect(recursiveResult.data.nested.password).to.equal('[SCRUBBED]');
|
|
174
|
-
|
|
175
|
-
const nonRecursiveResult = redactorNonRecursive.scrub(data);
|
|
176
|
-
expect(nonRecursiveResult.data.password).to.equal('[SCRUBBED]');
|
|
177
|
-
// Non-recursive should still scrub nested fields
|
|
178
|
-
// (the recursive flag controls traversal depth, not field matching)
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('Preset Integration', () => {
|
|
183
|
-
it('works with HEROKU_FIELDS preset', () => {
|
|
184
|
-
const redactor = createRedactor({ fields: HEROKU_FIELDS });
|
|
185
|
-
|
|
186
|
-
const result = redactor.scrub({
|
|
187
|
-
user: 'john',
|
|
188
|
-
password: 'secret',
|
|
189
|
-
access_token: 'token123',
|
|
190
|
-
api_key: 'key123',
|
|
191
|
-
email: 'john@example.com',
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
195
|
-
expect(result.data.access_token).to.equal('[SCRUBBED]');
|
|
196
|
-
expect(result.data.api_key).to.equal('[SCRUBBED]');
|
|
197
|
-
expect(result.data.user).to.equal('john');
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('works with GDPR_FIELDS preset', () => {
|
|
201
|
-
const redactor = createRedactor({ fields: GDPR_FIELDS });
|
|
202
|
-
|
|
203
|
-
const result = redactor.scrub({
|
|
204
|
-
name: 'John Doe',
|
|
205
|
-
email: 'john@example.com',
|
|
206
|
-
phone: '+1-555-1234',
|
|
207
|
-
address: '123 Main St',
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
expect(result.data.email).to.equal('[SCRUBBED]');
|
|
211
|
-
expect(result.data.phone).to.equal('[SCRUBBED]');
|
|
212
|
-
expect(result.data.address).to.equal('[SCRUBBED]');
|
|
213
|
-
expect(result.data.name).to.equal('John Doe');
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('works with PCI_FIELDS preset', () => {
|
|
217
|
-
const redactor = createRedactor({ fields: PCI_FIELDS });
|
|
218
|
-
|
|
219
|
-
const result = redactor.scrub({
|
|
220
|
-
user: 'john',
|
|
221
|
-
card_number: '4532-1234-5678-9010',
|
|
222
|
-
cvv: '123',
|
|
223
|
-
credit_card: '4111111111111111',
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
expect(result.data.card_number).to.equal('[SCRUBBED]');
|
|
227
|
-
expect(result.data.cvv).to.equal('[SCRUBBED]');
|
|
228
|
-
expect(result.data.credit_card).to.equal('[SCRUBBED]');
|
|
229
|
-
expect(result.data.user).to.equal('john');
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('works with combined presets', () => {
|
|
233
|
-
const redactor = createRedactor({
|
|
234
|
-
fields: [...HEROKU_FIELDS, ...GDPR_FIELDS, ...PCI_FIELDS],
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const result = redactor.scrub({
|
|
238
|
-
user: 'john',
|
|
239
|
-
password: 'secret', // HEROKU_FIELDS
|
|
240
|
-
email: 'john@example.com', // GDPR_FIELDS
|
|
241
|
-
cvv: '123', // PCI_FIELDS
|
|
242
|
-
public: 'data',
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
246
|
-
expect(result.data.email).to.equal('[SCRUBBED]');
|
|
247
|
-
expect(result.data.cvv).to.equal('[SCRUBBED]');
|
|
248
|
-
expect(result.data.public).to.equal('data');
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('Logging Library Integration Patterns', () => {
|
|
253
|
-
it('can be used with Winston-style loggers', () => {
|
|
254
|
-
// Simulate Winston logger integration
|
|
255
|
-
const redactor = createRedactor({ fields: ['password', 'apiToken'] });
|
|
256
|
-
|
|
257
|
-
// Simulate Winston's meta object
|
|
258
|
-
const logEntry = {
|
|
259
|
-
level: 'info',
|
|
260
|
-
message: 'User login',
|
|
261
|
-
metadata: {
|
|
262
|
-
user: 'john',
|
|
263
|
-
password: 'secret123',
|
|
264
|
-
apiToken: 'token123',
|
|
265
|
-
ip: '192.168.1.1',
|
|
266
|
-
},
|
|
267
|
-
timestamp: new Date().toISOString(),
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
const result = redactor.scrub(logEntry);
|
|
271
|
-
|
|
272
|
-
expect(result.data.metadata.password).to.equal('[SCRUBBED]');
|
|
273
|
-
expect(result.data.metadata.apiToken).to.equal('[SCRUBBED]');
|
|
274
|
-
expect(result.data.metadata.user).to.equal('john');
|
|
275
|
-
expect(result.data.level).to.equal('info');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('can be used with Pino-style loggers', () => {
|
|
279
|
-
// Simulate Pino logger integration
|
|
280
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
281
|
-
|
|
282
|
-
// Simulate Pino's flat log object
|
|
283
|
-
const logEntry = {
|
|
284
|
-
level: 30, // info
|
|
285
|
-
time: Date.now(),
|
|
286
|
-
msg: 'User action',
|
|
287
|
-
user: 'john',
|
|
288
|
-
password: 'secret',
|
|
289
|
-
req: {
|
|
290
|
-
method: 'POST',
|
|
291
|
-
url: '/api/login',
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const result = redactor.scrub(logEntry);
|
|
296
|
-
|
|
297
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
298
|
-
expect(result.data.user).to.equal('john');
|
|
299
|
-
expect(result.data.req.method).to.equal('POST');
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('can be used with Bunyan-style loggers', () => {
|
|
303
|
-
// Simulate Bunyan logger integration
|
|
304
|
-
const redactor = createRedactor({ fields: ['password', 'token'] });
|
|
305
|
-
|
|
306
|
-
const logEntry = {
|
|
307
|
-
name: 'myapp',
|
|
308
|
-
hostname: 'server01',
|
|
309
|
-
pid: 12345,
|
|
310
|
-
level: 30,
|
|
311
|
-
msg: 'Authentication successful',
|
|
312
|
-
password: 'secret',
|
|
313
|
-
token: 'jwt123',
|
|
314
|
-
time: new Date(),
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const result = redactor.scrub(logEntry);
|
|
318
|
-
|
|
319
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
320
|
-
expect(result.data.token).to.equal('[SCRUBBED]');
|
|
321
|
-
expect(result.data.msg).to.equal('Authentication successful');
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('handles array of log entries', () => {
|
|
325
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
326
|
-
|
|
327
|
-
const logs = [
|
|
328
|
-
{ msg: 'Login attempt', user: 'john', password: 'secret1' },
|
|
329
|
-
{ msg: 'Login success', user: 'jane', password: 'secret2' },
|
|
330
|
-
{ msg: 'Logout', user: 'john' },
|
|
331
|
-
];
|
|
332
|
-
|
|
333
|
-
const result = redactor.scrub(logs);
|
|
334
|
-
|
|
335
|
-
expect(result.data[0]?.password).to.equal('[SCRUBBED]');
|
|
336
|
-
expect(result.data[1]?.password).to.equal('[SCRUBBED]');
|
|
337
|
-
expect(result.data[2]?.password).to.be.undefined;
|
|
338
|
-
expect(result.data[0]?.user).to.equal('john');
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
describe('oauth-provider-adapters-for-mcp Migration', () => {
|
|
343
|
-
it('provides drop-in replacement for redaction.ts', () => {
|
|
344
|
-
// This test validates the interface matches what oauth-provider-adapters expects
|
|
345
|
-
const redactor = createRedactor({
|
|
346
|
-
fields: ['client_secret', 'refresh_token', 'access_token'],
|
|
347
|
-
paths: ['oauth.credentials.secret'],
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// Simulate OAuth data structure
|
|
351
|
-
const oauthData = {
|
|
352
|
-
client_id: 'my-client',
|
|
353
|
-
client_secret: 'secret123',
|
|
354
|
-
redirect_uri: 'http://localhost:3000/callback',
|
|
355
|
-
oauth: {
|
|
356
|
-
credentials: {
|
|
357
|
-
access_token: 'access123',
|
|
358
|
-
refresh_token: 'refresh123',
|
|
359
|
-
secret: 'hidden',
|
|
360
|
-
},
|
|
361
|
-
},
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
const result = redactor.scrub(oauthData);
|
|
365
|
-
|
|
366
|
-
// Verify the interface provides .data (scrubbed result)
|
|
367
|
-
expect(result).to.have.property('data');
|
|
368
|
-
expect(result).to.have.property('scrubbed');
|
|
369
|
-
expect(result).to.have.property('scrubbedPaths');
|
|
370
|
-
|
|
371
|
-
// Verify scrubbing worked
|
|
372
|
-
expect(result.data.client_secret).to.equal('[SCRUBBED]');
|
|
373
|
-
expect(result.data.oauth.credentials.access_token).to.equal('[SCRUBBED]');
|
|
374
|
-
expect(result.data.oauth.credentials.refresh_token).to.equal(
|
|
375
|
-
'[SCRUBBED]'
|
|
376
|
-
);
|
|
377
|
-
expect(result.data.oauth.credentials.secret).to.equal('[SCRUBBED]');
|
|
378
|
-
expect(result.data.client_id).to.equal('my-client');
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('supports the same API as oauth-provider redaction', () => {
|
|
382
|
-
const redactor = createRedactor({ fields: ['secret'] });
|
|
383
|
-
|
|
384
|
-
// The API should support:
|
|
385
|
-
// 1. scrub() method
|
|
386
|
-
expect(redactor).to.have.property('scrub');
|
|
387
|
-
expect(typeof redactor.scrub).to.equal('function');
|
|
388
|
-
|
|
389
|
-
// 2. Return value with .data property
|
|
390
|
-
const result = redactor.scrub({ secret: 'hidden' });
|
|
391
|
-
expect(result).to.have.property('data');
|
|
392
|
-
|
|
393
|
-
// 3. Immutability (original not modified)
|
|
394
|
-
const original = { secret: 'hidden', public: 'visible' };
|
|
395
|
-
const scrubbed = redactor.scrub(original);
|
|
396
|
-
expect(original.secret).to.equal('hidden'); // Original unchanged
|
|
397
|
-
expect(scrubbed.data.secret).to.equal('[SCRUBBED]');
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
describe('Performance', () => {
|
|
402
|
-
it('handles high-volume logging scenarios efficiently', () => {
|
|
403
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
404
|
-
|
|
405
|
-
const start = performance.now();
|
|
406
|
-
const iterations = 1000;
|
|
407
|
-
|
|
408
|
-
for (let i = 0; i < iterations; i++) {
|
|
409
|
-
redactor.scrub({
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
411
|
-
level: 'info',
|
|
412
|
-
message: 'User action',
|
|
413
|
-
user: `user${i}`,
|
|
414
|
-
password: `secret${i}`,
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const end = performance.now();
|
|
419
|
-
const avgTime = (end - start) / iterations;
|
|
420
|
-
|
|
421
|
-
// Should average < 1ms per log entry (target from discovery doc)
|
|
422
|
-
expect(avgTime).to.be.lessThan(1);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('handles large nested objects efficiently', () => {
|
|
426
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
427
|
-
|
|
428
|
-
// Create a large nested structure
|
|
429
|
-
const largeObject: Record<string, unknown> = {
|
|
430
|
-
level: 'info',
|
|
431
|
-
timestamp: Date.now(),
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
for (let i = 0; i < 100; i++) {
|
|
435
|
-
largeObject[`user${i}`] = {
|
|
436
|
-
id: i,
|
|
437
|
-
name: `User ${i}`,
|
|
438
|
-
password: `secret${i}`,
|
|
439
|
-
metadata: {
|
|
440
|
-
lastLogin: new Date().toISOString(),
|
|
441
|
-
loginCount: i * 10,
|
|
442
|
-
},
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const start = performance.now();
|
|
447
|
-
const result = redactor.scrub(largeObject);
|
|
448
|
-
const end = performance.now();
|
|
449
|
-
|
|
450
|
-
expect(end - start).to.be.lessThan(10); // Should be < 10ms
|
|
451
|
-
expect(result.scrubbed).to.be.true;
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
describe('Edge Cases', () => {
|
|
456
|
-
it('handles null and undefined values', () => {
|
|
457
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
458
|
-
|
|
459
|
-
const result = redactor.scrub({
|
|
460
|
-
user: 'john',
|
|
461
|
-
password: null,
|
|
462
|
-
apiToken: undefined,
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
// Fields matching the scrubber config are scrubbed regardless of value
|
|
466
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
467
|
-
// Fields not in the config are left as-is
|
|
468
|
-
expect(result.data.apiToken).to.be.undefined;
|
|
469
|
-
expect(result.data.user).to.equal('john');
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('handles circular references', () => {
|
|
473
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
474
|
-
|
|
475
|
-
const obj: Record<string, unknown> = {
|
|
476
|
-
user: 'john',
|
|
477
|
-
password: 'secret',
|
|
478
|
-
};
|
|
479
|
-
obj.self = obj; // Circular reference
|
|
480
|
-
|
|
481
|
-
const result = redactor.scrub(obj);
|
|
482
|
-
|
|
483
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
484
|
-
expect(result.data.user).to.equal('john');
|
|
485
|
-
// Circular reference handled gracefully
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
it('handles empty objects and arrays', () => {
|
|
489
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
490
|
-
|
|
491
|
-
expect(redactor.scrub({}).data).to.deep.equal({});
|
|
492
|
-
expect(redactor.scrub([]).data).to.deep.equal([]);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('handles primitive values', () => {
|
|
496
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
497
|
-
|
|
498
|
-
expect(redactor.scrub('string').data).to.equal('string');
|
|
499
|
-
expect(redactor.scrub(42).data).to.equal(42);
|
|
500
|
-
expect(redactor.scrub(true).data).to.equal(true);
|
|
501
|
-
expect(redactor.scrub(null).data).to.equal(null);
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
describe('Type Safety', () => {
|
|
506
|
-
it('preserves TypeScript types', () => {
|
|
507
|
-
interface LogEntry {
|
|
508
|
-
level: string;
|
|
509
|
-
message: string;
|
|
510
|
-
user: string;
|
|
511
|
-
password: string;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const redactor = createRedactor({ fields: ['password'] });
|
|
515
|
-
const entry: LogEntry = {
|
|
516
|
-
level: 'info',
|
|
517
|
-
message: 'User login',
|
|
518
|
-
user: 'john',
|
|
519
|
-
password: 'secret',
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
const result = redactor.scrub(entry);
|
|
523
|
-
|
|
524
|
-
// result.data should maintain LogEntry type
|
|
525
|
-
expect(result.data.level).to.be.a('string');
|
|
526
|
-
expect(result.data.message).to.be.a('string');
|
|
527
|
-
expect(result.data.user).to.be.a('string');
|
|
528
|
-
expect(result.data.password).to.equal('[SCRUBBED]');
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
});
|
package/src/core/patterns.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regex patterns for detecting PII in string content
|
|
3
|
-
*/
|
|
4
|
-
export const PII_PATTERNS = [
|
|
5
|
-
// Social Security Numbers (US)
|
|
6
|
-
/\b\d{3}-\d{2}-\d{4}\b/g,
|
|
7
|
-
|
|
8
|
-
// Credit Cards (basic - matches common formats)
|
|
9
|
-
/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
10
|
-
|
|
11
|
-
// Email addresses
|
|
12
|
-
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
13
|
-
|
|
14
|
-
// Phone numbers (US format)
|
|
15
|
-
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
|
|
16
|
-
|
|
17
|
-
// JWT tokens
|
|
18
|
-
/\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
|
|
19
|
-
|
|
20
|
-
// API keys (generic 32+ char alphanumeric strings)
|
|
21
|
-
/\b[A-Za-z0-9]{32,}\b/g,
|
|
22
|
-
];
|