@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 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, redaction, and the scope rules
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.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.1.0",
24
- "@onlineapps/runtime-config": "1.1.0",
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
- } = require('./redactSensitive');
42
- const { redactUrl } = require('./redactUrl');
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('../redactUrl');
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('../redactUrl');
5
+ const { redactUrl } = require('@onlineapps/logger-contract');
6
6
  const runtimeCfg = require('../config');
7
7
 
8
8
  /**
@@ -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('./redactUrl');
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 ./redactUrl.js
147
+ // @see @onlineapps/logger-contract
148
148
  const loggedUrl = redactUrl(url);
149
149
 
150
150
  client.on('error', (err) => {
@@ -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: { value: { env: name, required: true } },
90
+ schema: { [name]: { env: name, required: true } },
85
91
  });
86
92
  try {
87
- return cfg.get('value');
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: { value: { env: name, required: true, type } },
132
+ schema: { [name]: { env: name, required: true, type, ...(file ? { file } : {}) } },
122
133
  });
123
- return cfg.get('value');
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: { value: { env: name, default: defaultValue } },
205
+ schema: { [name]: { env: name, default: defaultValue } },
195
206
  });
196
- return cfg.get('value');
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: { value: { env: name, default: defaultValue, type: 'number' } },
219
+ schema: { [name]: { env: name, default: defaultValue, type: 'number' } },
209
220
  });
210
- return cfg.get('value');
221
+ return cfg.get(name);
211
222
  }
212
223
 
213
224
  /**
@@ -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
- };