@onlineapps/service-common 3.0.0 → 3.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/CHANGELOG.md +61 -0
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/index.js +8 -5
- package/src/infrastructure/waitForHealthCheckQueueReady.js +1 -1
- package/src/infrastructure/waitForInfrastructureReady.js +1 -1
- package/src/redisClient.js +2 -2
- package/src/runtime-config.js +19 -8
- package/src/redactSensitive.js +0 -87
- package/src/redactUrl.js +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,67 @@ All notable changes to this package. Follows [Keep a Changelog](https://keepacha
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.0.1] — 2026-09-15
|
|
8
|
+
|
|
9
|
+
### Fixed — vadná hodnota jmenuje klíč, ne interní `"value"` (d.466b)
|
|
10
|
+
|
|
11
|
+
Všechny čtyři env helpery (`requireEnv`, `requireTypedEnv` — tedy
|
|
12
|
+
`requireNumberEnv` i `requireFloatEnv` —, `optionalEnv`, `optionalNumberEnv`)
|
|
13
|
+
stavěly resolveru schéma pod interním jménem `value`. Hlášku o vadné hodnotě
|
|
14
|
+
vlastní `@onlineapps/runtime-config` a píše do ní klíč, který dostal, takže
|
|
15
|
+
operátor četl `Invalid number value for "value" (env: WS_MISSED_PONGS_LIMIT)` —
|
|
16
|
+
jméno, které v žádné konfiguraci není. INFRA to viděla při kaskádě 2026-09-15.
|
|
17
|
+
Klíč schématu je nově jméno proměnné prostředí: `Invalid number value for
|
|
18
|
+
"WS_MISSED_PONGS_LIMIT" (env: WS_MISSED_PONGS_LIMIT)`.
|
|
19
|
+
|
|
20
|
+
`requireNumberEnv`/`requireFloatEnv` navíc předávají do schématu `options.file`,
|
|
21
|
+
které se do té doby použilo jen na větu o CHYBĚJÍCÍ proměnné a pro vadnou
|
|
22
|
+
hodnotu zahodilo — obě věty přitom mluví o témž klíči v témž souboru. Místo
|
|
23
|
+
(`in config/env-active/<file>`) se v hlášce objeví s pinem
|
|
24
|
+
`@onlineapps/runtime-config` 1.2.0; pin 1.1.0 ji ještě nenese.
|
|
25
|
+
|
|
26
|
+
Hlášky o CHYBĚJÍCÍ proměnné a veřejné API balíčku se nemění.
|
|
27
|
+
|
|
28
|
+
### Fixed — redakce se bere z `@onlineapps/logger-contract`, lokální kopie jsou pryč (d.455)
|
|
29
|
+
|
|
30
|
+
Balíček držel dvě vlastní implementace pravidla „co smí dorazit do logu“:
|
|
31
|
+
`src/redactUrl.js` (userinfo z connection URL) a `src/redactSensitive.js`
|
|
32
|
+
(`sensitive_input_fields`, SecretBox F1.5). Obě jsou smazány; `src/index.js`
|
|
33
|
+
je teď **re-exportuje identitou** z `@onlineapps/logger-contract` 1.2.0.
|
|
34
|
+
Veřejné API balíčku se nemění — `redactUrl`, `redactSensitiveDeep`,
|
|
35
|
+
`sensitiveFieldsForOperation` a `REDACTED_PLACEHOLDER` se dál importují odsud,
|
|
36
|
+
protože odsud si je berou `@onlineapps/infrastructure-tools` (a přes něj
|
|
37
|
+
gateway) i monitoring.
|
|
38
|
+
|
|
39
|
+
Čtyři otázky ke smazané deklaraci (`change-discipline.md` § Removing):
|
|
40
|
+
|
|
41
|
+
1. **Proč vznikla** — obě kopie vznikly dřív, než core vrstva měla domov pro
|
|
42
|
+
toto pravidlo: `redactUrl` v d.245/d.245b (heslo z `REDIS_URL` ve třech
|
|
43
|
+
boot log lines), `redactSensitive` pro SecretBox F1.5. Importovat je
|
|
44
|
+
z vyšší vrstvy nešlo (`architecture-principles.md` §7), takže si každá
|
|
45
|
+
vrstva nesla svou.
|
|
46
|
+
2. **Která část koncepce je nesla** — `change-discipline.md` § One rail per
|
|
47
|
+
concern je naopak zakazuje: tři vlastníci jednoho pravidla (tady,
|
|
48
|
+
`mq-client-core`, `conn-orch-registry`) a nic, co selže, když se rozejdou.
|
|
49
|
+
Ta část koncepce stojí dál.
|
|
50
|
+
3. **Proč je dnes nikdo nečte** — `@onlineapps/logger-contract` 1.2.0 (d.453)
|
|
51
|
+
exportuje `redactUrl`, `UNPARSEABLE_PLACEHOLDER`, `redactSensitiveDeep`,
|
|
52
|
+
`sensitiveFieldsForOperation` a `REDACTED_PLACEHOLDER`; spustitelné řádky
|
|
53
|
+
jsou s oběma zdejšími kopiemi shodné (ověřeno `diff`, liší se jen JSDoc).
|
|
54
|
+
4. **Je náhrada koncepčnější** — ano. `logger-contract` je vrstva L1 bez
|
|
55
|
+
`@onlineapps` závislostí, takže na ni dosáhne každá kategorie balíčků;
|
|
56
|
+
brána G7 naopak odmítá pin `orchestration` → `runtime`, kterým si dřív
|
|
57
|
+
`conn-orch-orchestrator` sahal pro tyto helpery sem.
|
|
58
|
+
|
|
59
|
+
Testy `tests/unit/redactUrl.test.js` a `tests/unit/redactSensitive.test.js`
|
|
60
|
+
odešly s implementací (pokrytí drží `@onlineapps/logger-contract`). Místo nich
|
|
61
|
+
je `tests/unit/redactionReexport.test.js`: re-export je **identita**, lokální
|
|
62
|
+
soubory na disku nejsou, chování (happy path, cesta selhání) dorazí ke
|
|
63
|
+
konzumentovi beze změny, a kontrolní případ hlídá, že vlastní exporty balíčku
|
|
64
|
+
se tím nehnuly.
|
|
65
|
+
|
|
66
|
+
## [3.0.0] — 2026-09-14
|
|
67
|
+
|
|
7
68
|
### Fixed — registryReader se připojuje přes `connectRedis`; jeho timeoutová hláška renderovala heslo z `REDIS_URL` (d.441)
|
|
8
69
|
|
|
9
70
|
`registryReader.connect()` měl ve větvi, kde klienta vlastní, **druhou kolej**
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> Status: current
|
|
2
|
-
> Owns: the utilities infrastructure and business services share — JWT verification and tenant context, the Redis client, env and config readers,
|
|
2
|
+
> Owns: the utilities infrastructure and business services share — JWT verification and tenant context, the Redis client, env and config readers, and the scope rules; redakce se sem jen re-exportuje z `@onlineapps/logger-contract`, který ji vlastní
|
|
3
3
|
|
|
4
4
|
<!-- BEGIN GENERATED: library-uniform — regenerate: npx oa-sync-template readme-uniform --all -->
|
|
5
5
|
Uniform: [library/runtime](../connector/conn-orch-validator/manifests/library.manifest.json)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/service-common",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Common utilities for both infrastructure services and business services (JWT auth, Redis client, business errors, runtime config)",
|
|
5
5
|
"oa": {
|
|
6
6
|
"category": "runtime"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"author": "OA Drive Team",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onlineapps/logger-contract": "1.
|
|
24
|
-
"@onlineapps/runtime-config": "1.
|
|
23
|
+
"@onlineapps/logger-contract": "1.3.0",
|
|
24
|
+
"@onlineapps/runtime-config": "1.2.0",
|
|
25
25
|
"jsonwebtoken": "^9.0.3",
|
|
26
26
|
"nodemailer": "^10.0.9",
|
|
27
27
|
"redis": "^4.6.0"
|
package/src/index.js
CHANGED
|
@@ -37,9 +37,9 @@ const {
|
|
|
37
37
|
const {
|
|
38
38
|
REDACTED_PLACEHOLDER,
|
|
39
39
|
sensitiveFieldsForOperation,
|
|
40
|
-
redactSensitiveDeep
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
redactSensitiveDeep,
|
|
41
|
+
redactUrl
|
|
42
|
+
} = require('@onlineapps/logger-contract');
|
|
43
43
|
const {
|
|
44
44
|
isVisible,
|
|
45
45
|
filterVisible,
|
|
@@ -93,14 +93,17 @@ module.exports = {
|
|
|
93
93
|
extractTenantContext,
|
|
94
94
|
ROLES_VERSION_PREFIX,
|
|
95
95
|
|
|
96
|
-
// Sensitive-input redaction for observability sinks (SecretBox F1.5)
|
|
96
|
+
// Sensitive-input redaction for observability sinks (SecretBox F1.5).
|
|
97
|
+
// Re-export by identity from @onlineapps/logger-contract, which owns the rule
|
|
98
|
+
// (d.455); this package keeps the export because its consumers import it here.
|
|
97
99
|
// See: docs/architecture/secretbox.md §8
|
|
98
100
|
REDACTED_PLACEHOLDER,
|
|
99
101
|
sensitiveFieldsForOperation,
|
|
100
102
|
redactSensitiveDeep,
|
|
101
103
|
|
|
102
104
|
// Credential redaction for connection URLs written to a log
|
|
103
|
-
// (REDIS_URL carries the Redis password — one rail, INFRA lead 2026-09-10)
|
|
105
|
+
// (REDIS_URL carries the Redis password — one rail, INFRA lead 2026-09-10).
|
|
106
|
+
// Re-export by identity from @onlineapps/logger-contract (d.455).
|
|
104
107
|
redactUrl,
|
|
105
108
|
|
|
106
109
|
// Scoped-registry visibility rule (system ∪ own workspace)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { connectRedis, buildRedisUrl } = require('../redisClient');
|
|
4
4
|
const { createPrefixedLogger } = require('./prefixedLogger');
|
|
5
|
-
const { redactUrl } = require('
|
|
5
|
+
const { redactUrl } = require('@onlineapps/logger-contract');
|
|
6
6
|
const runtimeCfg = require('../config');
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { connectRedis, buildRedisUrl } = require('../redisClient');
|
|
4
4
|
const { createPrefixedLogger } = require('./prefixedLogger');
|
|
5
|
-
const { redactUrl } = require('
|
|
5
|
+
const { redactUrl } = require('@onlineapps/logger-contract');
|
|
6
6
|
const runtimeCfg = require('../config');
|
|
7
7
|
|
|
8
8
|
/**
|
package/src/redisClient.js
CHANGED
|
@@ -16,7 +16,7 @@ const { createClient } = require('redis');
|
|
|
16
16
|
const runtimeCfg = require('./config');
|
|
17
17
|
const { createRuntimeConfig } = require('@onlineapps/runtime-config');
|
|
18
18
|
const { assertLogger } = require('@onlineapps/logger-contract');
|
|
19
|
-
const { redactUrl } = require('
|
|
19
|
+
const { redactUrl } = require('@onlineapps/logger-contract');
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Share of the caller's `timeoutMs` that node-redis' OWN connect attempt gets;
|
|
@@ -144,7 +144,7 @@ function createRedisClient(options = {}) {
|
|
|
144
144
|
});
|
|
145
145
|
|
|
146
146
|
// The URL carries the credential; the log carries the endpoint only.
|
|
147
|
-
// @see
|
|
147
|
+
// @see @onlineapps/logger-contract
|
|
148
148
|
const loggedUrl = redactUrl(url);
|
|
149
149
|
|
|
150
150
|
client.on('error', (err) => {
|
package/src/runtime-config.js
CHANGED
|
@@ -79,12 +79,18 @@ function owningFileOf(options) {
|
|
|
79
79
|
*/
|
|
80
80
|
function requireEnv(name, description, options) {
|
|
81
81
|
const file = owningFileOf(options);
|
|
82
|
+
// Klíč schématu je jméno proměnné, ne interní `value`. Resolver jiné jméno
|
|
83
|
+
// klíče nezná, a co mu helper předá, to napíše do hlášky: do d.466b psal
|
|
84
|
+
// `Invalid number value for "value"`, takže z hlášky nešlo přečíst, který klíč
|
|
85
|
+
// je špatně (INFRA, kaskáda 2026-09-15). Zdejší kolej žádné jiné jméno nemá —
|
|
86
|
+
// helper dostává proměnnou, ne konfigurační klíč — a vymyslet mu jméno by
|
|
87
|
+
// znamenalo psát do hlášky fakt, který nikdo nedeklaroval.
|
|
82
88
|
const cfg = createRuntimeConfig({
|
|
83
89
|
defaults: {},
|
|
84
|
-
schema: {
|
|
90
|
+
schema: { [name]: { env: name, required: true } },
|
|
85
91
|
});
|
|
86
92
|
try {
|
|
87
|
-
return cfg.get(
|
|
93
|
+
return cfg.get(name);
|
|
88
94
|
} catch (err) {
|
|
89
95
|
// Jeden šablonový literál, ne skládání polem: statická sonda
|
|
90
96
|
// `tests/unit/error-message-contract.test.js` čte zprávu ze zdroje, a
|
|
@@ -116,11 +122,16 @@ function requireEnv(name, description, options) {
|
|
|
116
122
|
*/
|
|
117
123
|
function requireTypedEnv(name, description, type, options) {
|
|
118
124
|
requireEnv(name, description, options);
|
|
125
|
+
// `file` dojde až do schématu: hlášku o vadné hodnotě skládá resolver a bez
|
|
126
|
+
// deklarace nemá odkud vzít místo, kam má operátor sáhnout. Do d.466b se
|
|
127
|
+
// `options.file` použil jen na větu o CHYBĚJÍCÍ proměnné a pro vadnou hodnotu
|
|
128
|
+
// se zahodil, ačkoli obě věty mluví o témž klíči v témž souboru.
|
|
129
|
+
const file = owningFileOf(options);
|
|
119
130
|
const cfg = createRuntimeConfig({
|
|
120
131
|
defaults: {},
|
|
121
|
-
schema: {
|
|
132
|
+
schema: { [name]: { env: name, required: true, type, ...(file ? { file } : {}) } },
|
|
122
133
|
});
|
|
123
|
-
return cfg.get(
|
|
134
|
+
return cfg.get(name);
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
/**
|
|
@@ -191,9 +202,9 @@ function requireBoolEnv(name, description, options) {
|
|
|
191
202
|
function optionalEnv(name, defaultValue) {
|
|
192
203
|
const cfg = createRuntimeConfig({
|
|
193
204
|
defaults: {},
|
|
194
|
-
schema: {
|
|
205
|
+
schema: { [name]: { env: name, default: defaultValue } },
|
|
195
206
|
});
|
|
196
|
-
return cfg.get(
|
|
207
|
+
return cfg.get(name);
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
/**
|
|
@@ -205,9 +216,9 @@ function optionalEnv(name, defaultValue) {
|
|
|
205
216
|
function optionalNumberEnv(name, defaultValue) {
|
|
206
217
|
const cfg = createRuntimeConfig({
|
|
207
218
|
defaults: {},
|
|
208
|
-
schema: {
|
|
219
|
+
schema: { [name]: { env: name, default: defaultValue, type: 'number' } },
|
|
209
220
|
});
|
|
210
|
-
return cfg.get(
|
|
221
|
+
return cfg.get(name);
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
/**
|
package/src/redactSensitive.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sensitive-input redaction (SecretBox F1.5).
|
|
5
|
-
* Contract: api/docs/architecture/secretbox.md §8.
|
|
6
|
-
*
|
|
7
|
-
* Pure, dependency-free helpers used by every layer that logs, traces, or
|
|
8
|
-
* otherwise persists workflow/operation inputs (gateway, orchestrator, monitoring
|
|
9
|
-
* consumer). Single implementation — no duplication across layers.
|
|
10
|
-
*
|
|
11
|
-
* Operations declare sensitive input fields at operation level in operations.json:
|
|
12
|
-
* "sensitive_input_fields": ["value", ...]
|
|
13
|
-
* (NOT inside the JSON Schema — service-wrapper compiles input schemas with AJV
|
|
14
|
-
* strict:true, which throws on unknown keywords.)
|
|
15
|
-
*
|
|
16
|
-
* `redactSensitiveDeep` deep-clones the value and replaces any object key whose
|
|
17
|
-
* name is in `fieldNames` with the REDACTED placeholder, at any depth. It never
|
|
18
|
-
* mutates the input. Over-redaction (a sensitive field name matching elsewhere in
|
|
19
|
-
* the same payload) is acceptable and safe for observability sinks.
|
|
20
|
-
*
|
|
21
|
-
* IMPORTANT: this is for observability/log/trace copies only. The execution message
|
|
22
|
-
* that carries the value to its owning handler MUST NOT be redacted.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
const REDACTED_PLACEHOLDER = '[REDACTED]';
|
|
26
|
-
const CIRCULAR_PLACEHOLDER = '[Circular]';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Extract the sensitive input field names for one operation from a service spec
|
|
30
|
-
* (the `registry:service:<name>:spec` shape, or a local operations.json object).
|
|
31
|
-
* @param {object} serviceSpec - object with `.operations[<op>]`.
|
|
32
|
-
* @param {string} operationName
|
|
33
|
-
* @returns {string[]} field names (empty array when none / not found).
|
|
34
|
-
*/
|
|
35
|
-
function sensitiveFieldsForOperation(serviceSpec, operationName) {
|
|
36
|
-
const op =
|
|
37
|
-
serviceSpec &&
|
|
38
|
-
serviceSpec.operations &&
|
|
39
|
-
typeof serviceSpec.operations === 'object'
|
|
40
|
-
? serviceSpec.operations[operationName]
|
|
41
|
-
: undefined;
|
|
42
|
-
const list = op && op.sensitive_input_fields;
|
|
43
|
-
return Array.isArray(list) ? list.filter((f) => typeof f === 'string' && f.length > 0) : [];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function _walk(node, fields, seen) {
|
|
47
|
-
if (node === null || typeof node !== 'object') return node;
|
|
48
|
-
// Do not recurse into non-plain objects; return a shallow copy where safe.
|
|
49
|
-
if (node instanceof Date) return new Date(node.getTime());
|
|
50
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(node)) return node;
|
|
51
|
-
if (seen.has(node)) return CIRCULAR_PLACEHOLDER;
|
|
52
|
-
seen.add(node);
|
|
53
|
-
|
|
54
|
-
if (Array.isArray(node)) {
|
|
55
|
-
return node.map((el) => _walk(el, fields, seen));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const out = {};
|
|
59
|
-
for (const key of Object.keys(node)) {
|
|
60
|
-
if (fields.has(key)) {
|
|
61
|
-
out[key] = REDACTED_PLACEHOLDER;
|
|
62
|
-
} else {
|
|
63
|
-
out[key] = _walk(node[key], fields, seen);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Deep-clone `value`, replacing any object property named in `fieldNames` with
|
|
71
|
-
* the REDACTED placeholder. Does not mutate the input.
|
|
72
|
-
* @param {*} value - any JSON-ish value (object/array/primitive).
|
|
73
|
-
* @param {string[]|Set<string>} fieldNames - sensitive field names to redact.
|
|
74
|
-
* @returns {*} redacted deep clone (or the original value when there is nothing
|
|
75
|
-
* to redact — no sensitive fields means no mutation risk).
|
|
76
|
-
*/
|
|
77
|
-
function redactSensitiveDeep(value, fieldNames) {
|
|
78
|
-
const fields = fieldNames instanceof Set ? fieldNames : new Set(fieldNames || []);
|
|
79
|
-
if (fields.size === 0) return value;
|
|
80
|
-
return _walk(value, fields, new WeakSet());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
module.exports = {
|
|
84
|
-
REDACTED_PLACEHOLDER,
|
|
85
|
-
sensitiveFieldsForOperation,
|
|
86
|
-
redactSensitiveDeep
|
|
87
|
-
};
|
package/src/redactUrl.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Redaction of credentials carried in a connection URL.
|
|
5
|
-
*
|
|
6
|
-
* `REDIS_URL` is the single rail for the Redis credential (INFRA lead's decision
|
|
7
|
-
* 2026-09-10: one rail, no separate `REDIS_PASSWORD` in the shared env set).
|
|
8
|
-
* That makes every place which logs the URL verbatim a credential leak — and
|
|
9
|
-
* these logs go to Loki, so the leak is durable and searchable.
|
|
10
|
-
*
|
|
11
|
-
* `redactUrl` returns the same URL with the userinfo removed: what stays is the
|
|
12
|
-
* part a reader of the log actually needs — scheme, host, port, path.
|
|
13
|
-
*
|
|
14
|
-
* A value that is not a parseable URL is returned as a fixed placeholder rather
|
|
15
|
-
* than echoed: an unparseable string is exactly the case where nobody can say
|
|
16
|
-
* whether it holds a credential, and echoing it would be the leak this helper
|
|
17
|
-
* exists to prevent.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const UNPARSEABLE_PLACEHOLDER = '<unparseable-url>';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Strip userinfo (`user:password@`) from a connection URL.
|
|
24
|
-
*
|
|
25
|
-
* @param {string} url - connection URL, e.g. `redis://:secret@cache:6379`
|
|
26
|
-
* @returns {string} the URL without userinfo, e.g. `redis://cache:6379`,
|
|
27
|
-
* or `<unparseable-url>` when the input is not a URL.
|
|
28
|
-
*/
|
|
29
|
-
function redactUrl(url) {
|
|
30
|
-
if (typeof url !== 'string' || url.length === 0) {
|
|
31
|
-
return UNPARSEABLE_PLACEHOLDER;
|
|
32
|
-
}
|
|
33
|
-
let parsed;
|
|
34
|
-
try {
|
|
35
|
-
parsed = new URL(url);
|
|
36
|
-
} catch (_) {
|
|
37
|
-
return UNPARSEABLE_PLACEHOLDER;
|
|
38
|
-
}
|
|
39
|
-
// `new URL('cache-host:6379')` SUCCEEDS — it reads `cache-host:` as the scheme
|
|
40
|
-
// and `6379` as an opaque path, so the value comes back with an empty host and
|
|
41
|
-
// no userinfo to strip. Measured 2026-09-11; without this guard such a value
|
|
42
|
-
// would be echoed verbatim, which is the leak this helper exists to prevent.
|
|
43
|
-
if (!parsed.host) {
|
|
44
|
-
return UNPARSEABLE_PLACEHOLDER;
|
|
45
|
-
}
|
|
46
|
-
parsed.username = '';
|
|
47
|
-
parsed.password = '';
|
|
48
|
-
// `URL.href` re-appends a trailing slash for an empty path; the URLs this is
|
|
49
|
-
// used on are host:port endpoints, and `redis://cache:6379/` is not the shape
|
|
50
|
-
// the rest of the platform writes.
|
|
51
|
-
const href = parsed.href;
|
|
52
|
-
return parsed.pathname === '/' && !url.endsWith('/') ? href.replace(/\/$/, '') : href;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
module.exports = {
|
|
56
|
-
redactUrl,
|
|
57
|
-
UNPARSEABLE_PLACEHOLDER
|
|
58
|
-
};
|