@onlineapps/service-common 2.0.1 → 3.0.0
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 +540 -0
- package/README.md +282 -22
- package/package.json +10 -7
- package/src/config.js +13 -1
- package/src/defaults.js +28 -1
- package/src/index.js +12 -27
- package/src/infrastructure/prefixedLogger.js +49 -0
- package/src/infrastructure/waitForHealthCheckQueueReady.js +47 -44
- package/src/infrastructure/waitForInfrastructureReady.js +88 -60
- package/src/jwt/createJwtValidator.js +159 -29
- package/src/jwt/verifyAccessToken.js +30 -11
- package/src/redactUrl.js +58 -0
- package/src/redisClient.js +175 -37
- package/src/registryReader.js +26 -42
- package/src/reporting/monitoringFallbackEmail.js +163 -28
- package/src/runtime-config.js +137 -29
- package/src/errors/BusinessError.js +0 -118
- package/src/errors/errorMiddleware.js +0 -112
- package/src/errors/index.js +0 -29
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Changelog — @onlineapps/service-common
|
|
2
|
+
|
|
3
|
+
All notable changes to this package. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Fixed — registryReader se připojuje přes `connectRedis`; jeho timeoutová hláška renderovala heslo z `REDIS_URL` (d.441)
|
|
8
|
+
|
|
9
|
+
`registryReader.connect()` měl ve větvi, kde klienta vlastní, **druhou kolej**
|
|
10
|
+
téhož mechanismu jako `connectRedis` v `src/redisClient.js`: vlastní
|
|
11
|
+
`createClient({ url, socket: { connectTimeout } })`, vlastní `Promise.race`
|
|
12
|
+
a vlastní `setTimeout` (`change-discipline.md` § One rail per concern). Rozdíl
|
|
13
|
+
mezi kolejemi nebyl kosmetický:
|
|
14
|
+
|
|
15
|
+
- Hláška při nedostupném Redisu zněla `no connection to ${redisUrl}` — tedy
|
|
16
|
+
**syrová URL včetně credentialu**. Změřeno na HEAD:
|
|
17
|
+
`… no connection to redis://registryreader:integr4tion-s3cr3t@127.0.0.1:1 within 1000ms`.
|
|
18
|
+
Je to táž třída vady, kterou pro `redisClient` zavřela d.245/d.245b
|
|
19
|
+
(`redactUrl`, `tests/unit/redisUrlNotLogged.test.js`), a tyto logy jdou do
|
|
20
|
+
Loki, takže únik je trvalý a vyhledatelný.
|
|
21
|
+
- Lhůta se předávala node-redisu i závodu ve stejné hodnotě, takže pokus
|
|
22
|
+
knihovny končil až PO tvrdém stropu a socket v letu přežíval vlastní
|
|
23
|
+
`disconnect()` — přesně to, co d.437b pro `connectRedis` opravilo
|
|
24
|
+
(`CONNECT_ATTEMPT_SHARE`).
|
|
25
|
+
- `client.on('error')` logoval `${CONTEXT} Redis client error: ${err.message}`
|
|
26
|
+
mimo jednotný tvar `[Redis] Error` s redigovanou URL.
|
|
27
|
+
|
|
28
|
+
Nově reader ve větvi `ownsClient` jen řekne rozpočet a zbytek nechá jediné
|
|
29
|
+
koleji:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
redis = await connectRedis({ purpose: 'registryReader', redisUrl, logger, timeoutMs: connectTimeoutMs });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Navenek se nemění `connectTimeoutMs` (dál strop pro `connect()`, výchozí 15 s),
|
|
36
|
+
volba `redisUrl` vs. `client` ani chování s injektovaným klientem (ten reader
|
|
37
|
+
neotvírá a `close()` ho nikdy nezavírá). Mění se **znění hlášky** — jednotný
|
|
38
|
+
kontraktní tvar `[service-common][redisClient] Redis connection <verdikt> to
|
|
39
|
+
<endpoint> - <důvod>` s původní chybou jako `cause` a s endpointem bez
|
|
40
|
+
userinfo.
|
|
41
|
+
|
|
42
|
+
**Zpřísnění kontraktu loggeru (breaking pro volajícího s neúplným loggerem):**
|
|
43
|
+
reader si dosud ověřoval vlastní třímetodovou variantu (`info`/`warn`/`error`)
|
|
44
|
+
— poslední lokální kopii kontraktu v tomto balíčku. Klient, kterého reader
|
|
45
|
+
vlastní, píše do téhož objektu, takže kontrakt teď vlastní
|
|
46
|
+
`@onlineapps/logger-contract` (`assertLogger`, čtyři metody včetně `debug`,
|
|
47
|
+
konfirmace `connector-logger-contract` 001/004) a ověří se při KONSTRUKCI, ne
|
|
48
|
+
až při `connect()`. Jediný volající mimo balíček — `api_biz/meta/index.js:72` —
|
|
49
|
+
předává `wrapper.logger`, který kontrakt splňuje.
|
|
50
|
+
|
|
51
|
+
Testy: `tests/unit/registryReader.connectRail.test.js` (cesta selhání bez
|
|
52
|
+
credentialu, rozpočet předaný `connectRedis`, výchozí lhůta, KONTROLA úspěšného
|
|
53
|
+
připojení, KONTROLA injektovaného klienta) a nový případ v
|
|
54
|
+
`tests/integration/registryReader.integration.test.js` proti živému Redisu
|
|
55
|
+
(odmítnutý port s credentialem v URL → hláška i log bez hesla). Počet hlášek
|
|
56
|
+
v `tests/unit/error-message-contract.test.js` klesl 42 → 40: zanikla timeoutová
|
|
57
|
+
hláška readeru i jeho `requireLogger`.
|
|
58
|
+
|
|
59
|
+
### Changed (breaking) — kontrolu zastaralých rolí dělá injektovaná funkce, ne redis klient (d.431)
|
|
60
|
+
|
|
61
|
+
`createJwtValidator({ logger, secret, excludePaths, redisClient })` testoval
|
|
62
|
+
`redisClient.isOpen` a volal `redisClient.get()` — tedy API node-redis v4. Služba,
|
|
63
|
+
jejíž klient je `ioredis` (`infra/api_meta_reader`), kontrolu nemohla dostat vůbec;
|
|
64
|
+
tvar klienta přitom není znalost knihovny (`architecture-principles.md` §1, §8).
|
|
65
|
+
|
|
66
|
+
Nový vstup je funkce a je **povinná**:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
createJwtValidator({ logger, secret, readRolesVersion, excludePaths })
|
|
70
|
+
// readRolesVersion: (personId) => Promise<number|null>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `null` = marker neexistuje → token platný (beze změny, konfirmace
|
|
74
|
+
`jwt-stale-check-fail-closed` 001).
|
|
75
|
+
- odmítnutí čtenáře **nebo** odpověď, která není konečné číslo ani `null` → 503
|
|
76
|
+
`ROLES_VERSION_CHECK_UNAVAILABLE`, nikdy průchod (táž konfirmace).
|
|
77
|
+
- chybějící `readRolesVersion` → výjimka při KONSTRUKCI: kontrola běží pro každý
|
|
78
|
+
platný token, takže instance, která ji neumí provést, nesmí vzniknout (§4).
|
|
79
|
+
- parametr `redisClient` zaniká celý, bez přechodového tvaru (§11).
|
|
80
|
+
|
|
81
|
+
Klíč čte volající, ale skládá ho z exportovaného `ROLES_VERSION_PREFIX` =
|
|
82
|
+
`state:meta:person:roles_version:` — marker píše biz-meta přes konektor, který
|
|
83
|
+
prefix `state:meta:` přidává, takže čtenář holého `person:roles_version:<id>`
|
|
84
|
+
dostane `null` pro každou osobu a `TOKEN_STALE` nepadne nikdy (rozhodnutí
|
|
85
|
+
vlastníka 2026-09-14, `redis-state-prefix` 002). README § JWT validation nese
|
|
86
|
+
hotový tvar čtenáře.
|
|
87
|
+
|
|
88
|
+
Volající mimo tento balíček pinují vydané 2.0.x a tímto commitem se nemění;
|
|
89
|
+
překlápějí se s pinem příštího vydání: `infra/api_gateway/index.js:621`,
|
|
90
|
+
`infra/api_gateway/routes/protected.js:45`, `infra/api_meta_reader/src/index.js:81`
|
|
91
|
+
(a testy `infra/api_gateway/tests/integration/webhookWorkflowProjection.test.js:219`,
|
|
92
|
+
`infra/api_gateway/tests/unit/authPipelineBootWindow.test.js:88`).
|
|
93
|
+
|
|
94
|
+
### Fixed — token vydaný ve stejné sekundě jako marker už není TOKEN_STALE (d.430)
|
|
95
|
+
|
|
96
|
+
`iat` je celý počet SEKUND zaokrouhlený dolů (RFC 7519 §4.1.6), marker je epoch
|
|
97
|
+
MILISEKUND (`docs/standards/redis-key-contract.md`). Porovnání `decoded.iat * 1000 <
|
|
98
|
+
versionTs` proto prohlásilo za zastaralý i token vydaný pár milisekund PO změně
|
|
99
|
+
role, kterou už nese — `addMembership` zapíše `String(Date.now())` a `api_auth`
|
|
100
|
+
podepíše nový token v téže sekundě. Držitel dostal pokyn obnovit token a obnovený
|
|
101
|
+
token spadl do téže sekundy znovu.
|
|
102
|
+
|
|
103
|
+
Porovnává se v hrubší z obou jednotek: marker se zaokrouhlí dolů na sekundy
|
|
104
|
+
(`decoded.iat < Math.floor(rolesVersion / 1000)`). Token ze STEJNÉ sekundy je
|
|
105
|
+
platný, token o sekundu starší je dál `TOKEN_STALE` — obojí pokryto v unit i
|
|
106
|
+
integračním tieru proti živému Redisu.
|
|
107
|
+
|
|
108
|
+
### Fixed — connectRedis uvolní socket na vlastní lhůtě a každé selhání hlásí jednou hláškou (d.437b)
|
|
109
|
+
|
|
110
|
+
`disconnect()` z d.437 uvolní jen socket, který node-redis už přiřadil
|
|
111
|
+
(`RedisSocket#disconnect`); spojení, které je pořád v letu, ho přežije a drží
|
|
112
|
+
otevřený TCPWRAP handle po celou výchozí lhůtu node-redisu (5 s). Změřeno
|
|
113
|
+
2026-09-14 na unit tieru balíčku: `npx jest --detectOpenHandles` hlásil
|
|
114
|
+
`TCPWRAP … at connect (src/redisClient.js:138)` z
|
|
115
|
+
`tests/unit/redisUrlNotLogged.test.js` a běh končil hláškou `A worker process
|
|
116
|
+
has failed to exit gracefully`.
|
|
117
|
+
|
|
118
|
+
Pokus node-redisu proto dostává lhůtu odvozenou z rozpočtu volajícího
|
|
119
|
+
(`CONNECT_ATTEMPT_SHARE` = 0,8 × `timeoutMs`, tedy 12 s z výchozích 15 s) a
|
|
120
|
+
dostane ji jako `socket.connectTimeout` — socket tak ničí knihovna, která ho
|
|
121
|
+
otevřela, ještě uvnitř stropu (změřeno: 803 ms z 1000ms stropu). Vlastní závod
|
|
122
|
+
na `timeoutMs` zůstává tvrdým celkovým stropem: mimo `forTests` může
|
|
123
|
+
reconnect-strategie zkusit víc pokusů, takže se neruší.
|
|
124
|
+
|
|
125
|
+
Každé selhání spojení teď opouští `connectRedis` jedinou hláškou ve tvaru
|
|
126
|
+
`[Context] Problem - Fix` s původní chybou jako `cause` — dosud prošla
|
|
127
|
+
node-redisí holá `Connection timeout` i `ECONNREFUSED` beze všeho kontextu:
|
|
128
|
+
|
|
129
|
+
- lhůta (naše i ta předaná node-redisu) → `… Redis connection timeout after <timeoutMs>ms to <endpoint> - <důvod>. Fix: …`
|
|
130
|
+
- jiné selhání → `… Redis connection failed to <endpoint> - <důvod>. Fix: …`; odmítnuté spojení není timeout a hláška to o něm netvrdí.
|
|
131
|
+
|
|
132
|
+
Endpoint v hlášce je dál redigovaný (`loggedUrl`), takže credential se ven
|
|
133
|
+
nedostane. Věta `Fix:` nově radí zvýšit `timeoutMs` — dosavadní
|
|
134
|
+
`connectTimeoutMs` je vstup `registryReader`, `connectRedis` takový nikdy
|
|
135
|
+
neměl. `createRedisClient` přijímá nepovinný `connectAttemptTimeoutMs` (lhůta
|
|
136
|
+
JEDNOHO pokusu, ne celkový strop).
|
|
137
|
+
|
|
138
|
+
### Fixed — connectRedis po neúspěšném spojení uvolní klienta, kterého otevřel (d.437)
|
|
139
|
+
|
|
140
|
+
Prohraje-li `client.connect()` závod s timeoutem, běží dál — s výchozí
|
|
141
|
+
reconnect-strategií donekonečna — a volající klienta na této cestě nikdy
|
|
142
|
+
nedostane, takže ho nemá kdo zavřít. `connectRedis` ho teď zavírá sám
|
|
143
|
+
(`client.disconnect()`, jen když je `isOpen`; selhání zavření se loguje a
|
|
144
|
+
původní chybu nepřekryje).
|
|
145
|
+
|
|
146
|
+
### Fixed — asserty hlášky chybějící env na znění runtime-config 1.1.0 (d.426)
|
|
147
|
+
|
|
148
|
+
Po pinu `@onlineapps/runtime-config` na 1.1.0 citovaly dva asserty
|
|
149
|
+
v `tests/unit/defaults.test.js` starou větu `Fix: set REDIS_URL in environment or
|
|
150
|
+
pass explicit config.`; nově citují celou hlášku ve znění, které předepisuje
|
|
151
|
+
`architecture-principles.md` §5 — `Fix: set REDIS_URL in env-active/*.env (or pass
|
|
152
|
+
explicit config).`.
|
|
153
|
+
|
|
154
|
+
### Changed (breaking) — the JWT signing secret is an input, not something the library fetches
|
|
155
|
+
|
|
156
|
+
`verifyAccessToken(token)` resolved the secret itself, per call, from
|
|
157
|
+
`process.env.JWT_SECRET` (`src/jwt/verifyAccessToken.js`, the deleted
|
|
158
|
+
`getJwtSecret()`). A shared library that reads the environment knows where the
|
|
159
|
+
platform keeps its configuration — the dependency nobody declared
|
|
160
|
+
(`architecture-principles.md` §1 dependencies by constructor, §8 explicit over
|
|
161
|
+
implicit; measured by INFRA-DOCS 2026-09-07, recorded in
|
|
162
|
+
`docs/standards/CONCEPT_AUDIT.md` as grey).
|
|
163
|
+
|
|
164
|
+
The secret is now an input on both entry points:
|
|
165
|
+
|
|
166
|
+
- `verifyAccessToken(token, secret)` — second argument, required.
|
|
167
|
+
- `createJwtValidator({ logger, secret, excludePaths, redisClient })` — validated
|
|
168
|
+
at CONSTRUCTION, so an instance can never exist that every request would then
|
|
169
|
+
be refused by (§4 fail-fast).
|
|
170
|
+
|
|
171
|
+
A **value**, not a `getSecret()` function: `docs/standards/JWT_AUTH.md`
|
|
172
|
+
§ Configuration distributes one shared secret via env/Docker secrets and knows no
|
|
173
|
+
secret rotation: `grep -in rotat docs/standards/JWT_AUTH.md` returns exactly one
|
|
174
|
+
line, the certificate rotation listed as a COST of the mTLS option §9 rejects. An
|
|
175
|
+
injected `getSecret()` resolver would be a mechanism for a concept that does not
|
|
176
|
+
exist; when rotation is decided, the parameter it replaces is one line.
|
|
177
|
+
|
|
178
|
+
The caller reads the key once at boot, through the helper that composes the whole
|
|
179
|
+
`Fix:` sentence (d.399):
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
const secret = requireEnv('JWT_SECRET', 'HMAC-SHA256 signing secret', { file: 'shared.env' });
|
|
183
|
+
app.use(createJwtValidator({ logger, secret, excludePaths, redisClient }));
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**The rule about the value stays in the library**, because it is a property of
|
|
187
|
+
the secret and not of where it came from: `assertSecret()` — a string of at least
|
|
188
|
+
`MIN_SECRET_LENGTH` (16) characters, the minimum `JWT_AUTH.md` § Configuration
|
|
189
|
+
states — is the single rail both entry points validate against.
|
|
190
|
+
|
|
191
|
+
Callers outside this package still pin the published 2.0.x and are untouched by
|
|
192
|
+
this commit; they move with the pin of the next release:
|
|
193
|
+
`infra/api_gateway/index.js:599`, `infra/api_gateway/routes/protected.js:19`,
|
|
194
|
+
`infra/api_meta_reader/src/index.js:81` (all `createJwtValidator`),
|
|
195
|
+
`infra/api_delivery_endpoint/src/ws/WebSocketServer.js:124` and
|
|
196
|
+
`infra/api_delivery_endpoint/src/routes/workflows.js:31` (both
|
|
197
|
+
`verifyAccessToken`).
|
|
198
|
+
|
|
199
|
+
#### `getJwtSecret()` deleted — the four questions (`change-discipline.md` § Removing)
|
|
200
|
+
|
|
201
|
+
1. **Why it existed.** It was the verifier's only way to a secret: with no
|
|
202
|
+
injection point, the library resolved `JWT_SECRET` itself so that every
|
|
203
|
+
service verifying a token got the same key and the same fail-fast on a
|
|
204
|
+
missing or too-short value.
|
|
205
|
+
2. **Which part of the concept carried it.** `docs/standards/JWT_AUTH.md`
|
|
206
|
+
§ Configuration — one shared `JWT_SECRET`, minimum 16 characters, identical
|
|
207
|
+
across every service that validates a JWT — and `gateway-jwt-failfast` 001, a
|
|
208
|
+
boot that dies on an absent key rather than serving unauthenticated traffic.
|
|
209
|
+
Both still stand: the key, the minimum and the fail-fast are unchanged. Only
|
|
210
|
+
the PLACE that reads the environment moves, from the library to the service
|
|
211
|
+
that owns its configuration.
|
|
212
|
+
3. **Why nothing reads it.** Nothing outside this file ever did: it was absent
|
|
213
|
+
from `src/jwt/index.js` and from `src/index.js`, so no consumer could import
|
|
214
|
+
it (`grep -rn getJwtSecret` over `api`, `api_biz`, `fe_adminui` finds only
|
|
215
|
+
`infra/api_auth/src/tokenService.js:9`, a private function of the same name in
|
|
216
|
+
the token ISSUER, which is a different service and is not touched here). Its
|
|
217
|
+
one reader was `verifyAccessToken`, three lines below it, and that reader now
|
|
218
|
+
receives the value.
|
|
219
|
+
4. **Is the replacement more conceptual?** Yes. The secret as a parameter is the
|
|
220
|
+
principle the function violated, and the env read lands where the concept
|
|
221
|
+
already puts configuration: at the boot of the owning service, through
|
|
222
|
+
`requireEnv('JWT_SECRET', …, { file: 'shared.env' })`, which names the key,
|
|
223
|
+
the owning env file and the fix in one sentence. A pure `getJwtSecret()`
|
|
224
|
+
kept "without env" would have been a rename of the hole: whoever called it
|
|
225
|
+
would still be asking the library where secrets live.
|
|
226
|
+
|
|
227
|
+
Tests: `tests/unit/jwt/verifyAccessToken.test.js` and
|
|
228
|
+
`tests/unit/jwt/createJwtValidator.test.js` now run with a DIFFERENT valid secret
|
|
229
|
+
in `process.env.JWT_SECRET` throughout, so a re-introduced environment read fails
|
|
230
|
+
the happy path of both files, not only the one case that names it; the same
|
|
231
|
+
control runs against the live Redis in
|
|
232
|
+
`tests/integration/jwtValidator.rolesVersion.integration.test.js`.
|
|
233
|
+
|
|
234
|
+
### Unchanged — the four infrastructure-health wait defaults keep their readers, and the reason is now asserted
|
|
235
|
+
|
|
236
|
+
`infrastructureHealthWaitMaxTimeMs`, `infrastructureHealthWaitCheckIntervalMs`,
|
|
237
|
+
`infrastructureHealthQueueWaitMaxTimeMs` and
|
|
238
|
+
`infrastructureHealthQueueWaitCheckIntervalMs` were put up for deletion as
|
|
239
|
+
declarations with no production reader
|
|
240
|
+
(`infra/api_services_registry/tests/unit/infrastructureHealthConfigShape.test.js:14-18`).
|
|
241
|
+
The four questions `change-discipline.md` § Removing requires say keep, because
|
|
242
|
+
question 3 fails on the evidence:
|
|
243
|
+
|
|
244
|
+
1. **Why they exist.** So `waitForInfrastructureReady()` and
|
|
245
|
+
`waitForHealthCheckQueueReady()` have a platform limit and probe interval when
|
|
246
|
+
the caller names none — a service that boots before the infrastructure has to
|
|
247
|
+
wait, not die.
|
|
248
|
+
2. **Which part of the concept carries them.** The boot wait on
|
|
249
|
+
`infrastructure:health:all`, documented in
|
|
250
|
+
`docs/operations/infrastructure-troubleshooting.md` § — *"up to the limit
|
|
251
|
+
`INFRASTRUCTURE_HEALTH_WAIT_MAX_TIME`"* — and run at gateway boot. It stands.
|
|
252
|
+
3. **Why nothing reads them.** It does. Each of the four is read in this package,
|
|
253
|
+
in the function it exists for: `waitForInfrastructureReady.js:66-67` and
|
|
254
|
+
`waitForHealthCheckQueueReady.js:58-59`, via `runtimeCfg.get(key, override)`.
|
|
255
|
+
The INFRA measurement is true but narrower than its wording: it says the
|
|
256
|
+
REGISTRY does not take these four from `getInfrastructureHealthConfig()`.
|
|
257
|
+
4. **Is the replacement more conceptual?** There is nothing to replace them with.
|
|
258
|
+
Deleting them would leave both wait functions with no limit of their own.
|
|
259
|
+
|
|
260
|
+
So nothing was deleted, and `tests/unit/healthTimingOwnership.test.js` — which
|
|
261
|
+
already owns "who owns which health timing" — now states it by value: each key's
|
|
262
|
+
default, the file that reads it, and the environment variable that overrides it,
|
|
263
|
+
with a control case proving the probe would notice a reader that disappeared.
|
|
264
|
+
Measured against itself: removing one default and one reader turns three of those
|
|
265
|
+
cases red.
|
|
266
|
+
|
|
267
|
+
**One rail is still two, and the other half is not ours.**
|
|
268
|
+
`getInfrastructureHealthConfig()` returns the same four values a second time, and
|
|
269
|
+
`infra/api_gateway/index.js:526-534` plus
|
|
270
|
+
`infra/api_delivery_dispatcher/src/services/DeliveryDispatcher.js:167-168, 227`
|
|
271
|
+
read them from there only to hand them straight back to
|
|
272
|
+
`waitForInfrastructureReady()` as `maxWait`/`checkInterval` — the same env key
|
|
273
|
+
resolved twice, the second resolution posing as an override of itself. The fields
|
|
274
|
+
stay because the gateway fail-fasts on a missing one, so removing them here would
|
|
275
|
+
stop a boot before those two callers stop reading: a gate lands with compliance,
|
|
276
|
+
never ahead of it (`automation-gates.md` §3), and the compliance is in a tree this
|
|
277
|
+
package must not edit. The shape is pinned so nobody removes it early — or widens
|
|
278
|
+
it.
|
|
279
|
+
|
|
280
|
+
### Changed — the `Fix:` sentence is composed by the helper, not by the caller
|
|
281
|
+
|
|
282
|
+
`architecture-principles.md` §5 requires a missing key to be reported with
|
|
283
|
+
`Fix: set <ENV_KEY> in env-active/*.env (or pass explicit config).` The helper
|
|
284
|
+
did not write it: `requireEnv()` appended `description` and stopped, so the
|
|
285
|
+
sentence appeared exactly where somebody had typed it into that description by
|
|
286
|
+
hand. Measured across the platform: six call sites, all in
|
|
287
|
+
`infra/api_delivery_endpoint/src/config.js`, and nowhere else. Every other
|
|
288
|
+
required key on the platform — `RABBITMQ_URL` in the dispatcher, `REDIS_URL` in
|
|
289
|
+
the gateway and in `ServiceWrapper`, `SECRETS_MASTER_KEY` — failed with no fix at
|
|
290
|
+
all. A rule that holds only where somebody remembered it is enforced by review,
|
|
291
|
+
not by a mechanism (`automation-gates.md` §5); this moves it into the mechanism.
|
|
292
|
+
|
|
293
|
+
`requireEnv`, `requireNumberEnv`, `requireFloatEnv` and `requireBoolEnv` now take
|
|
294
|
+
a third argument, `{ file }`, and compose the whole sentence themselves:
|
|
295
|
+
|
|
296
|
+
- with the owning file: `Fix: set REDIS_URL in config/env-active/shared.env.`
|
|
297
|
+
- without it: §5's own generic wording, verbatim.
|
|
298
|
+
|
|
299
|
+
The file is a **bare name** (`shared.env`); the helper writes the
|
|
300
|
+
`config/env-active/` prefix, so the message has one shape rather than as many as
|
|
301
|
+
there are spellings. A value carrying a path, or not ending in `.env`, is refused
|
|
302
|
+
**at the call**, not the day the key goes missing — a typo must fail on the happy
|
|
303
|
+
path too (principle 4).
|
|
304
|
+
|
|
305
|
+
Why the file is an input and not a derivation, measured in d.399:
|
|
306
|
+
`api/config/shared-env.json` is the single owner of the platform's shared key set
|
|
307
|
+
and would answer "which file carries this key" — but only build-time tooling
|
|
308
|
+
reads it (`oa-sync-template shared-env` in `@onlineapps/conn-orch-validator`). It
|
|
309
|
+
is absent from a service's runtime; a biz repository does not carry it at all. A
|
|
310
|
+
library resolving it through `__dirname`/cwd would break principle 1 and invent a
|
|
311
|
+
second map of the same fact. So the call site names the file, the declaration
|
|
312
|
+
stays the authority review checks it against, and no second map exists.
|
|
313
|
+
|
|
314
|
+
The message is built as ONE template literal rather than assembled from parts,
|
|
315
|
+
because `tests/unit/error-message-contract.test.js` reads throws statically: a
|
|
316
|
+
message it cannot resolve is reported as context-less, and a probe that cannot
|
|
317
|
+
see is not a mechanism. Its census moved 40 → 41 with the new fail-fast on a
|
|
318
|
+
malformed `options.file`.
|
|
319
|
+
|
|
320
|
+
### `INFRASTRUCTURE_HEALTH_REDIS_TTL` má jediného vlastníka — registr; druhá deklarace pryč (d.391)
|
|
321
|
+
|
|
322
|
+
`defaults.js` deklaroval `infrastructureHealthRedisTtlSeconds: 30`, `config.js` k tomu
|
|
323
|
+
env `INFRASTRUCTURE_HEALTH_REDIS_TTL` a `getInfrastructureHealthConfig()` to vydával jako
|
|
324
|
+
`redisKeyTTL`. Nikdo to nečetl a hodnota byla navíc nepravdivá.
|
|
325
|
+
|
|
326
|
+
**Čtyři otázky `change-discipline.md` § Removing:**
|
|
327
|
+
|
|
328
|
+
1. **Proč vznikla:** hlavička `defaults.js` to říká sama — „Keep infra-related defaults
|
|
329
|
+
here to avoid duplication across files". Blok health timingů měl mít jednoho vlastníka
|
|
330
|
+
a TTL Redis klíče do něj bylo počítáno.
|
|
331
|
+
2. **Která část koncepce ji nesla a stojí dál:** „jeden vlastník bloku health timingů"
|
|
332
|
+
platí — jen jmenuje špatného vlastníka. Redis klíč `infrastructure:health:*` píše
|
|
333
|
+
**jedině registr** (`infrastructureHealthTracker.js`, `setEx(key, this.redisKeyTTL, …)`)
|
|
334
|
+
a hodnota dává smysl jen vedle prahu, který musí přežít (`gracePeriod` 60 s); ten práh
|
|
335
|
+
tenhle balíček nedeklaruje a deklarovat nemá.
|
|
336
|
+
3. **Proč to dnes nikdo nečte:** registr přestal spreadovat celý
|
|
337
|
+
`getInfrastructureHealthConfig()` a bere z něj přesně dva klíče — `queueName`
|
|
338
|
+
(`config.js:28`) a `publishInterval` (`config.js:296`); zbytek bloku si deklaruje sám
|
|
339
|
+
(`redisKeyTTL: optionalNumberEnv('INFRASTRUCTURE_HEALTH_REDIS_TTL', 90)`, `config.js:316`).
|
|
340
|
+
Změřeno 2026-09-14 grepem `redisKeyTTL` přes všechny `src` adresáře `infra`, `shared`
|
|
341
|
+
i `api_biz` (pozitivní kontrola `publishInterval` = 10 shod): mimo registr a samotnou
|
|
342
|
+
deklaraci nula čtenářů.
|
|
343
|
+
4. **Je náhrada koncepčnější:** ano. Hodnota stojí v bloku, kde je vidět invariant
|
|
344
|
+
„TTL > gracePeriod", a je jenom jedna. Druhá kopie mohla s tou první už jen nesouhlasit
|
|
345
|
+
— a nesouhlasila: 30 versus živých 90 (3× interval sondy).
|
|
346
|
+
|
|
347
|
+
Sady: unit **260 → 265/265** (nová `tests/unit/healthTimingOwnership.test.js`, 5 testů:
|
|
348
|
+
klíč pryč z defaultů, pryč z výstupu getteru, env `INFRASTRUCTURE_HEALTH_REDIS_TTL=777`
|
|
349
|
+
výstup nezmění, a dva kontrolní — `queueName`/`publishInterval` beze změny, blok má přesně
|
|
350
|
+
zbylých osm klíčů). Integrace **33/33** beze změny.
|
|
351
|
+
|
|
352
|
+
### Záložní e-mail monitoringu: sdílené spojení a opakování dočasného odmítnutí (d.318)
|
|
353
|
+
|
|
354
|
+
`sendMonitoringFailFallbackEmail` otevíral na každý mail nové TCP+TLS+AUTH
|
|
355
|
+
spojení. Změřeno INFRA-monitoringem 2026-09-11: sedm epizod `service_down`
|
|
356
|
+
v jedné sekundě, relay odpověděl `421 4.7.0 … too many connections` **18×**
|
|
357
|
+
a **12 poplachů** skončilo nedoručeno, protože je nic neopakovalo.
|
|
358
|
+
|
|
359
|
+
- **Pool.** Transport má `pool: true` a meze `maxConnections` / `maxMessages` /
|
|
360
|
+
`rateDelta` / `rateLimit` jako **deklarované konfigurační klíče**
|
|
361
|
+
(`INFRA_REPORT_SMTP_MAX_CONNECTIONS`, `…_MAX_MESSAGES`, `…_RATE_DELTA`,
|
|
362
|
+
`…_RATE_LIMIT`), žádné literály v těle. Výchozí hodnoty jsou **odvozené**
|
|
363
|
+
z limitů relay zapsaných v konfirmaci `alert-smtp-relay` 003 (20 spojení /
|
|
364
|
+
10 AUTH za 60 s): jedno sdílené spojení = jedna autentizace na celou dávku.
|
|
365
|
+
- **Opakování 4xx patří transportu.** Dočasné odmítnutí (4yz, RFC 5321 §4.2.1)
|
|
366
|
+
se opakuje zde, ohraničeně (`INFRA_REPORT_SMTP_MAX_ATTEMPTS`, výchozí 3)
|
|
367
|
+
a s rozestupem pod limitem relay (`INFRA_REPORT_SMTP_RETRY_DELAY`, výchozí
|
|
368
|
+
6 000 ms = 60 000/10, backoff se zdvojuje); volající dostane **jeden** verdikt,
|
|
369
|
+
až je po opakováních. 5yz je permanentní a neopakuje se; chyba **bez** kódu
|
|
370
|
+
odpovědi (mrtvý socket, padlý TLS handshake) se za dočasnou nepovažuje.
|
|
371
|
+
Umístění plyne z `delivery-mail-channel` 001 bodu 5 („a refused send is the
|
|
372
|
+
mail service's provider retry") — pro kanál poplachů je „mail service" tento
|
|
373
|
+
modul, takže druhá smyčka u konzumenta by byla zakázané zdvojení.
|
|
374
|
+
- **Fail-fast** na `INFRA_REPORT_SMTP_MAX_ATTEMPTS < 1`: to není „bez
|
|
375
|
+
opakování", je to smyčka, která nikdy neodešle.
|
|
376
|
+
- Logovací řádky `Email sent` / `Email send failed` nově nesou `attempts` —
|
|
377
|
+
verdikt je jeden, tak kolik pokusů stál, je vidět jen tam.
|
|
378
|
+
- README dostalo oddíl o této funkci a v něm **návrh** písemného zdůvodnění
|
|
379
|
+
druhé mailové koleje (podmínka `delivery-mail-channel` 001 § Conditions →
|
|
380
|
+
`duplicity-justification` 001). Je označen jako PROPOSAL: platí až jej
|
|
381
|
+
vlastník přijme.
|
|
382
|
+
|
|
383
|
+
### Marker změny rolí se čte plným klíčem `state:meta:person:roles_version:` (d.382)
|
|
384
|
+
|
|
385
|
+
`ROLES_VERSION_PREFIX` byl holý `person:roles_version:`, ale zapisovatel markeru —
|
|
386
|
+
služba `biz-meta` — ho píše skrz svou stavovou projekci, tedy pod `state:meta:`.
|
|
387
|
+
Čtenář a zapisovatel se tak nikdy nepotkali: `TOKEN_STALE` po změně členství
|
|
388
|
+
nepadalo, protože klíč, který validátor četl, nikdo nezapisoval.
|
|
389
|
+
|
|
390
|
+
- **Kontrakt je plný klíč** `state:meta:person:roles_version:<person_id>` — týž
|
|
391
|
+
tvar, jakým platforma čte každou jinou projekci meta (entitlementy, kredity,
|
|
392
|
+
secrets). Rozhodnutí vlastníka 2026-09-14, konfirmace `redis-state-prefix` 002;
|
|
393
|
+
zamítnuta varianta „holý klíč mimo prefix" (klíč bez vlastníka prefixu, bez
|
|
394
|
+
obnovy z DB, druhá kolej v konektoru kvůli jedinému klíči).
|
|
395
|
+
- Exportovaná konstanta `ROLES_VERSION_PREFIX` zůstává jménem i významem; mění se
|
|
396
|
+
jen její hodnota. Obě hlášky, které ji citují — 401 `TOKEN_PERSON_ID_MISSING`
|
|
397
|
+
a 503 `ROLES_VERSION_CHECK_UNAVAILABLE` — nesou plný klíč, takže čtenář hlášky
|
|
398
|
+
míří na místo, kde marker opravdu je. Nově to tvrdí i testy.
|
|
399
|
+
- Testy tvrdí **kontrakt, ne kód**: unit i integrační tier píší a očekávají klíč
|
|
400
|
+
jako literál, ne přes exportovanou konstantu. Fixtura odvozená z konstanty se
|
|
401
|
+
sveze s jakoukoli hodnotou a zůstane zelená i pro špatný prefix — přesně to tuto
|
|
402
|
+
vadu drželo neviditelnou. Integrační tier píše marker do skutečného Redisu.
|
|
403
|
+
- **Chování se do vydání a pinu v gateway + meta-readeru nemění**: dokud tyto dvě
|
|
404
|
+
služby nepinují novou verzi, `TOKEN_STALE` dál nepadá — stav jako dnes, ne horší.
|
|
405
|
+
Projekci markeru z `tenant_membership` dluží `biz-meta` (tamtéž, § Conditions).
|
|
406
|
+
|
|
407
|
+
### `nodemailer` `^6.9.8` → `^10.0.9` — dvanáct advisories v jednom bumpu (d.317)
|
|
408
|
+
|
|
409
|
+
`npm audit --omit=dev` hlásil na `nodemailer <=9.1.0` dvanáct advisories
|
|
410
|
+
(`high`), mezi nimi GHSA-p6gq-j5cr-w38f (`raw` obchází
|
|
411
|
+
`disableFileAccess`/`disableUrlAccess` → čtení souborů a SSRF, fix až 9.0.6),
|
|
412
|
+
GHSA-rcmh-qjqh-p98v a GHSA-2x7j-588g-ccc2 (DoS v `addressparser`) a
|
|
413
|
+
GHSA-c7w3-x93f-qmm8 (SMTP command injection přes `envelope.size`). Rozsah
|
|
414
|
+
`^6.9.8` na žádnou opravu nedosáhl, takže je knihovna vlekla do každého stromu,
|
|
415
|
+
který ji pinuje (INFRA změřila 8 stromů, `api/shared/TODO.md` § Od vlákna INFRA
|
|
416
|
+
(2026-08-31, dávka 128) a § Od INFRA (2026-09-09, 4)).
|
|
417
|
+
|
|
418
|
+
Čtyři majory mezi 6 a 10 a co z nich se nás týká: **7.0.0** vyhodil staré SES
|
|
419
|
+
SDK (nepoužíváme, transport je SMTP), **8.0.0** přejmenoval chybový kód
|
|
420
|
+
`NoAuth` → `ENOAUTH` (nevětvíme na něj — `monitoringFallbackEmail.js` kód jen
|
|
421
|
+
loguje), **9.0.0** zapnul ověřování TLS certifikátu při stahování vzdáleného
|
|
422
|
+
obsahu a v OAuth2/proxy (ani jedno nepoužíváme), **10.0.0** zvedl minimum na
|
|
423
|
+
Node 20 (máme `>=24.0.0 <25`) a přepsal balíček do TypeScriptu s CJS i ESM
|
|
424
|
+
buildem — `require('nodemailer').createTransport` zůstává funkce (změřeno).
|
|
425
|
+
|
|
426
|
+
Chování transportu se NEMĚNÍ: `createTransport({host, port, secure,
|
|
427
|
+
requireTLS, auth})` a `sendMail()` jsou beze změny. Doložil to integrační test
|
|
428
|
+
`tests/integration/monitoringFallbackEmailStarttls.integration.test.js` proti
|
|
429
|
+
skutečnému SMTP dialogu — relay bez STARTTLS dostane `EHLO`, `STARTTLS`, a
|
|
430
|
+
konec; heslo na drát nejde, kód chyby je pořád `ETLS` a hláška `Error upgrading
|
|
431
|
+
connection with STARTTLS: 502 5.5.1 Command not implemented` doslova. Naměřená
|
|
432
|
+
verze v komentářích toho testu je proto přepsaná na 10.0.9.
|
|
433
|
+
|
|
434
|
+
`npm audit --omit=dev`: **1 high → 0 zranitelností**. Sady: unit 253/253,
|
|
435
|
+
integrace 29/29.
|
|
436
|
+
|
|
437
|
+
### `getCriticalConfigWithFallbacks()` smazán — jméno bez těla i bez konzumenta (d.257d)
|
|
438
|
+
|
|
439
|
+
Funkce měla tělo `return getCriticalConfig();` a vlastní komentář přiznával
|
|
440
|
+
„kept for compatibility but DOES NOT provide fallbacks". Jméno tedy slibovalo
|
|
441
|
+
přesný opak toho, co dělalo, a `architecture-principles.md` §11 (žádné
|
|
442
|
+
přechodové shimy před produkcí) takovou deklaraci zakazuje. Čtyři otázky
|
|
443
|
+
`.claude/rules/change-discipline.md` § Removing (konfirmace
|
|
444
|
+
`declaration-removal` 001):
|
|
445
|
+
|
|
446
|
+
1. **Proč vznikla.** `9133ffa8` („unified infrastructure client config with ENV
|
|
447
|
+
priority") ji zavedl s **opravdovými** hardcoded fallbacky pro
|
|
448
|
+
docker-compose — `redis://api_node_cache:6379`,
|
|
449
|
+
`amqp://guest:guest@api_services_queuer:5672` — a v témže commitu označil
|
|
450
|
+
`getCriticalConfig` za `DEPRECATED - use getCriticalConfigWithFallbacks()
|
|
451
|
+
instead`. Byla to tehdy doporučená cesta.
|
|
452
|
+
2. **Která část koncepce ji nesla a stojí-li dál.** „Knihovna zná topologii a
|
|
453
|
+
dosadí ji, když env chybí." Ta část **nestojí**: `architecture-principles.md`
|
|
454
|
+
§2 (No Hardcoded Values), §3 (No Fallbacks) a rozhodnutí, že topologie je
|
|
455
|
+
fail-fast bez defaultů.
|
|
456
|
+
3. **Proč ji dnes nikdo nečte.** `a0f0a04c` („rollout: enforce strict runtime
|
|
457
|
+
config") jí vzal tělo, takže zbylo jméno delegující na `getCriticalConfig`;
|
|
458
|
+
poslední konzument (mock v `monitoring-core`) padl v `e58a0afa` (d.154a).
|
|
459
|
+
Role se přesně obrátily — deprecated je dnes ta funkce, ne `getCriticalConfig`.
|
|
460
|
+
4. **Je náhrada koncepčnější?** Ano. `getCriticalConfig` je fail-fast a jeho
|
|
461
|
+
hláška pojmenuje klíč i fix. Proto „smazat", ne „zapojit".
|
|
462
|
+
|
|
463
|
+
Konzumenti: **nula** v produkčním kódu celého workspace (`grep` přes
|
|
464
|
+
`*.js`/`*.json`/`*.md` mimo `node_modules`; jediné zbylé výskyty byly deklarace
|
|
465
|
+
a testy). Nález a doporučení „smazat, testy přesměrovat" na sobě mělo vlákno
|
|
466
|
+
INFRA-DOCS od 2026-09-02 (`api/shared/TODO.md`, dávka 103) — tahle změna ho
|
|
467
|
+
provádí.
|
|
468
|
+
|
|
469
|
+
Chování, na kterém záleží, zůstává pokryté: test fail-fastu při chybějící
|
|
470
|
+
`REDIS_URL` se v `tests/unit/defaults.test.js` přesunul na `getCriticalConfig`,
|
|
471
|
+
včetně doslovné hlášky.
|
|
472
|
+
|
|
473
|
+
### Čísla z prostředí čte resolver — `requireNumberEnv` deleguje, `requireFloatEnv` přibyl (d.257)
|
|
474
|
+
|
|
475
|
+
`requireNumberEnv()` parsoval hodnotu vlastním `Number(raw)` s vlastní hláškou
|
|
476
|
+
`[ServiceConfig] … must be a number`, zatímco sesterský `optionalNumberEnv()`
|
|
477
|
+
šel přes `@onlineapps/runtime-config` `type: 'number'`. Dvě definice toho, co je
|
|
478
|
+
číslo, nad jedněmi a týmiž env klíči
|
|
479
|
+
(`.claude/rules/change-discipline.md` § One rail per concern). Nově obě jdou
|
|
480
|
+
přes resolver: koerci i hlášku o chybném tvaru vlastní výhradně on, duplicitní
|
|
481
|
+
parsování v service-common zmizelo.
|
|
482
|
+
|
|
483
|
+
Ostrost celočíselné koerce **následuje pin resolveru**, ne tuhle změnu: s dnešním
|
|
484
|
+
`@onlineapps/runtime-config` 1.0.3 se neceločíselná hodnota pořád ořízne
|
|
485
|
+
`parseInt`em, s vydáním d.253 (`type: 'number'` = jen celé číslo) bude odmítnuta.
|
|
486
|
+
Proto unit testy ověřují **kontrakt delegace** (jaké schéma helper resolveru
|
|
487
|
+
předá a že vrátí jeho hodnotu beze změny), ne koerci samu — ta patří testům
|
|
488
|
+
resolveru.
|
|
489
|
+
|
|
490
|
+
Přibyl `requireFloatEnv(name, description)` → `type: 'float'` pro klíče, které
|
|
491
|
+
jsou poměrem nebo faktorem, ne počtem. Volba mezi oběma kolejemi je vlastnost
|
|
492
|
+
klíče, a proto patří na volání.
|
|
493
|
+
|
|
494
|
+
⚠️ **Kaskáda:** `TRACE_COMPLETION_RATE_THRESHOLD` (api_monitoring `index.js:211`,
|
|
495
|
+
hodnota `0.8`) musí přejít na `requireFloatEnv` ve stejném kaskádovém commitu
|
|
496
|
+
jako pin service-common — jinak monitoring buď tiše ztratí alert (dnes: `0.8` →
|
|
497
|
+
`0`, a watchdog nulu přijímá jako legitimní práh,
|
|
498
|
+
`watchers/workflowWatchdog.js:204-208`), nebo nenabootuje (po vlně). Je to jediný
|
|
499
|
+
desetinný klíč ze 33, které přes `requireNumberEnv` jdou — změřeno d.257 proti
|
|
500
|
+
`config/env-active/*.env`.
|
|
501
|
+
|
|
502
|
+
Tvar hlášky o **chybějící** proměnné se nemění:
|
|
503
|
+
`[ServiceConfig] Missing environment variable - <NAME> is required. <popis>`,
|
|
504
|
+
společný pro `requireEnv` / `requireNumberEnv` / `requireFloatEnv` /
|
|
505
|
+
`requireBoolEnv`.
|
|
506
|
+
|
|
507
|
+
### `requireTLS: true` u záložního e-mailu monitoringu (d.231)
|
|
508
|
+
|
|
509
|
+
`monitoringFallbackEmail.js` stavěl nodemailer transport bez `requireTLS`, takže
|
|
510
|
+
proti relayi s `smtpd_tls_security_level = may` by při chybějícím STARTTLS odešlo
|
|
511
|
+
`AUTH PLAIN` po nešifrovaném soketu. Flag je nastaven bezpodmínečně (u implicitního
|
|
512
|
+
TLS na 465 nic nemění, žádná větev podle prostředí). Integrační test staví SMTP
|
|
513
|
+
server bez STARTTLS na loopbacku: odeslání je odmítnuto (`ETLS`) a server `AUTH`
|
|
514
|
+
nikdy nedostane; kontrolní klient bez požadavku STARTTLS `AUTH` odešle.
|
|
515
|
+
|
|
516
|
+
### `redactUrl()` — připojovací URL v logu bez credentialu (d.245)
|
|
517
|
+
|
|
518
|
+
`REDIS_URL` nese heslo k Redisu (jedna kolej, rozhodnutí leada INFRA
|
|
519
|
+
2026-09-10), takže každé místo, které URL logovalo doslova, byl únik do Loki.
|
|
520
|
+
Nový `redactUrl(url)` (`src/redactUrl.js`, exportován z indexu) vrací totéž URL
|
|
521
|
+
bez userinfo — `redis://:heslo@cache:6379` → `redis://cache:6379`. Hodnota,
|
|
522
|
+
která není URL s hostitelem, se nahradí `<unparseable-url>`; neechuje se
|
|
523
|
+
(`new URL('cache-host:6379')` uspěje a přečte `cache-host:` jako schéma, proto
|
|
524
|
+
ta kontrola).
|
|
525
|
+
|
|
526
|
+
Předěláno na něj na třech místech, která URL psala doslova:
|
|
527
|
+
|
|
528
|
+
- `src/infrastructure/waitForInfrastructureReady.js` — `Redis URL: …` při každém bootu
|
|
529
|
+
- `src/infrastructure/waitForHealthCheckQueueReady.js` — totéž
|
|
530
|
+
- `src/redisClient.js` — `url` v payloadu každého `[Redis] Error`
|
|
531
|
+
|
|
532
|
+
Doplněno v d.245b i na zbylá tři místa v `connectRedis()` — obě hlášky
|
|
533
|
+
`[Redis] Connected` (úspěšná cesta, každý boot) a text timeoutové chyby. Aby se
|
|
534
|
+
redakce odvodila jednou na klienta, vrací `createRedisClient()` nově vedle `url`
|
|
535
|
+
i `loggedUrl`: `url` je skutečná adresa a jde klientovi, `loggedUrl` je jediná,
|
|
536
|
+
která smí do logu nebo do textu chyby. Šest míst ze šesti.
|
|
537
|
+
|
|
538
|
+
## [2.0.1] — 2026-08-29
|
|
539
|
+
|
|
540
|
+
- Changelog started with this release; the history before it is in git (`git log -- shared/service-common`).
|