@onlineapps/conn-orch-registry 6.0.3 → 8.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 ADDED
@@ -0,0 +1,621 @@
1
+ # Changelog — @onlineapps/conn-orch-registry
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
+ ## [8.0.0] — 2026-10-03
8
+
9
+ pin: `@onlineapps/mq-client-core` 5.0.0 → 5.1.0
10
+
11
+ ### Changed — BREAKING: `discoveryTimeoutMs` je povinný, `getService()` má termín a typuje selhání (d.1093)
12
+
13
+ - Nový povinný parametr konstruktoru `discoveryTimeoutMs` (kladné číslo ms, bez výchozí hodnoty;
14
+ chybějící i neplatná hodnota se odmítne při konstrukci). Hodnota patří platformnímu env, čte ji
15
+ `@onlineapps/service-wrapper`.
16
+ - `getService()` dřív při ležícím nebo půlotevřeném Redisu čekal bez termínu (node-redis drží příkaz
17
+ v offline frontě nebo čeká na odpověď, d.1088, d.1090) a krok workflow visel, dokud broker po
18
+ `consumer_timeout` nezavřel kanál a zpráva neexpirovala do DLQ. Nově:
19
+ - dotaz, který neskončí do `discoveryTimeoutMs` → `Error` s `code: 'REGISTRY_DISCOVERY_TIMEOUT'`,
20
+ `type: 'TRANSIENT'`, `serviceName`, bez `cause` (nic pod ním neselhalo);
21
+ - odmítnutí, když klient hlásí `isReady === false` (offline s `disableOfflineQueue`, socket zavřený
22
+ pod příkazem) → `code: 'REGISTRY_DISCOVERY_UNAVAILABLE'`, `type: 'TRANSIENT'`, původní chyba v `cause`;
23
+ - jiné odmítnutí (chybová odpověď připojeného Redisu) projde beze změny.
24
+ Typuje se tady, kde je selhání známé (vzor mq-client-core d.683); `@onlineapps/cookbook-router`
25
+ převezme `type` z `cause`, klasifikátor L1 Redis nezná.
26
+ - **Proč termín závodem a ne `AbortSignal`:** node-redis 4.7.1 (`@redis/client` 1.6.1) nabízí
27
+ `commandOptions({ signal })`, ale abort příkazu, který už odešel na server, odebere jeho uzel z fronty
28
+ podruhé (posluchač abortu zůstane po odeslání připojený), délka fronty klesne pod nulu a další
29
+ `flushAll` — chyba socketu při `disableOfflineQueue`, nebo `disconnect()` — hodí
30
+ `TypeError: Cannot read properties of undefined (reading 'reject')`; z handleru chyby socketu je to
31
+ nezachycená výjimka, která ukončí proces (změřeno d.1093 proti falešnému RESP serveru). Závod příkaz
32
+ nechá ve frontě: pozdní odpověď se spáruje správně a jeho pozdní odmítnutí se zachytí a zaloguje
33
+ (`debug`), nikdy nezůstane neošetřené.
34
+ - devDependency `redis` 4.7.1 (přesně) pro integrační sadu `tests/integration/discoveryDeadline.integration.test.js`
35
+ (živý node-redis proti falešnému RESP serveru; dev Redis se nedotýká).
36
+
37
+ ### Fixed — `deregister()` zastaví heartbeat PŘED publikací, ne až po ní (d.1083)
38
+
39
+ Smyčka se dřív zastavovala až po úspěšné publikaci zprávy `deregister`; když publikace selhala,
40
+ volání odmítlo, ale smyčka tepala dál za kopii, která se odhlašuje. `stopHeartbeat()` je teď první
41
+ krok `deregister()`, ať publikace dopadne jakkoli. Úspěšná deregistrace se chová stejně jako dřív
42
+ (zpráva odejde, `heartbeatStopped: true`). Sada `tests/unit/registryClient.deregisterStopsHeartbeat.test.js`.
43
+ `@onlineapps/service-wrapper` zastavuje smyčku v `shutdown()` sám před `unregister()` (d.1076), pro něj
44
+ je změna neutrální.
45
+
46
+ ### Documentation — docblocky `startHeartbeat` a konstruktoru (`heartbeatInterval`) říkají pravdu (d.1083)
47
+
48
+ `startHeartbeat`: rozpočet selhání žije jen tady, wrapper smyčku zapíná a vlastní nemá (odkaz na
49
+ service-wrapper d.1076 a jeho CHANGELOG místo ručně psaného data). `opts.heartbeatInterval`: wrapper
50
+ kadenci čte z env `BIZ_HEARTBEAT_INTERVAL_MS`; klíč `wrapper.registry.heartbeatInterval` je zrušený
51
+ a wrapper ho odmítá. `API.md` přegenerován (`npm run docs`). Tutéž pravdu teď říká i `Fix:` hlášky
52
+ konstruktoru při chybějícím `heartbeatInterval` (dřív radila právě zrušený klíč; test
53
+ `registryClient.registryAnswer.test.js` tvrdí celou `Fix:` větu), `README.md` (vlastník kadence)
54
+ a hlavička sady `registryClient.registryAnswer.test.js`.
55
+
56
+ ### Documentation — `examples/` srovnané na kontrakt 8.0.0 (d.1187)
57
+
58
+ Oba příklady stavěly klienta, kterého konstruktor odmítne: chyběl `instanceId` (d.817) a
59
+ `discoveryTimeoutMs` (d.1093), `event-consumer-example.js` navíc `specificationEndpoint` a
60
+ `heartbeatInterval` (d.278c). Registrovaly operace ve tvaru v2 (`endpoint`, `method`, neprázdné
61
+ `endpoints`), který registr odmítá (`schema-v3.md` § Keys the v3 schema rejects). Nově:
62
+ - každá povinná volba je zapsaná výslovně; nastavení se čte z env podle jména (klíče platformy:
63
+ `RABBITMQ_URL`, `REDIS_URL`, `SERVICE_INSTANCE_ID`, `BIZ_HEARTBEAT_INTERVAL_MS`,
64
+ `REGISTRY_DISCOVERY_TIMEOUT_MS`, …) přes `examples/env.js`, který chybějící, prázdný nebo
65
+ nečíselný klíč odmítne s `Fix:`; žádný `||` fallback (dřív `REDIS_HOST || 'localhost'`,
66
+ `MINIO_ACCESS_KEY || 'minioadmin'` aj.);
67
+ - operace ve tvaru v3 (`handler`, `bundle_scope`, `mutates`, `resource_type`, `minRole`, `input`,
68
+ `output`), `endpoints: []`, `metadata: { version, description }`;
69
+ - `register()` se větví podle verdiktu registru (`success`, `errorCode`), ne podle událostí
70
+ `registerConfirmed`/`registerRejected`, které klient nevysílá; ukončení volá `deregister()` před
71
+ `close()`;
72
+ - `event-consumer-example.js` běží v režimu jen-index bez `storageConfig`: `subscribeToChanges()`
73
+ staví konzumenta bez `StorageConnector` a úplný `storageConfig` by konstruktor
74
+ `RegistryEventConsumer` odmítl; Redis je node-redis (`createClient`), tentýž klient, kterým
75
+ `getService()` čte projekci.
76
+ - devDependency `ioredis` odebrána: jejím jediným čtenářem byl `event-consumer-example.js`.
77
+ - `README.md` § Quick Start čte `instanceId` přes `requireEnv` jako příklady, ne jako holé
78
+ `process.env.SERVICE_INSTANCE_ID` (d.1187b); `amqpUrl` přes `requireEnv('RABBITMQ_URL', …)` místo
79
+ holého `process.env.AMQP_URL`, `heartbeatInterval` a `discoveryTimeoutMs` z klíčů platformy
80
+ `BIZ_HEARTBEAT_INTERVAL_MS` a `REGISTRY_DISCOVERY_TIMEOUT_MS` místo čísel napsaných v kódu;
81
+ § Configuration už netvrdí, že se konfigurace dá předat proměnnými prostředí (klient žádnou
82
+ nečte) (d.1187c).
83
+
84
+ Nová sada `tests/unit/examples.contract.test.js`: každý příklad projde `node --check`, nenese `||`
85
+ ani `??`, nedeklaruje klíč operace v2 a konstruktoru předá všechny povinné volby jménem, žádnou jako
86
+ holé `process.env`. Seznam povinných voleb sada sama ověří proti konstruktoru (bez kterékoli z nich
87
+ konstrukce selže a hláška ji jmenuje), takže nová povinná volba sadu shodí dřív, než ji příklad tiše
88
+ vynechá. Příklady sada nespouští (připojují se k RabbitMQ a Redisu).
89
+
90
+ ## [7.0.0] — 2026-09-27
91
+
92
+ pin: `@onlineapps/mq-client-core` 4.2.0 → 5.0.0
93
+
94
+ ### Documentation — `API.md` je součástí tarballu (d.1039)
95
+
96
+ `files` teď jmenuje `API.md`; dokument ležel v balíčku, ale do vydaného artefaktu se nedostal.
97
+
98
+ ### Changed — BREAKING — odpověď na registraci chodí na frontu instance `<service>.registry.<instance_id>` (d.817c)
99
+
100
+ Konfirmace `registry-instance-identity` 001 (c): odpověď na registraci jde na frontu TÉ kopie, která
101
+ se registrovala, přes existující `responseQueue`. Sdílenou `<service>.registry` dostávají od registru
102
+ jen podněty. Klient teď:
103
+ - ve zprávě `register` posílá `responseQueue: "<service>.registry.<instance_id>"` (dřív
104
+ `"<service>.registry"`); nová vlastnost `instanceResponseQueue`;
105
+ - v `init()` konzumuje DVĚ fronty, obě s `requeueOnError: false`: sdílenou `<service>.registry`
106
+ (podněty registru, např. `revalidate`, které `api_services_registry` posílá na
107
+ `<serviceName>.registry`) a frontu instance (odpovědi);
108
+ - hlášku o vypršení registrace jmenuje frontou instance a log `Initialized` nese `promptQueue` a
109
+ `responseQueue`.
110
+
111
+ Heartbeat a `deregister` se nemění. Pořadí z konfirmace 002 platí: šablona `registry.{instance_id}`
112
+ s dead-letter cestou musí být v pinovaném `@onlineapps/mq-client-core` (d.817b), jinak `consume()`
113
+ frontu instance odmítne. Tato změna proto vyžaduje mq-client-core s d.817b. Server registru
114
+ (`registry.listener.js`, `validationResponse.listener.js`) odpovídá na `msg.responseQueue` ze
115
+ zprávy a změnu nepotřebuje.
116
+
117
+ Proč BREAKING: mění se veřejný tvar zprávy `register` (`responseQueue`), topologie, kterou klient
118
+ deklaruje (nová trvalá fronta na instanci), a minimální `@onlineapps/mq-client-core` (musí nést d.817b).
119
+
120
+ Životnost fronty instance (`auto-delete`, `x-expires`, trvalá) rozhodnutá není (konfirmace 002,
121
+ Conditions; šablona d.817b ji záměrně nenese). Fronta je trvalá a po skončení kopie zůstává.
122
+
123
+ ### Documentation — README, průvodce a komentáře testů ukazují na vlastníka drátu jeho jménem (d.999b, d.999d)
124
+
125
+ Balíček citoval kontrakt registrace starým jménem dokumentu, který v `docs/biz/30-operations/` není.
126
+ Odkazy přitom vedly na `registration-wire.md`. Všechna místa teď jmenují existující uzel a jeho
127
+ sekci:
128
+ - úvod README: `registration-wire.md §2` (MQ wire format) (d.999b);
129
+ - komentář v příkladu `register()` v README a parametr `serviceInfo.operations` v
130
+ `docs/REGISTRY_CLIENT_GUIDE.md`: `§3` (Operation schema);
131
+ - úvod průvodce a komentáře ve třech unit testech: `§2.1` (`register`) (d.999d).
132
+
133
+ ### Changed — BREAKING — konstruktor `ServiceRegistryClient` vyžaduje `instanceId`; `instance_id` jde v `register`, `heartbeat` i `deregister` (d.817)
134
+
135
+ Každá zpráva na drát teď říká nejen, KTERÁ služba, ale i KTERÁ běžící kopie (konfirmace
136
+ `registry-instance-identity` 001): `register`, `heartbeat` a `deregister` nesou `instance_id`.
137
+ Registr podle něj maže jen při shodě, takže krátce žijící cizí kopie už neodhlásí tu živou.
138
+ Konstruktor bez `instanceId` (nebo s prázdným řetězcem) skončí chybou:
139
+ `[RegistryClient] <service>: Missing instanceId - got undefined. Expected: a non-empty string naming
140
+ THIS running copy of the service, the same value for the whole process; … Fix: pass the instance id
141
+ the wrapper made once at boot (SERVICE_INSTANCE_ID when the deployment gives one, otherwise a UUID v4)
142
+ - this client never makes one, because the same value also reaches monitoring-core as serviceInstanceId.`
143
+
144
+ Migrace volajícího: předat `instanceId`, jednu hodnotu na proces. `@onlineapps/service-wrapper` ji
145
+ předává (`this.instanceId`, stejná hodnota jde do monitoringu jako `serviceInstanceId`). Kdo staví
146
+ klienta sám, musí ji doplnit. Registrační odpověď dál chodí do sdílené fronty `<service>.registry`,
147
+ viz komentář u `serviceRegistryQueue` v `src/registryClient.js`.
148
+
149
+ ### Changed — BREAKING — `register()` přijímá jen klíče registračního kontraktu (d.702)
150
+
151
+ `register(serviceInfo)` odmítne klíč, který kontrakt nezná (`registration-wire.md` §2.1). Dřív ho
152
+ tiše zahodil. Povolené klíče: `endpoints`, `operations`, `metadata`, `health`, `workspaceScoped`,
153
+ `timeout`. Hláška:
154
+ `[RegistryClient] <service>: serviceInfo key "foo" is not part of the registration contract - Expected
155
+ one of: endpoints, operations, metadata, health, workspaceScoped, timeout (api/docs/biz/30-operations/
156
+ registration-wire.md §2.1). Fix: delete "foo" from the object passed to register(), or send the value
157
+ under the key the contract names for it.`
158
+ Zaniklé klíče si dál nesou vlastní hlášku (např. `token`: `serviceInfo carries the retired key "token" …`).
159
+ Migrace: z objektu pro `register()` odstranit vše mimo šest klíčů výše.
160
+
161
+ ### Changed — druhý souběžný `register()` pod týmž request id a druhé `startHeartbeat()` se odmítnou, místo tichého přepisu (d.760b, d.760c)
162
+
163
+ - `register()`, dokud čeká registrace se stejným `id`, skončí chybou `[RegistryClient] <service>: Duplicate
164
+ request id "<id>" - a registration with this id is still pending. … Fix: await the register() already in
165
+ flight before starting another one …`. Dřív druhé volání přepsalo záznamy prvního a handle jeho timeru
166
+ zmizel, takže starší timer doběhl celý timeout bez možnosti ho zrušit. S `uuid.v4` se id neopakují,
167
+ takže běžný volající to nepozná.
168
+ - `startHeartbeat()` nad klientem, jehož smyčka běží, skončí chybou `[RegistryClient] <service>: A heartbeat
169
+ loop is already running - … Fix: call client.stopHeartbeat() before starting a new loop, or keep the loop
170
+ that is already running.`. Dřív přepsal handle a první smyčka tepala dál bez možnosti ji zastavit.
171
+
172
+ ### Fixed — `RegistryEventConsumer` bere jméno výměny `registry.changes` z `queueConfig` (d.809); `stopHeartbeat()` ruší jen `setTimeout` (d.752c)
173
+
174
+ - Jméno výměny, na kterou se odběratel změn váže, vlastní `@onlineapps/mq-client-core`
175
+ (`queueConfig.queueName('registry', 'changes')`), ne literál v balíčku. Hodnota se nemění:
176
+ `registry.changes`.
177
+ - `stopHeartbeat()` volal nad týmž handlem `clearTimeout` i `clearInterval`, přestože smyčka je
178
+ rekurzivní `setTimeout`. Druhé volání je pryč, chování se nemění.
179
+
180
+ ### Changed — `CHANGELOG.md` je součástí tarballu (d.997)
181
+
182
+ `package.json` `files` nese `CHANGELOG.md`. Vydaná 6.0.3 ho nenesla.
183
+
184
+ ## [6.0.3] — 2026-09-25
185
+
186
+ pin: `@onlineapps/mq-client-core` 4.1.0 → 4.2.0
187
+
188
+ ## [6.0.2] — 2026-09-24
189
+
190
+ pin: `@onlineapps/mq-client-core` 4.0.1 → 4.1.0
191
+
192
+ ## [6.0.1] — 2026-09-17
193
+
194
+ pin: `@onlineapps/mq-client-core` 4.0.0 → 4.0.1
195
+
196
+ ## [6.0.0] — 2026-09-17
197
+
198
+ ### Changed (testy) — odmítnutá deklarace se tvrdí hodnotou značky (d.579b)
199
+
200
+ `tests/integration/ensureQueuesRefusal.integration.test.js` čekal `cause.code === 406` z doby pinu jádra 3.2.0; pinované
201
+ `@onlineapps/mq-client-core` 4.x značí odmítnutou deklaraci u zdroje (d.582), takže případ teď tvrdí `code: MQ_CONNECTION_FATAL`,
202
+ `reason: broker-refused` a 406 brokeru čte o patro níž (`cause.cause.code`). Sada byla od re-pinu červená (W592 měření).
203
+
204
+ ### Removed — BREAKING: `deployable` odchází z registrační zprávy (d.592)
205
+
206
+ Verdikt shody s uniformou jezdil na registraci od d.221 a **nikdo ho nečetl**:
207
+ `processRegistration` (`api/infra/api_services_registry/src/listeners/registry.listener.js`)
208
+ zprávu destrukturuje bez něj a jiný čtenář na platformě není (`grep -rn deployable`
209
+ nad `api/infra/**` a `fe_adminui/src` vrací prázdno). Slib konfirmace
210
+ `biz-service-manifest` 001 §4 — „registr ho promítne do
211
+ `infrastructure:health:<service>` vedle `status`" — zůstal bez implementace.
212
+
213
+ Čtyři otázky `change-discipline.md` § Removing something removes its declaration:
214
+
215
+ - **Vznik.** d.221 klíč zavedla, aby nasaditelnost dojela z běhu služby k registru.
216
+ - **Role v koncepci.** 001 §4: dvě závažnosti, dva důsledky — `boot` shodí start,
217
+ `deploy` nechá službu běžet a označí ji jako nenasaditelnou.
218
+ - **Proč nikdo nečte.** Projekce nikdy nevznikla, a mezitím se ukázalo, že
219
+ verdikt z běžícího kontejneru nevypovídá o repozitáři: produkční obraz není git
220
+ checkout a nenese compose ani README, takže krok 7 nad ním hlásí 12 nálezů
221
+ nepřítomnosti — na drát by u každé zdravé služby jel `false`.
222
+ - **Náhrada je koncepčnější.** Nasaditelnost dokládá z každého commitu CI job
223
+ `validate-uniform` nad checkoutem (konfirmace `biz-service-manifest` 010), tedy
224
+ jediné místo, které vidí, co dostane klon. Rozhodnutí vlastníka 2026-09-17,
225
+ konfirmace `biz-service-manifest` 011.
226
+
227
+ S klíčem odešel i jeho `@param` v JSDoc a blok `if ('deployable' in serviceInfo)`;
228
+ `API.md` je zregenerovaná ze zdrojů (`npm run docs`).
229
+
230
+ ### Changed — BREAKING: `register()` odmítá zaniklý klíč jménem (d.592)
231
+
232
+ `deployable` je šestý řádek tabulky `RETIRED_SERVICE_INFO_KEYS` (vedle `spec`,
233
+ `token`, `validationToken`, `secret`, `tokenSecret` z d.562). Volání, které ho
234
+ ještě pošle, **skončí výjimkou před publikací**, ne tiše bez účinku:
235
+
236
+ [RegistryClient] biz-invoicing: serviceInfo carries the retired key
237
+ "deployable" - the manifest-conformance verdict of a boot-time validation run
238
+ (owner decision 2026-09-17, confirmation
239
+ `api/docs/governance/confirmations/biz-service-manifest.md` 011). … Fix:
240
+ delete "deployable" from the object passed to register().
241
+
242
+ Odmítnutí se ptá na **přítomnost klíče, ne na jeho hodnotu** — `false` i `null`
243
+ byly stavy, které wrapper posílal nejčastěji, a kontrola podle pravdivosti by je
244
+ propustila.
245
+
246
+ Dopad: `ServiceWrapper._buildRegistrationPayload()` klíč dnes posílá, takže
247
+ wrapperová půlka změny (d.591) musí přistát ve stejné vlně; bez ní boot skončí
248
+ touto výjimkou. Pro ostatní přímé volající `register()` platí `Fix:` z hlášky.
249
+
250
+ Sady: unit 19 suites / 205 testů (o jednu sadu méně — `registryClient.deployable`
251
+ i `deployable-on-the-wire` odešly s klíčem, který tvrdily); integrace proti
252
+ živému brokeru včetně případu `deployable: false` odmítnutého dřív, než něco
253
+ odjede.
254
+
255
+ ## [5.0.0] — 2026-09-16
256
+
257
+ ### Fixed — klasifikace chyby přežije i rewrap ve FÁZI 0.8 (d.579)
258
+
259
+ `ensureQueues()` byla **třetí** cesta, která obalovala pád do
260
+ `new Error(msg, { cause })` — a ta konstrukce nenese žádnou vlastní vlastnost,
261
+ takže `code` i `reason`, které na chybu napsal
262
+ `@onlineapps/mq-client-core`, tady umíraly. d.532 opravila dvě cesty FÁZE 0.7
263
+ (`QueueManager.init()`, `RegistryClient.init()`); deklarace business front ve
264
+ FÁZI 0.8 zůstala. `@onlineapps/service-wrapper` rozhoduje podle `error.code`,
265
+ takže odmítnutí z této cesty dostal jako neklasifikované a ukončil ho jako
266
+ přechodné.
267
+
268
+ Teď se klasifikace přenáší týmž vzorem jako v `init()`: `refusal.code =
269
+ assertErr.code`, `refusal.reason = assertErr.reason`, původní chyba zůstává
270
+ v `cause` a věta, kterou tato vrstva přidává (která fronta, co to spraví), je
271
+ beze změny.
272
+
273
+ **Nic se tu neklasifikuje**: co zdroj nechal neoznačené, zůstává neoznačené.
274
+ Tenhle balíček nečte AMQP reply kódy ani nematchuje text hlášky — otázku „odpověděl
275
+ broker a odmítl?" vlastní `@onlineapps/mq-client-core` (`REFUSAL_REPLY_CODES`,
276
+ `_isBrokerRefusal()`) a druhá kopie toho seznamu by se od něj mohla rozejít.
277
+
278
+ Změřeno a nezamlčeno: proti pinu `@onlineapps/mq-client-core` 3.2.0 předává
279
+ `assertQueue()` syrovou chybu z amqplib, takže dnes touto cestou cestuje **číslo
280
+ 406**, ne značka `code: 'MQ_CONNECTION_FATAL'` / `reason: 'broker-refused'`,
281
+ podle které wrapper končí boot natrvalo. d.577 dala značku rail `consume()`
282
+ v jádře; deklarační rail ji zatím nemá. Tento přenos je tedy nutný, ne
283
+ dostačující — a integrační test to říká nahlas místo toho, aby tvrdil značku,
284
+ kterou žádný zdroj nepíše.
285
+
286
+ ### Documentation — komentář v `registryClient.js` ukazoval na zrušený dokument (d.574)
287
+
288
+ Komentář nad propagací `workspaceScoped` posílal čtenáře na
289
+ `biz-service-onboarding.md` §4 — dokument, který 2026-08-16 přešel na
290
+ `api/docs/biz/60-templates/onboarding-checklist.md` (čísla sekcí se přenesla,
291
+ `api/docs/biz/RETIRED-VOCABULARY.md` §4). Nová podoba je `@see` s živou cestou.
292
+ Ostatních 16 citací `api/docs/…` v tomto souboru bylo změřeno (`ls`) a všechny
293
+ cíle existují. Chování ani API se nemění.
294
+
295
+ ### Removed — BREAKING: `spec`, `validationToken` a `tokenSecret` odcházejí z registrační zprávy (d.562)
296
+
297
+ Tři klíče jezdily na každé registraci a nikdo je nečetl. Změřeno 2026-09-16 nad
298
+ `api/infra/api_services_registry/src/**`: `processRegistration` destrukturuje
299
+ `id, serviceName, version, endpoints, operations, metadata, health,
300
+ specificationEndpoint, responseQueue, timestamp, validationProof,
301
+ validationData, workspaceScoped` — a `grep -rn 'validationToken|tokenSecret'`
302
+ nad tím stromem nevrací nic. Kontrakt to už popisoval slovy „emitted, unread"
303
+ (`api/docs/biz/30-operations/registration-wire.md` §2.1).
304
+
305
+ Čtyři otázky `change-discipline.md` § Removing something removes its declaration:
306
+
307
+ - `spec` nesl OpenAPI dokument z doby, kdy registr dereferencoval HTTP cestu
308
+ `/specification`. ADR 0005 vzalo biz kontejnerům HTTP; co registr dnes
309
+ **validuje**, je `operations` podle schema-v3 (§3 téhož kontraktu). Schéma,
310
+ které protistrana kontroluje klíč po klíči, je koncepčnější než dokument,
311
+ který se jen uložil.
312
+ - `validationToken` a `tokenSecret` patřily k JWT token API, které odplulo
313
+ z `@onlineapps/service-validator-core` v d.344; poslední čtenář (HTTP
314
+ `POST /validate`) byl smazán 2026-08-22 a validator si zrušení zapsal do
315
+ vlastní konfigurace (`api/infra/api_services_validator/src/config/config.js`
316
+ § security). Běh dnes dokládá `validationProof` / `validationData` a podepsaný
317
+ certifikát — důkaz, který si příjemce ověří, místo sdíleného tajemství, které
318
+ tahle zpráva vozila po brokeru v čistém tvaru.
319
+
320
+ S klíči padly i dvě jejich záložky: `spec: serviceInfo.spec || null`
321
+ (výchozí hodnota pod `architecture-principles.md` §3) a dva pravopisy jednoho
322
+ pole — `serviceInfo.token || serviceInfo.validationToken`, `serviceInfo.secret
323
+ || serviceInfo.tokenSecret`.
324
+
325
+ ### Changed — BREAKING: `register()` odmítá zaniklý klíč jménem (d.562)
326
+
327
+ Volání, které některý z pěti zaniklých klíčů (`spec`, `token`,
328
+ `validationToken`, `secret`, `tokenSecret`) ještě posílá, **skončí výjimkou
329
+ ještě před publikací** — nikoli tiše bez účinku, protože tiše zahozený klíč
330
+ nechává volajícího věřit, že něco odjelo:
331
+
332
+ [RegistryClient] biz-invoicing: serviceInfo carries the retired key
333
+ "tokenSecret" - the shared secret of that same token API, whose last reader
334
+ (HTTP POST /validate) was deleted 2026-08-22; nothing verifies a token with
335
+ it, and a broker message is no place for a secret. Fix: delete "tokenSecret"
336
+ from the object passed to register().
337
+
338
+ Hláška **nikdy neopakuje předanou hodnotu** — dva z těch klíčů nesly tajemství.
339
+
340
+ `ServiceWrapper._buildRegistrationPayload()` žádný z nich neodesílá, takže pro
341
+ běžící biz služby se nemění nic; dopad je na přímého volajícího `register()`.
342
+
343
+ Sady: unit 20 suites / 199 testů; integrace 6 suites / 38 testů proti živému
344
+ brokeru, včetně nové `registrationPayloadKeys.integration.test.js`, která čte
345
+ zprávu zpět z fronty — `undefined` klíč zmizí až v serializaci, takže to, co
346
+ brokerem opravdu jede, neukáže žádný unit tier.
347
+
348
+ ### Fixed — značka odmítnutí brokerem přežije přebalení chyby (d.532)
349
+
350
+ Obě selhávající cesty tohohle balíčku chybu z jádra **přebalují**, aby dodaly
351
+ fakt, který jádru chybí: který broker (`QueueManager.init()`) a která fronta
352
+ které služby (`RegistryClient.init()`). Dělaly to holým `new Error(msg, { cause })`,
353
+ které nenese žádnou vlastní vlastnost — takže `code: 'MQ_CONNECTION_FATAL'`
354
+ a `reason: 'broker-refused'`, které na chybu napsal její vlastník
355
+ (`@onlineapps/mq-client-core`, `REFUSAL_REPLY_CODES` + `_isBrokerRefusal()`),
356
+ tady umřely.
357
+
358
+ Volající, který podle nich jedná, je `@onlineapps/service-wrapper`: FÁZE 0.3
359
+ ukončí boot natrvalo právě podle `error.code`, a od d.532 to dělá i FÁZE 0.7.
360
+ Bez značky viděl neklasifikované selhání a odmítnutí ohlásil jako přechodné —
361
+ změřeno na biz-meta (dev, wrapper 8.1.1): FÁZE 0.7 FAILED pro 406 na
362
+ `biz-meta.registry`, hlášeno jako „a restart against a recovered dependency can
363
+ succeed", a služba pak v crash loopu.
364
+
365
+ `code` i `reason` se teď přenášejí **bezpodmínečně** — táž kolej, jakou drží
366
+ `@onlineapps/conn-infra-mq` v `ConnectorMQClient` (d.498b). Nic se tu
367
+ neklasifikuje: co zdroj neoznačil, zůstává neoznačené, takže se do balíčku
368
+ nedostane druhá kopie seznamu AMQP reply kódů ani čtení textu hlášky.
369
+
370
+ Sonda `tests/unit/error-message-contract.test.js` navíc umí tvar
371
+ `const <jméno> = new …Error(…); … throw <jméno>;` — chyba, která nese vlastní
372
+ vlastnosti, se jinak postavit nedá. Bez toho by obě hlášky tiše vypadly z jejího
373
+ dosahu (změřeno: kontrolní počet spadl 28 → 26), což je přesně ta falešná záruka,
374
+ kvůli které kontrolní případ existuje (`automation-gates.md` §5).
375
+
376
+ ### Breaking — `startHeartbeat({ maxFailures, onFatal })`: smyčka heartbeatu má mez selhání (d.496)
377
+
378
+ Do téhle dávky `startHeartbeat()` selhání pouze logovala a plánovala další beat
379
+ donekonečna (`src/registryClient.js`, větev `heartbeatLoop`/`catch`). Služba, jejíž
380
+ beaty registr nedostával, tedy běžela dál a nikdo v tomhle balíčku neuměl říct, že
381
+ je „živá" jen naoko. Mez měl jen `@onlineapps/service-wrapper`, ve **vlastní druhé
382
+ smyčce** kolem `sendHeartbeat()` — dvě smyčky nad jednou kadencí, což
383
+ `.claude/rules/change-discipline.md` § One rail per concern označuje za vadu
384
+ a co si CHANGELOG wrapperu sám poznamenal do „Co tahle dávka NEudělala"
385
+ (*„Ta metoda selhání jen loguje — nemá počítadlo ani mez"*).
386
+
387
+ `startHeartbeat()` nově **vyžaduje objekt se dvěma volbami** a ani jednu si
388
+ nedosazuje (`architecture-principles` §3 a §4; chybějící nebo nesmyslná hodnota je
389
+ výjimka §5 dřív, než vznikne timer):
390
+
391
+ - `maxFailures` — kladné celé číslo beatů **v řadě**, které služba smí minout.
392
+ Jeden úspěch počítadlo nuluje.
393
+ - `onFatal(err)` — zavolá se **právě jednou**, ve chvíli, kdy je rozpočet vyčerpán.
394
+ Dostane chybu, která jmenuje počet, poslední příčinu (i jako `cause`) a Fix.
395
+ Smyčka je v tu chvíli už zastavená, takže do zavírající se služby nepřiletí
396
+ další beat.
397
+
398
+ **Kadence parametrem metody NENÍ** a záměrně: má jednoho vlastníka, a tím je
399
+ volba konstruktoru `heartbeatInterval` (rozhodnutí vlastníka
400
+ `api/docs/governance/confirmations/biz-health-freshness.md` 001 bod 3). Přijímat
401
+ ji ještě jednou při volání by bylo přesně to druhé nezávisle nastavené číslo pro
402
+ jednu věc, které to potvrzení odmítá.
403
+
404
+ Co s tím musí udělat volající: doplnit obě volby. Volání bez nich je od téhle
405
+ verze výjimka, ne tichý default. `@onlineapps/service-wrapper` pak svou vlastní
406
+ smyčku ruší ve prospěch téhle — samostatná dávka, po pinu.
407
+
408
+ Dvě menší změny chování ve stejné smyčce:
409
+
410
+ - **První (okamžitý) beat se počítá do téhož rozpočtu.** Dřív měl vlastní
411
+ `catch` a vlastní hlášku `[RegistryClient] Initial heartbeat failed`; ta
412
+ zaniká. Beat započtený mimo rozpočet by z `maxFailures` udělal „N + 1 pro
413
+ první selhání" — číslo, které volající nenastavil.
414
+ - **Každé selhání se loguje s počtem, kterého dosáhlo, a s rozpočtem, proti
415
+ kterému se počítá** (`consecutiveFailures`, `maxFailures` v meta), takže log
416
+ říká, jak blízko ke konci smyčka je, ne jen že jeden beat neprošel.
417
+
418
+ Testy: nová sada `tests/unit/registryClient.heartbeatBudget.test.js` (13 případů —
419
+ odmítnutí obou voleb hodnotou, vyčerpaný rozpočet, a kontrolní případy, které se
420
+ změnit nesmí: `maxFailures − 1` selhání a úspěch, zdravá smyčka bez `onFatal`,
421
+ idempotentní `stopHeartbeat`) a `tests/integration/heartbeatBudget.integration.test.js`
422
+ (3 případy proti **živému** RabbitMQ: beaty na drátě, skutečné odmítnutí po ztrátě
423
+ spojení, a reset počítadla beatem, který prošel). Selhání se v obou tierech vyrábí
424
+ skutečnou cestou kódu — nikdy atrapou `sendHeartbeat`, která by dokázala jen to, že
425
+ počítadlo počítá volání.
426
+
427
+ Kontrolní počet chyb v `tests/unit/error-message-contract.test.js` je 28 (bylo 25):
428
+ tři nové jsou odmítnutí voleb. Chyba předaná do `onFatal` mezi nimi **není** a hledat
429
+ se tam nemá — nevzniká `throw` ani `reject`, takže ji ta sonda nevidí; její znění
430
+ tvrdí hodnotou nová sada výše.
431
+
432
+ `README.md`, `docs/REGISTRY_CLIENT_GUIDE.md` a oba příklady učí novou signaturu;
433
+ `API.md` je přegenerované (`npm run docs`).
434
+
435
+ ### Fixed
436
+
437
+ - `API.md` regenerated from the sources (`npm run docs`). The committed file still
438
+ described the connector as asserting `workflow` and `<serviceName>.registry` on
439
+ startup and as handing out its channel for further operations — a surface replaced
440
+ when the connector moved onto a single `@onlineapps/mq-client-core` client, declared
441
+ its queues through `queueConfig` and stopped asserting infrastructure queues it does
442
+ not own. 167 lines changed, generated content only: the file is written by `jsdoc2md`,
443
+ never by hand (`.claude/rules/doc-code-binding.md` §1). What now keeps it true is
444
+ `scripts/ci/verify-generated-api-docs.mjs` (`npm run docs:api:check`).
445
+
446
+ ## [4.0.3] — 2026-09-15
447
+
448
+ ## [4.0.2] — 2026-09-15
449
+
450
+ ### Fixed — `redactUrl` se bere z `@onlineapps/logger-contract`, ne přes `mq-client-core` (d.455)
451
+
452
+ `src/queueManager.js` si redaktor bral z `@onlineapps/mq-client-core` (d.446b),
453
+ protože tehdy jediný deklaroval platformní sémantiku. Od
454
+ `@onlineapps/logger-contract` 1.2.0 (d.453) ji vlastní core vrstva, kterou
455
+ pinuje každá kategorie, takže import míří tam — vedle `assertLogger`, které si
456
+ tenhle soubor z téhož balíčku bere už dřív. Chování se nemění: pro `amqp` /
457
+ `amqps` vrací obě implementace shodný řetězec (změřeno).
458
+
459
+ Tady se nic nemaže — smazaná deklarace byla `src/redactUrl.js` a odešla už
460
+ v d.446b. Mění se jen to, **odkud** se tatáž funkce bere; `mq-client-core`
461
+ zůstává závislostí kvůli `BaseClient` a `queueConfig`.
462
+
463
+ Sady `queueManager.test.js`, `queueManager.spec.test.js`
464
+ a `queueManager.credentialRedaction.test.js`: dvojník `@onlineapps/mq-client-core`
465
+ už **záměrně nenabízí** `redactUrl` ani `UNPARSEABLE_PLACEHOLDER` — kdyby je
466
+ nabízel, sada by prošla i pro import ze špatného balíčku (to byla červená, která
467
+ tuhle změnu vynutila: `TypeError: redactUrl is not a function`,
468
+ `src/queueManager.js:69`). Přibyl kontrolní případ, že dvojník redaktor nemá,
469
+ a cesta selhání (neparsovatelná adresa → placeholder).
470
+
471
+ ## [4.0.1] — 2026-09-14
472
+
473
+ ## [4.0.0] — 2026-09-14
474
+
475
+ ### Breaking
476
+
477
+ #### Delivery of a registry message belongs to the core, not to this package (d.198b-3)
478
+
479
+ Both consumers here held a RAW amqplib channel (`queueManager.channel`): they
480
+ asserted their own queue, called `channel.consume(…, { noAck: false })` and settled
481
+ every delivery by hand — `channel.ack(msg)` on success,
482
+ `channel.nack(msg, false, false)` on failure. That is a second implementation of the
483
+ delivery contract (`.claude/rules/change-discipline.md` § One rail per concern), and
484
+ it is the half that cannot count: a hand-written nack knows nothing about the attempt
485
+ budget, the `x-oa-delivery-attempts` header or the `message_dlq` event
486
+ `@onlineapps/mq-client-core` publishes. Worse, `nack(requeue=false)` asks the broker
487
+ to DROP the message — and it did, because `<service>.registry` declared no
488
+ dead-letter route at all. A registry reply this service could not read simply ceased
489
+ to exist.
490
+
491
+ Both now run on `consume(queue, handler, { requeueOnError: false })`: one attempt,
492
+ and a failure is rejected into `<service>.dlq` over `dlx`, the route `queueConfig`
493
+ declares for these queues. A handler reports failure by THROWING and settles nothing
494
+ itself. Measured on the live broker (`tests/integration/deliveryPolicy.integration`):
495
+ the poison message arrives in the DLQ verbatim with `x-death[0].reason: 'rejected'`,
496
+ the handler ran exactly once, and the control case leaves both queues empty.
497
+
498
+ `QueueManager` is now the package's single MQ surface, built on
499
+ `@onlineapps/mq-client-core`: `init/isConnected/ensureQueues/checkQueue/
500
+ assertExchange/bindQueue/publish/consume/close`. **It hands out no `channel` any
501
+ more** — a caller reaching for `client.queueManager.channel` gets `undefined`.
502
+ Queue names are this package's; the DECLARATION behind them (durability, TTL, cap,
503
+ dead-letter route) is `queueConfig`'s, so `<service>.registry` and
504
+ `<service>.registry.events` are asserted with no options of our own. The 406 branch
505
+ in `subscribeToChanges()` — "the queue exists with different arguments, use it
506
+ as-is" — went with them: that is the fallback `architecture-principles.md` §3
507
+ forbids, and the reason a queue keeps whatever arguments its first declarer happened
508
+ to pass. `register()` publishes through the core (`persistent: true`), so a failed
509
+ publish THROWS instead of returning `false`, and the infrastructure-queue check runs
510
+ on the core's queue channel rather than a private one.
511
+
512
+ #### `heartbeatInterval` and `specificationEndpoint` are REQUIRED (d.278c)
513
+
514
+ Both were module defaults (`10000`, `'/api/v1/specification'`) and both were
515
+ statements this package had no standing to make.
516
+
517
+ The heartbeat cadence has ONE owner — `wrapper.registry.heartbeatInterval`, fed from
518
+ `BIZ_HEARTBEAT_INTERVAL_MS` (owner confirmation `biz-health-freshness` 001 point 3),
519
+ and the freshness window monitoring computes is a multiple of it. A library default
520
+ beside it is a second cadence that fires whenever the caller forgets the first. The
521
+ specification path is REQUIRED on the wire (`registration-wire.md` §2.1) and the
522
+ registry stores what it is sent, so a substituted path lands in `registry:services`
523
+ for a service that never declared it. Both are refused at construction, by value —
524
+ a string `'10000'` is not parsed into a cadence — naming the fix.
525
+
526
+ **Callers must now pass both.** `@onlineapps/service-wrapper` passes
527
+ `specificationEndpoint` already and does NOT pass `heartbeatInterval` (it runs its
528
+ own heartbeat loop from `wrapper.registry.heartbeatInterval` and calls
529
+ `sendHeartbeat()`); that value has to reach the constructor.
530
+
531
+ #### `register()` and `deregister()` return what actually happened (d.278c)
532
+
533
+ `register()` read the registry's reply and dropped two of its fields on the floor:
534
+ `validationStatus` and `instructions` (`registration-wire.md` §2.5 — the parked
535
+ registration, "you have not pre-validated yet" plus what to run about it). A caller
536
+ saw `success: false` and nothing that explained it. Both are now passed through
537
+ verbatim, like `errorCode`: a confirmed registration sends neither and both stay
538
+ `undefined` — nothing is substituted.
539
+
540
+ `deregister()` answered a hardcoded `{ success: true, message: 'Service deregistered
541
+ successfully' }` whatever happened. The registry sends NO reply to a `deregister`
542
+ (`registration-wire.md` §2.3), so there was no success to report — a claim about the
543
+ registry made by the one party that never heard from it. It now returns what this
544
+ call knows: `{ requestId, serviceName, version, publishedTo, heartbeatStopped,
545
+ publishedAt }`. A publish that fails REJECTS; reaching the return means the broker
546
+ took the message. There is no `success` key.
547
+
548
+ ### Fixed
549
+
550
+ #### The integration tier connects with a credential (d.446b)
551
+
552
+ `tests/config.js` composed the AMQP URI as `amqp://${host}:${port}` from
553
+ `RABBITMQ_HOST` + `RABBITMQ_PORT` — a URI with no account, which the broker answers
554
+ `403 ACCESS-REFUSED` to. 21 of the tier's 24 tests failed on it, on every run, for as
555
+ long as the broker has required authentication. The tier now takes `RABBITMQ_URL`
556
+ whole (credential included) and DERIVES host and port from it, so the address it
557
+ probes in `integration/setup.js` is the address it then connects to. The banner it
558
+ prints names host and port only.
559
+
560
+ Two more reds went with them, both stale rather than broken: `readRegistrySpec()`
561
+ asked `GET /services/<name>/spec`, a route `api_services_registry` stopped mounting
562
+ when the `/services` alias went in batch 263c — so `status === 404` passed for the
563
+ WRONG reason (a missing route, not a missing record) while the body assertion failed.
564
+ It reads `/microservices/<name>/spec` now, the route `src/app.js` mounts. And the
565
+ "nobody consumes the registry queue" failure path pointed at a queue that did not
566
+ exist; since the publish path asks the broker whether the target exists, that is now
567
+ a different (and faster) failure, so the harness declares the queue and leaves it
568
+ unconsumed — the situation the timeout exists for.
569
+
570
+
571
+ #### AMQP URL v hlášce i logu bez credentialu (d.446)
572
+
573
+ `AMQP_URL` je jediná kolej pro heslo k brokeru, takže obě chybové cesty
574
+ `QueueManager.init()` — timeout spojení i selhání `amqp.connect()` — vypisovaly to
575
+ heslo doslova: URL šla do hlášky nezredigovaná. Chyba konektoru jde rovnou do logu
576
+ služby a odtud do Loki, kde je trvalá a vyhledatelná. Táž třída úniku, kterou pro
577
+ Redis zavřel `shared/service-common/src/redisClient.js`.
578
+
579
+ Nově jde URL do hlášky přes `src/redactUrl.js`: userinfo (`uživatel:heslo@`) pryč,
580
+ schéma, host, port i vhost zůstávají — to je, co čtenář logu potřebuje. Hodnota,
581
+ kterou nelze přečíst jako URL, se nevypisuje vůbec, nahradí ji `<unparseable-url>`
582
+ (neparsovatelný řetězec je právě ten případ, kdy nikdo neumí říct, jestli
583
+ credential nese). Připojení samo dál používá původní URL — redakce se týká výhradně
584
+ renderu do textu.
585
+
586
+ Helper byl zprvu lokální (`src/redactUrl.js`): pinované závislosti balíčku tehdy
587
+ žádnou redakci nevystavovaly. **Od d.446b je pryč** — `@onlineapps/mq-client-core`
588
+ 3.0.0 exportuje `redactUrl` i `UNPARSEABLE_PLACEHOLDER` s totožnou sémantikou
589
+ (d.448, celé userinfo pryč), takže lokální kopie byla druhá kolej pro jeden koncept
590
+ (`change-discipline.md` § One rail per concern). `QueueManager` ho importuje z jádra;
591
+ identitu toho exportu drží test, a `tests/unit/redactUrl.test.js` (chování lokální
592
+ kopie) odešel s ní.
593
+
594
+ ### Konstruktor klienta už nenese `registryUrl` (d.244)
595
+
596
+ Parametr `registryUrl` i pole `this.registryUrl` zrušeny. Od konfirmace
597
+ `biz-discovery-redis` 001 a commitu `725b25c2` se jen ukládaly a nikdo je nečetl
598
+ (v `src/` žádný `axios`/`fetch`/`http.request`). Uložený nečtený parametr není
599
+ neškodný: říkal každému volajícímu, že na té adrese něco závisí — a to drželo
600
+ `REGISTRY_URL` jako povinný klíč v osmi biz konfiguracích. Konfirmace
601
+ `biz-discovery-redis` 002.
602
+
603
+ Volající, který `registryUrl` stále předá, ho dostane ignorovaný, ne uložený.
604
+ JSDoc konstruktoru ho nikdy nepopisoval (změřeno), takže se tam nic nemazalo.
605
+ Integrační tier balíčku `REGISTRY_URL` dál vyžaduje — čte jím HTTP API registru,
606
+ aby ověřil, co registr uložil; to je testovací harness, ne klient.
607
+
608
+ ### `register()` nese `deployable` na drát (d.221)
609
+
610
+ `register()` staví zprávu z pevného seznamu klíčů, takže klíč, který nejmenuje,
611
+ se na drát nedostane — tiše. Nově jmenuje `deployable`: verdikt shody s manifestem
612
+ (krok 7/7 validace), který registry promítá do `infrastructure:health:<service>`
613
+ vedle `status` (potvrzení vlastníka
614
+ `api/docs/governance/confirmations/biz-service-manifest.md` 001 §4). Přenáší se
615
+ doslova ve všech třech stavech — `true`, `false`, `null` (běh konformitu nezměřil) —
616
+ proto test přítomnosti klíče, ne pravdivosti hodnoty. Volající, který verdikt
617
+ neposílá, nemá klíč ve zprávě; nic se za něj nedosazuje.
618
+
619
+ ## [3.0.1] — 2026-09-04
620
+
621
+ - Changelog started with this release; the history before it is in git (`git log -- shared/connector/conn-orch-registry`).