@lowdefy/node-utils 0.0.0-experimental-20260914131506 → 0.0.0-experimental-20260925210809
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/createSecretScrubber.js +106 -0
- package/dist/getDevInstancePath.js +22 -0
- package/dist/index.js +5 -1
- package/dist/isPidAlive.js +24 -0
- package/dist/readDevInstance.js +48 -0
- package/package.json +3 -3
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
const REDACTED = '[REDACTED]';
|
|
17
|
+
// Shorter values, like `true` or a port number, would shred unrelated text.
|
|
18
|
+
const MIN_SECRET_LENGTH = 8;
|
|
19
|
+
function collectStringLeaves(value, leaves) {
|
|
20
|
+
if (type.isString(value)) {
|
|
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
|
+
}
|
|
82
|
+
function createSecretScrubber({ secrets, env = process.env }) {
|
|
83
|
+
const values = collectStringLeaves(secrets, []);
|
|
84
|
+
if (!type.isNone(env.CRON_SECRET)) values.push(env.CRON_SECRET);
|
|
85
|
+
if (!type.isNone(env.BETTER_AUTH_SECRET)) values.push(env.BETTER_AUTH_SECRET);
|
|
86
|
+
const expanded = values.flatMap((value)=>[
|
|
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
|
+
// Longest first, so a form is replaced whole before a shorter form it contains can break it up.
|
|
98
|
+
const sortedForms = [
|
|
99
|
+
...forms
|
|
100
|
+
].sort((a, b)=>b.length - a.length);
|
|
101
|
+
return function scrub(value) {
|
|
102
|
+
if (!type.isString(value)) return value;
|
|
103
|
+
return sortedForms.reduce((text, form)=>text.split(form).join(REDACTED), value);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export default createSecretScrubber;
|
|
@@ -0,0 +1,22 @@
|
|
|
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 path from 'path';
|
|
16
|
+
// Keyed on the config directory, not the dev server directory, so one app has
|
|
17
|
+
// one record however it was launched (CLI, --dev-directory, the monorepo dev
|
|
18
|
+
// script) and every reader - CLI, MCP shim, hub - finds it the same way.
|
|
19
|
+
function getDevInstancePath({ configDirectory }) {
|
|
20
|
+
return path.join(configDirectory, '.lowdefy', 'instance.json');
|
|
21
|
+
}
|
|
22
|
+
export default getDevInstancePath;
|
package/dist/index.js
CHANGED
|
@@ -14,13 +14,17 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import cleanDirectory from './cleanDirectory.js';
|
|
16
16
|
import copyFileOrDirectory from './copyFileOrDirectory.js';
|
|
17
|
+
import createSecretScrubber from './createSecretScrubber.js';
|
|
17
18
|
import findAvailablePort from './findAvailablePort.js';
|
|
19
|
+
import getDevInstancePath from './getDevInstancePath.js';
|
|
18
20
|
import getFileExtension, { getFileSubExtension } from './getFileExtension.js';
|
|
19
21
|
import getSecretsFromEnv from './getSecretsFromEnv.js';
|
|
20
22
|
import installIfPackageJsonChanged from './installIfPackageJsonChanged.js';
|
|
23
|
+
import isPidAlive from './isPidAlive.js';
|
|
21
24
|
import isPortAvailable from './isPortAvailable.js';
|
|
25
|
+
import readDevInstance from './readDevInstance.js';
|
|
22
26
|
import spawnProcess from './spawnProcess.js';
|
|
23
27
|
import readFile from './readFile.js';
|
|
24
28
|
import writeFile from './writeFile.js';
|
|
25
29
|
import writeFileIfChanged from './writeFileIfChanged.js';
|
|
26
|
-
export { cleanDirectory, copyFileOrDirectory, findAvailablePort, getFileExtension, getFileSubExtension, getSecretsFromEnv, installIfPackageJsonChanged, isPortAvailable, spawnProcess, readFile, writeFile, writeFileIfChanged };
|
|
30
|
+
export { cleanDirectory, copyFileOrDirectory, createSecretScrubber, findAvailablePort, getDevInstancePath, getFileExtension, getFileSubExtension, getSecretsFromEnv, installIfPackageJsonChanged, isPidAlive, isPortAvailable, readDevInstance, spawnProcess, readFile, writeFile, writeFileIfChanged };
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
*/ function isPidAlive(pid) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
return true;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// EPERM means the process exists but belongs to another user.
|
|
21
|
+
return error.code === 'EPERM';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export default isPidAlive;
|
|
@@ -0,0 +1,48 @@
|
|
|
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 fs from 'fs';
|
|
16
|
+
import getDevInstancePath from './getDevInstancePath.js';
|
|
17
|
+
import isPidAlive from './isPidAlive.js';
|
|
18
|
+
function readRecord(instancePath) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(instancePath, 'utf8'));
|
|
21
|
+
} catch {
|
|
22
|
+
// No record, or one mid-write - either way no live instance to report.
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// A record is live only when it was written for this exact directory and its
|
|
27
|
+
// process still runs. The directory check matters because .lowdefy/ gets
|
|
28
|
+
// copied between git worktrees - a copied record names the source checkout
|
|
29
|
+
// and must not make this checkout look occupied.
|
|
30
|
+
function readDevInstance({ configDirectory }) {
|
|
31
|
+
const record = readRecord(getDevInstancePath({
|
|
32
|
+
configDirectory
|
|
33
|
+
}));
|
|
34
|
+
if (record === null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let realConfigDirectory;
|
|
38
|
+
try {
|
|
39
|
+
realConfigDirectory = fs.realpathSync(configDirectory);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (record.configDirectory !== realConfigDirectory || !isPidAlive(record.pid)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
export default readDevInstance;
|
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-20260925210809",
|
|
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-20260925210809",
|
|
38
|
+
"@lowdefy/helpers": "0.0.0-experimental-20260925210809",
|
|
39
39
|
"fs-extra": "11.3.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|