@lowdefy/node-utils 0.0.0-experimental-20261007124348 → 0.0.0-experimental-20261009120232
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/bindCredentialScrub.js +26 -0
- package/dist/collectStringLeaves.js +30 -0
- package/dist/createSecretScrubber.js +8 -92
- package/dist/createValueScrubber.js +101 -0
- package/dist/credentialStorage.js +21 -0
- package/dist/getMarkedCredentials.js +27 -0
- package/dist/index.js +6 -1
- package/dist/markCredential.js +31 -0
- package/dist/runInCredentialScope.js +23 -0
- package/dist/scrubCredentials.js +30 -0
- package/package.json +3 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import credentialStorage from './credentialStorage.js';
|
|
16
|
+
import scrubCredentials from './scrubCredentials.js';
|
|
17
|
+
// Returns a scrub for the current scope's marked credentials that keeps working after the scope
|
|
18
|
+
// has ended, for work a request leaves behind, such as the Sentry transaction sent once the
|
|
19
|
+
// response is written. Values marked later in the scope are scrubbed too.
|
|
20
|
+
function bindCredentialScrub() {
|
|
21
|
+
const scope = credentialStorage.getStore();
|
|
22
|
+
return function scrubBoundCredentials(value) {
|
|
23
|
+
return scrubCredentials(value, scope);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export default bindCredentialScrub;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
function collectStringLeaves(value, leaves = []) {
|
|
17
|
+
if (type.isString(value)) {
|
|
18
|
+
leaves.push(value);
|
|
19
|
+
return leaves;
|
|
20
|
+
}
|
|
21
|
+
if (type.isArray(value)) {
|
|
22
|
+
value.forEach((item)=>collectStringLeaves(item, leaves));
|
|
23
|
+
return leaves;
|
|
24
|
+
}
|
|
25
|
+
if (type.isObject(value)) {
|
|
26
|
+
Object.values(value).forEach((item)=>collectStringLeaves(item, leaves));
|
|
27
|
+
}
|
|
28
|
+
return leaves;
|
|
29
|
+
}
|
|
30
|
+
export default collectStringLeaves;
|
|
@@ -13,102 +13,18 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
leaves.push(value);
|
|
22
|
-
return leaves;
|
|
23
|
-
}
|
|
24
|
-
if (type.isArray(value)) {
|
|
25
|
-
value.forEach((item)=>collectStringLeaves(item, leaves));
|
|
26
|
-
return leaves;
|
|
27
|
-
}
|
|
28
|
-
if (type.isObject(value)) {
|
|
29
|
-
Object.values(value).forEach((item)=>collectStringLeaves(item, leaves));
|
|
30
|
-
}
|
|
31
|
-
return leaves;
|
|
32
|
-
}
|
|
33
|
-
// Authors JSON-encode structured secrets and pick a leaf with `_json.parse`, so the leaf, not
|
|
34
|
-
// the whole encoded string, is what ends up in an error message.
|
|
35
|
-
function parseJsonLeaves(value) {
|
|
36
|
-
let parsed;
|
|
37
|
-
try {
|
|
38
|
-
parsed = JSON.parse(value);
|
|
39
|
-
} catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
if (!type.isObject(parsed) && !type.isArray(parsed)) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
return collectStringLeaves(parsed, []);
|
|
46
|
-
}
|
|
47
|
-
// A secret embedded in a larger base64 payload (a Basic auth header built from
|
|
48
|
-
// `user:password`) only matches the secret's own base64 when both start on the same 3-byte
|
|
49
|
-
// boundary. Encoding at each offset and keeping only the characters whose 6 bits fall wholly
|
|
50
|
-
// inside the secret's bytes gives a fragment that matches whatever surrounds it.
|
|
51
|
-
function base64Fragments(value) {
|
|
52
|
-
const bytes = Buffer.from(value, 'utf8');
|
|
53
|
-
const fragments = [];
|
|
54
|
-
[
|
|
55
|
-
0,
|
|
56
|
-
1,
|
|
57
|
-
2
|
|
58
|
-
].forEach((offset)=>{
|
|
59
|
-
const padded = Buffer.concat([
|
|
60
|
-
Buffer.alloc(offset),
|
|
61
|
-
bytes
|
|
62
|
-
]);
|
|
63
|
-
const start = Math.ceil(8 * offset / 6);
|
|
64
|
-
const end = Math.floor(8 * (offset + bytes.length) / 6);
|
|
65
|
-
[
|
|
66
|
-
'base64',
|
|
67
|
-
'base64url'
|
|
68
|
-
].forEach((encoding)=>{
|
|
69
|
-
fragments.push(padded.toString(encoding).replace(/=+$/, '').slice(start, end));
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
return fragments;
|
|
73
|
-
}
|
|
74
|
-
function secretForms(value) {
|
|
75
|
-
return [
|
|
76
|
-
value,
|
|
77
|
-
encodeURIComponent(value),
|
|
78
|
-
JSON.stringify(value).slice(1, -1),
|
|
79
|
-
...base64Fragments(value)
|
|
80
|
-
];
|
|
81
|
-
}
|
|
16
|
+
import collectStringLeaves from './collectStringLeaves.js';
|
|
17
|
+
import createValueScrubber from './createValueScrubber.js';
|
|
18
|
+
import scrubCredentials from './scrubCredentials.js';
|
|
19
|
+
// The scrub replaces the app's secrets, and the values the current request marked as
|
|
20
|
+
// credentials (markCredential), with [REDACTED].
|
|
82
21
|
function createSecretScrubber({ secrets, env = process.env }) {
|
|
83
|
-
const values = collectStringLeaves(secrets
|
|
22
|
+
const values = collectStringLeaves(secrets);
|
|
84
23
|
if (!type.isNone(env.CRON_SECRET)) values.push(env.CRON_SECRET);
|
|
85
24
|
if (!type.isNone(env.BETTER_AUTH_SECRET)) values.push(env.BETTER_AUTH_SECRET);
|
|
86
|
-
const
|
|
87
|
-
value,
|
|
88
|
-
...parseJsonLeaves(value)
|
|
89
|
-
]);
|
|
90
|
-
const secretValues = new Set(expanded.filter((value)=>value.length >= MIN_SECRET_LENGTH));
|
|
91
|
-
const forms = new Set();
|
|
92
|
-
secretValues.forEach((value)=>{
|
|
93
|
-
secretForms(value).forEach((form)=>{
|
|
94
|
-
if (form.length > 0) forms.add(form);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
// No secrets means no pattern: an empty alternation would match at every position.
|
|
98
|
-
if (forms.size === 0) {
|
|
99
|
-
return function scrub(value) {
|
|
100
|
-
return value;
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
// One alternation, so every log line and Sentry string takes a single pass however many
|
|
104
|
-
// secrets there are. Longest first, so a form is replaced whole before a shorter form it
|
|
105
|
-
// contains can match at the same position.
|
|
106
|
-
const pattern = new RegExp([
|
|
107
|
-
...forms
|
|
108
|
-
].sort((a, b)=>b.length - a.length).map((form)=>form.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
25
|
+
const scrubValues = createValueScrubber(values);
|
|
109
26
|
return function scrub(value) {
|
|
110
|
-
|
|
111
|
-
return value.replace(pattern, REDACTED);
|
|
27
|
+
return scrubCredentials(scrubValues(value));
|
|
112
28
|
};
|
|
113
29
|
}
|
|
114
30
|
export default createSecretScrubber;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import collectStringLeaves from './collectStringLeaves.js';
|
|
17
|
+
const REDACTED = '[REDACTED]';
|
|
18
|
+
// Shorter values, like `true` or a port number, would shred unrelated text.
|
|
19
|
+
const MIN_SECRET_LENGTH = 8;
|
|
20
|
+
// Authors JSON-encode structured secrets and pick a leaf with `_json.parse`, so the leaf, not
|
|
21
|
+
// the whole encoded string, is what ends up in an error message.
|
|
22
|
+
function parseJsonLeaves(value) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (!type.isObject(parsed) && !type.isArray(parsed)) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return collectStringLeaves(parsed);
|
|
33
|
+
}
|
|
34
|
+
// A secret embedded in a larger base64 payload (a Basic auth header built from
|
|
35
|
+
// `user:password`) only matches the secret's own base64 when both start on the same 3-byte
|
|
36
|
+
// boundary. Encoding at each offset and keeping only the characters whose 6 bits fall wholly
|
|
37
|
+
// inside the secret's bytes gives a fragment that matches whatever surrounds it.
|
|
38
|
+
function base64Fragments(value) {
|
|
39
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
40
|
+
const fragments = [];
|
|
41
|
+
[
|
|
42
|
+
0,
|
|
43
|
+
1,
|
|
44
|
+
2
|
|
45
|
+
].forEach((offset)=>{
|
|
46
|
+
const padded = Buffer.concat([
|
|
47
|
+
Buffer.alloc(offset),
|
|
48
|
+
bytes
|
|
49
|
+
]);
|
|
50
|
+
const start = Math.ceil(8 * offset / 6);
|
|
51
|
+
const end = Math.floor(8 * (offset + bytes.length) / 6);
|
|
52
|
+
[
|
|
53
|
+
'base64',
|
|
54
|
+
'base64url'
|
|
55
|
+
].forEach((encoding)=>{
|
|
56
|
+
fragments.push(padded.toString(encoding).replace(/=+$/, '').slice(start, end));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return fragments;
|
|
60
|
+
}
|
|
61
|
+
function secretForms(value) {
|
|
62
|
+
return [
|
|
63
|
+
value,
|
|
64
|
+
encodeURIComponent(value),
|
|
65
|
+
JSON.stringify(value).slice(1, -1),
|
|
66
|
+
...base64Fragments(value)
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
function returnUnchanged(value) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
// Returns a function that replaces each of the values, and the forms they take once encoded,
|
|
73
|
+
// with [REDACTED] in a string.
|
|
74
|
+
function createValueScrubber(values) {
|
|
75
|
+
const expanded = values.flatMap((value)=>[
|
|
76
|
+
value,
|
|
77
|
+
...parseJsonLeaves(value)
|
|
78
|
+
]);
|
|
79
|
+
const secretValues = new Set(expanded.filter((value)=>value.length >= MIN_SECRET_LENGTH));
|
|
80
|
+
const forms = new Set();
|
|
81
|
+
secretValues.forEach((value)=>{
|
|
82
|
+
secretForms(value).forEach((form)=>{
|
|
83
|
+
if (form.length > 0) forms.add(form);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// No values means no pattern: an empty alternation would match at every position.
|
|
87
|
+
if (forms.size === 0) {
|
|
88
|
+
return returnUnchanged;
|
|
89
|
+
}
|
|
90
|
+
// One alternation, so every log line and Sentry string takes a single pass however many
|
|
91
|
+
// values there are. Longest first, so a form is replaced whole before a shorter form it
|
|
92
|
+
// contains can match at the same position.
|
|
93
|
+
const pattern = new RegExp([
|
|
94
|
+
...forms
|
|
95
|
+
].sort((a, b)=>b.length - a.length).map((form)=>form.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
96
|
+
return function scrub(value) {
|
|
97
|
+
if (!type.isString(value)) return value;
|
|
98
|
+
return value.replace(pattern, REDACTED);
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export default createValueScrubber;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
|
+
// Holds the credentials marked while one request is handled, so a log line written anywhere in
|
|
17
|
+
// that request, by any logger, can be scrubbed of them. The store is { values, scrub }: values is
|
|
18
|
+
// the Set of marked strings, and scrub the scrubber built from them, or null until the next line
|
|
19
|
+
// needs it.
|
|
20
|
+
const credentialStorage = new AsyncLocalStorage();
|
|
21
|
+
export default credentialStorage;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import credentialStorage from './credentialStorage.js';
|
|
16
|
+
// The values marked so far in the current scope, so work the request hands to another request
|
|
17
|
+
// (a detached CallApi) can mark them there too. Outside any scope nothing has been marked.
|
|
18
|
+
function getMarkedCredentials() {
|
|
19
|
+
const scope = credentialStorage.getStore();
|
|
20
|
+
if (scope === undefined) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
...scope.values
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
export default getMarkedCredentials;
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import acquireMachineSlot from './acquireMachineSlot.js';
|
|
16
|
+
import bindCredentialScrub from './bindCredentialScrub.js';
|
|
16
17
|
import checkEnvironmentGuards from './checkEnvironmentGuards.js';
|
|
17
18
|
import cleanDirectory from './cleanDirectory.js';
|
|
18
19
|
import collectEnvironmentGuards from './collectEnvironmentGuards.js';
|
|
@@ -29,6 +30,7 @@ import findPnpmWorkspaceRoot from './findPnpmWorkspaceRoot.js';
|
|
|
29
30
|
import findWorkspacePackages from './findWorkspacePackages.js';
|
|
30
31
|
import getDevInstancePath from './getDevInstancePath.js';
|
|
31
32
|
import getFileExtension, { getFileSubExtension } from './getFileExtension.js';
|
|
33
|
+
import getMarkedCredentials from './getMarkedCredentials.js';
|
|
32
34
|
import getLowdefyHome from './getLowdefyHome.js';
|
|
33
35
|
import getProcessStartTime from './getProcessStartTime.js';
|
|
34
36
|
import getSecretsFromEnv from './getSecretsFromEnv.js';
|
|
@@ -54,6 +56,8 @@ import readServerRegistry from './readServerRegistry.js';
|
|
|
54
56
|
import registerServer from './registerServer.js';
|
|
55
57
|
import listRecordingFiles, { RECORDING_SOURCES } from './recordings/listRecordingFiles.js';
|
|
56
58
|
import readRecordings from './recordings/readRecordings.js';
|
|
59
|
+
import runInCredentialScope from './runInCredentialScope.js';
|
|
60
|
+
import scrubCredentials from './scrubCredentials.js';
|
|
57
61
|
import spawnProcess from './spawnProcess.js';
|
|
58
62
|
import readFile from './readFile.js';
|
|
59
63
|
import writeFile from './writeFile.js';
|
|
@@ -72,6 +76,7 @@ import journeyStepSchema, { JOURNEY_STEP_SCHEMAS } from './journeyGrammar/journe
|
|
|
72
76
|
import hashSequence from './journeyCompiler/hashSequence.js';
|
|
73
77
|
import journeySequence from './journeyCompiler/journeySequence.js';
|
|
74
78
|
import listFailurePaths from './journeyCompiler/listFailurePaths.js';
|
|
79
|
+
import markCredential from './markCredential.js';
|
|
75
80
|
import parseTraceLines from './journeyCompiler/parseTraceLines.js';
|
|
76
81
|
import profileProduction from './journeyEvidence/profileProduction.js';
|
|
77
82
|
import buildSessionReport from './sessionLog/buildSessionReport.js';
|
|
@@ -83,4 +88,4 @@ import validateJourneySteps, { getStepKey, INTERACTION_STEP_KEYS, STEP_KEYS, TAR
|
|
|
83
88
|
import validateJourneyTags, { JOURNEY_TAG_PATTERN } from './journeyGrammar/validateJourneyTags.js';
|
|
84
89
|
import validateJourneyUser from './journeyGrammar/validateJourneyUser.js';
|
|
85
90
|
import validateTraceRecord from './journeyTrace/validateTraceRecord.js';
|
|
86
|
-
export { acquireMachineSlot, buildSessionReport, checkEnvironmentGuards, cleanDirectory, collectEnvironmentGuards, compareProcessStartTimes, compileSegments, copyFileOrDirectory, countDataSetDocuments, countTextTokens, createClientAddressResolver, createConnectPublic, createLookupPublicAddress, createSecretScrubber, devPassiveHeader, dataSetNamePattern, failurePathKey, findAvailablePort, findPlaceholderStep, findPnpmWorkspaceRoot, formatSessionLog, formatSessionReport, findWorkspacePackages, getDevInstancePath, getFileExtension, getFileSubExtension, getLowdefyHome, getProcessStartTime, getSecretsFromEnv, hashSequence, installIfPackageJsonChanged, isBackedBy, isPidAlive, isProcessAlive, isPublicAddress, isProcessStartTime, isPortAvailable, journeySequence, journeyStepSchema, linkDependenciesToWorkspace, linkWorkspaceDependencies, listDataSets, parseDataSet, listFailurePaths, listRecordingFiles, matchPagePath, normaliseBlockId, normaliseJourneySteps, normaliseClickText, parseIpRange, parseSince, parsePsStartTime, parseTraceLines, profileProduction, readDevInstance, readDevInstanceAsync, readProcessStartTime, readServerRegistry, registerServer, readRecordings, RECORDING_SOURCES, spawnProcess, stepIdentity, summariseSessions, readFile, writeFile, writeFileAtomic, watchOwner, writeFileIfChanged, getStepKey, INTERACTION_STEP_KEYS, JOURNEY_STEP_SCHEMAS, STEP_KEYS, TARGET_KEYS, validateJourneySteps, validateTraceRecord, JOURNEY_TAG_PATTERN, validateJourneyTags, validateJourneyUser };
|
|
91
|
+
export { acquireMachineSlot, bindCredentialScrub, buildSessionReport, checkEnvironmentGuards, cleanDirectory, collectEnvironmentGuards, compareProcessStartTimes, compileSegments, copyFileOrDirectory, countDataSetDocuments, countTextTokens, createClientAddressResolver, createConnectPublic, createLookupPublicAddress, createSecretScrubber, devPassiveHeader, dataSetNamePattern, failurePathKey, findAvailablePort, findPlaceholderStep, findPnpmWorkspaceRoot, formatSessionLog, formatSessionReport, findWorkspacePackages, getDevInstancePath, getFileExtension, getFileSubExtension, getMarkedCredentials, getLowdefyHome, getProcessStartTime, getSecretsFromEnv, hashSequence, installIfPackageJsonChanged, isBackedBy, isPidAlive, isProcessAlive, isPublicAddress, isProcessStartTime, isPortAvailable, journeySequence, journeyStepSchema, linkDependenciesToWorkspace, linkWorkspaceDependencies, listDataSets, parseDataSet, listFailurePaths, listRecordingFiles, markCredential, matchPagePath, normaliseBlockId, normaliseJourneySteps, normaliseClickText, parseIpRange, parseSince, parsePsStartTime, parseTraceLines, profileProduction, readDevInstance, readDevInstanceAsync, readProcessStartTime, readServerRegistry, registerServer, readRecordings, RECORDING_SOURCES, runInCredentialScope, scrubCredentials, spawnProcess, stepIdentity, summariseSessions, readFile, writeFile, writeFileAtomic, watchOwner, writeFileIfChanged, getStepKey, INTERACTION_STEP_KEYS, JOURNEY_STEP_SCHEMAS, STEP_KEYS, TARGET_KEYS, validateJourneySteps, validateTraceRecord, JOURNEY_TAG_PATTERN, validateJourneyTags, validateJourneyUser };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import collectStringLeaves from './collectStringLeaves.js';
|
|
16
|
+
import credentialStorage from './credentialStorage.js';
|
|
17
|
+
// Every string in value is scrubbed from the log lines of the rest of the current scope.
|
|
18
|
+
function markCredential(value) {
|
|
19
|
+
const scope = credentialStorage.getStore();
|
|
20
|
+
if (scope === undefined) {
|
|
21
|
+
throw new Error('A credential can only be marked while the server handles a request.');
|
|
22
|
+
}
|
|
23
|
+
const count = scope.values.size;
|
|
24
|
+
collectStringLeaves(value).forEach((leaf)=>scope.values.add(leaf));
|
|
25
|
+
// The scrub is rebuilt from every marked value on the next log line, not here, so a routine that
|
|
26
|
+
// marks many values pays for one build, and one that writes no line pays for none.
|
|
27
|
+
if (scope.values.size !== count) {
|
|
28
|
+
scope.scrub = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export default markCredential;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import credentialStorage from './credentialStorage.js';
|
|
16
|
+
// Runs fn, and everything it starts, with its own empty set of marked credentials.
|
|
17
|
+
function runInCredentialScope(fn) {
|
|
18
|
+
return credentialStorage.run({
|
|
19
|
+
values: new Set(),
|
|
20
|
+
scrub: null
|
|
21
|
+
}, fn);
|
|
22
|
+
}
|
|
23
|
+
export default runInCredentialScope;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import createValueScrubber from './createValueScrubber.js';
|
|
16
|
+
import credentialStorage from './credentialStorage.js';
|
|
17
|
+
// Replaces the credentials marked in a scope, by default the current one, with [REDACTED]. A line
|
|
18
|
+
// written outside any scope (server start-up) has none to replace.
|
|
19
|
+
function scrubCredentials(value, scope = credentialStorage.getStore()) {
|
|
20
|
+
if (scope === undefined) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (scope.scrub === null) {
|
|
24
|
+
scope.scrub = createValueScrubber([
|
|
25
|
+
...scope.values
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
return scope.scrub(value);
|
|
29
|
+
}
|
|
30
|
+
export default scrubCredentials;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lowdefy/node-utils",
|
|
3
|
-
"version": "0.0.0-experimental-
|
|
3
|
+
"version": "0.0.0-experimental-20261009120232",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"homepage": "https://lowdefy.com",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"dist/*"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@lowdefy/errors": "0.0.0-experimental-
|
|
38
|
-
"@lowdefy/helpers": "0.0.0-experimental-
|
|
37
|
+
"@lowdefy/errors": "0.0.0-experimental-20261009120232",
|
|
38
|
+
"@lowdefy/helpers": "0.0.0-experimental-20261009120232",
|
|
39
39
|
"fs-extra": "11.3.4",
|
|
40
40
|
"undici": "7.30.0",
|
|
41
41
|
"yaml": "2.9.0"
|