@onlineapps/mq-client-core 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.
@@ -1,18 +1,207 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * The platform's dead-letter exchange.
5
+ *
6
+ * A name three services must agree on has to have ONE owner, or it has as many
7
+ * as the places that type it: this file typed the name four times, and
8
+ * `@onlineapps/conn-infra-mq` reads it back out of a queue's arguments
9
+ * (`getBusinessQueueConfig('queue')['x-dead-letter-exchange']`) because there was
10
+ * nothing to import. Same reasoning as `src/index.js` § topology, which gave the
11
+ * queue names that travel on the wire one declaration each.
12
+ *
13
+ * Exported as `deadLetterExchange()` — a getter, not the bare constant, so an
14
+ * importer cannot rebind platform topology.
15
+ */
16
+ const DEAD_LETTER_EXCHANGE = 'dlx';
17
+
3
18
  /**
4
19
  * queueConfig.js
5
20
  *
6
- * Central configuration for INFRASTRUCTURE queues only.
7
- * Business queues ({service}.workflow, {service}.queue, {service}.dlq) are created
8
- * by individual services via QueueManager.setupServiceQueues().
21
+ * Central configuration for INFRASTRUCTURE queues, and for the TEMPLATES of the
22
+ * queues a business service owns. `{service}.workflow`, `{service}.queue` and
23
+ * `{service}.dlq` are created by the service itself via
24
+ * QueueManager.setupServiceQueues(); the registry-client templates are declared here
25
+ * for the same reason — one declaration per queue, which is what the dead-letter gate
26
+ * of consume() reads.
9
27
  *
10
28
  * This ensures consistent configuration for infrastructure queues across all services.
11
29
  *
12
30
  * NOTE: This is part of mq-client-core (spodní vrstva) because it's used by both:
13
31
  * - Infrastructure services (via mq-client-core)
14
32
  * - Business services (via conn-infra-mq connector)
33
+ *
34
+ * Every lookup below fails fast with a `ValidationError`: an unknown queue name is a
35
+ * caller mistake, not a runtime condition, and the package has one error rail
36
+ * (`utils/errorHandler.js`) rather than a mix of typed and bare errors.
37
+ */
38
+
39
+ const { ValidationError } = require('../utils/errorHandler');
40
+
41
+ /**
42
+ * The prefix each section's keys carry ON THE WIRE.
43
+ *
44
+ * A section name is NOT automatically the prefix of the names it declares, and
45
+ * assuming it was is the defect d.267 fixed: `deliveryEvents` declares three
46
+ * `delivery.` queues, so a lookup that stripped `delivery.` and read only the
47
+ * `delivery` section answered "not defined" for every one of them. The mapping is
48
+ * stated once, here, and every lookup and every message derives from it rather
49
+ * than repeating a list by hand (`single-source-of-truth.md`).
50
+ *
51
+ * `business` is absent on purpose: it holds TEMPLATES (`{service}.workflow`), not
52
+ * full names, so no prefix composes a wire name from it.
53
+ */
54
+ const SECTION_PREFIXES = Object.freeze({
55
+ workflow: 'workflow',
56
+ delivery: 'delivery',
57
+ deliveryEvents: 'delivery',
58
+ validation: 'validation',
59
+ infrastructure: 'infrastructure',
60
+ monitoring: 'monitoring',
61
+ telemetry: 'telemetry',
62
+ alerts: 'alert',
63
+ registry: 'registry'
64
+ });
65
+
66
+ /**
67
+ * Every infrastructure prefix and the lookup that answers for it — ONE map, read by
68
+ * the classifier, by the dispatch and by the refusal message alike.
69
+ *
70
+ * It is a map rather than three lists because of d.269: `isInfrastructureQueue()`
71
+ * accepted `telemetry.` while `getInfrastructureQueueConfig()` had no branch for it,
72
+ * so a telemetry name was classified as infrastructure and then refused as not being
73
+ * one — and the refusal listed four prefixes while the classifier knew seven, because
74
+ * the sentence was typed by hand. A prefix the classifier knows but nothing can look
75
+ * up is not expressible here (`single-source-of-truth.md`).
76
+ */
77
+ const INFRASTRUCTURE_PREFIX_LOOKUPS = Object.freeze({
78
+ workflow: 'getWorkflowQueueConfig',
79
+ registry: 'getRegistryQueueConfig',
80
+ infrastructure: 'getInfrastructureHealthQueueConfig',
81
+ validation: 'getValidationQueueConfig',
82
+ monitoring: 'getMonitoringQueueConfig',
83
+ telemetry: 'getTelemetryQueueConfig',
84
+ delivery: 'getDeliveryQueueConfig'
85
+ });
86
+
87
+ const INFRASTRUCTURE_PREFIXES = Object.freeze(Object.keys(INFRASTRUCTURE_PREFIX_LOOKUPS));
88
+
89
+ /** Every section declaring names under one prefix — derived, never a second list. */
90
+ const sectionsForPrefix = (prefix) =>
91
+ Object.keys(SECTION_PREFIXES).filter((section) => SECTION_PREFIXES[section] === prefix);
92
+
93
+ const DELIVERY_SECTIONS = sectionsForPrefix('delivery');
94
+
95
+ /** A `{placeholder}` segment of a business template key: matches any ONE name part. */
96
+ const TEMPLATE_PLACEHOLDER = /^\{[^.{}]+\}$/;
97
+
98
+ /**
99
+ * One business template key as a SHAPE: the parts a queue name must carry after the
100
+ * service name, with `null` where the template accepts anything (`registry.events`
101
+ * -> `['registry', 'events']`; a key writing `registry.{kind}` would give
102
+ * `['registry', null]`).
103
+ */
104
+ const templateShape = (template) =>
105
+ template.split('.').map((part) => (TEMPLATE_PLACEHOLDER.test(part) ? null : part));
106
+
107
+ /**
108
+ * Match a queue name against the templates the `business` section declares.
109
+ *
110
+ * The section IS the list — d.278 removed the hand-typed `['workflow', 'queue',
111
+ * 'dlq']`, which had to be edited in step with the templates and therefore could
112
+ * disagree with them. A template nobody can express here is a queue nobody can
113
+ * classify, which is the state `{service}.registry` was in: declared nowhere,
114
+ * classified as neither kind, and so refused by the dead-letter gate of consume().
115
+ *
116
+ * @param {Object} business - The `business` section (templates keyed by their shape).
117
+ * @param {string} queueName - Full queue name.
118
+ * @returns {{serviceName: string, queueType: string}|null} The service and the
119
+ * template key that declares the queue's arguments, or `null` for a name no
120
+ * template matches.
121
+ * @throws {ValidationError} If two templates match one name - a defect in this file,
122
+ * never a runtime condition, and one that would otherwise be resolved by key order.
15
123
  */
124
+ const matchBusinessTemplate = (business, queueName) => {
125
+ const parts = queueName.split('.');
126
+ if (parts.length < 2 || parts[0] === '') return null;
127
+
128
+ const tail = parts.slice(1);
129
+ const matched = Object.keys(business).filter((template) => {
130
+ const shape = templateShape(template);
131
+ return (
132
+ shape.length === tail.length
133
+ && shape.every((part, index) => (part === null ? tail[index] !== '' : part === tail[index]))
134
+ );
135
+ });
136
+
137
+ if (matched.length > 1) {
138
+ throw new ValidationError(
139
+ `[queueConfig] Business queue ${queueName} matches more than one template: ${matched.join(', ')} - `
140
+ + 'Expected: exactly one template declares a name, so one queue has one definition. '
141
+ + 'Fix: make the overlapping keys in the `business` section of src/config/queueConfig.js distinct.'
142
+ );
143
+ }
144
+
145
+ return matched.length === 1 ? { serviceName: parts[0], queueType: matched[0] } : null;
146
+ };
147
+
148
+ /**
149
+ * Every full wire name the infrastructure sections DECLARE.
150
+ *
151
+ * Derived from the sections themselves — `SECTION_PREFIXES` says which prefix each
152
+ * one puts on the wire, and only sections whose prefix has a lookup take part, so a
153
+ * name this set contains can always be resolved (the invariant d.269 established).
154
+ * `alerts` is therefore absent, exactly as it is today: nothing looks up `alert.`.
155
+ *
156
+ * @param {Object} config - The queueConfig object (its sections are read from `this`).
157
+ * @returns {Set<string>} The declared names, e.g. `registry.register`, `workflow.dlq`.
158
+ */
159
+ const declaredInfrastructureNames = (config) => {
160
+ const names = new Set();
161
+
162
+ for (const [section, prefix] of Object.entries(SECTION_PREFIXES)) {
163
+ if (!Object.prototype.hasOwnProperty.call(INFRASTRUCTURE_PREFIX_LOOKUPS, prefix)) continue;
164
+ for (const key of Object.keys(config[section] || {})) {
165
+ names.add(`${prefix}.${key}`);
166
+ }
167
+ }
168
+
169
+ return names;
170
+ };
171
+
172
+ /**
173
+ * The two ways out of "this configuration declares no such queue", named in the
174
+ * refusal itself.
175
+ *
176
+ * A name like `registry.foo` can be two different mistakes, and the message cannot
177
+ * tell which: an infrastructure queue nobody declared, or a queue owned by a service
178
+ * named `registry` written in a shape no template matches. Naming only the first sends
179
+ * the reader to add a platform queue for something a service owns — the very confusion
180
+ * d.279 came from. The service-owned alternatives are rendered from the `business`
181
+ * templates, never typed out, so the sentence cannot fall behind them.
182
+ *
183
+ * @param {Object} business - The `business` section.
184
+ * @param {string} queueName - The full name that was not found.
185
+ * @returns {string} The `Fix:` sentence (`architecture-principles.md` §5).
186
+ */
187
+ const waysOut = (business, queueName) => {
188
+ const parts = queueName.split('.');
189
+ const owner = parts[0];
190
+ const declare = 'Fix: declare the entry in src/config/queueConfig.js if the platform owns the queue';
191
+
192
+ // Only the templates a name of THIS shape could take: offering `registry.registry.events`
193
+ // as the fix for `registry.foo` would be a suggestion that cannot be followed.
194
+ const owned = Object.keys(business)
195
+ .filter((template) => template.split('.').length === parts.length - 1)
196
+ .map((template) => `${owner}.${template}`);
197
+
198
+ if (owned.length === 0) {
199
+ return `${declare}; no queue a service owns has this shape, so the name is not a service-owned one either.`;
200
+ }
201
+
202
+ return `${declare}, or — if the service named "${owner}" owns it — use one of the names that service's own `
203
+ + `queues take (${owned.join(', ')}), which this configuration already classifies.`;
204
+ };
16
205
 
17
206
  module.exports = {
18
207
  /**
@@ -65,23 +254,67 @@ module.exports = {
65
254
 
66
255
  /**
67
256
  * workflow.completed - Completed workflows
257
+ *
258
+ * Dead-letters to workflow.dlq over the DEFAULT exchange, where the routing key
259
+ * is the destination queue name — the shape workflow.init/workflow.control
260
+ * already use. The dispatcher rejects a message here on an invalid payload, a
261
+ * schema violation and a delivery failure; until d.278 the broker dropped each
262
+ * one, because the queue declared nowhere to put it.
263
+ *
264
+ * @see api/docs/governance/confirmations/mq-consumer-contract.md 003
68
265
  */
69
266
  completed: {
70
267
  durable: true,
71
268
  arguments: {
72
269
  'x-message-ttl': 300000, // 5 minutes TTL
73
- 'x-max-length': 10000
270
+ 'x-max-length': 10000,
271
+ 'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
272
+ 'x-dead-letter-routing-key': 'workflow.dlq'
273
+ }
274
+ },
275
+
276
+ /**
277
+ * workflow.delivery_requested - a run handed over to the Delivery Dispatcher.
278
+ *
279
+ * The hand-over is NOT the end of a run. A cookbook declaring `delivery.on_result`
280
+ * has one step left when the document is ready to leave the platform: the step that
281
+ * records how the delivery went. Publishing that hand-over on `workflow.completed`
282
+ * made the terminal message mean "not finished yet", and left the real end of such a
283
+ * run without a message at all. The owner gave the hand-over a name of its own:
284
+ * `workflow.delivery_requested`, carrying `status: "delivery_requested"`, while
285
+ * `workflow.completed` stays the single terminal message.
286
+ *
287
+ * Same arguments as `workflow.completed` — a hand-over is the same payload with a
288
+ * different verb, read by the same consumer, so it expires and caps the same way —
289
+ * and the same dead-letter destination: the mechanical rule of confirmation
290
+ * `mq-consumer-contract` 003 routes every queue of this family to `workflow.dlq`
291
+ * over the DEFAULT exchange, where the routing key IS the destination queue's name.
292
+ *
293
+ * @see api/docs/governance/confirmations/delivery-receipt-chain.md 002, 003
294
+ * @see api/docs/governance/confirmations/mq-consumer-contract.md 003
295
+ */
296
+ delivery_requested: {
297
+ durable: true,
298
+ arguments: {
299
+ 'x-message-ttl': 300000, // 5 minutes TTL, as workflow.completed
300
+ 'x-max-length': 10000,
301
+ 'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
302
+ 'x-dead-letter-routing-key': 'workflow.dlq'
74
303
  }
75
304
  },
76
305
 
77
306
  /**
78
307
  * workflow.failed - Failed workflows (DLQ entry point)
79
- * Workflows land here after max retries exhausted, waiting for human decision
308
+ * Workflows land here after max retries exhausted, waiting for human decision.
309
+ *
310
+ * No TTL: the queue is read by an operator, not by a service, and it has no
311
+ * dead-letter exchange — an expiring message is a deleted message.
312
+ *
313
+ * @see api/docs/governance/confirmations/mq-consumer-contract.md 002
80
314
  */
81
315
  failed: {
82
316
  durable: true,
83
317
  arguments: {
84
- 'x-message-ttl': 300000, // 5 minutes TTL
85
318
  'x-max-length': 10000
86
319
  }
87
320
  },
@@ -89,17 +322,28 @@ module.exports = {
89
322
  /**
90
323
  * workflow.discarded - Discarded workflows (final state)
91
324
  * Workflows land here after human decision to discard from DLQ
325
+ *
326
+ * Dead-letters to workflow.dlq, like every other queue of this family
327
+ * (confirmation mq-consumer-contract 003). The operator's own queues —
328
+ * workflow.failed and workflow.dlq — are the two that keep no onward route.
92
329
  */
93
330
  discarded: {
94
331
  durable: true,
95
332
  arguments: {
96
333
  'x-message-ttl': 300000, // 5 minutes TTL
97
- 'x-max-length': 10000
334
+ 'x-max-length': 10000,
335
+ 'x-dead-letter-exchange': '',
336
+ 'x-dead-letter-routing-key': 'workflow.dlq'
98
337
  }
99
338
  },
100
339
 
101
340
  /**
102
341
  * workflow.dlq - Dead letter queue for workflows
342
+ *
343
+ * The end of the road for this family: read by an operator through the DLQ
344
+ * dashboard contract, never forwarded onward. No TTL and no dead-letter route,
345
+ * for the same reason workflow.failed has neither — an expiring or forwarded
346
+ * message is one the operator never gets to decide about.
103
347
  */
104
348
  dlq: {
105
349
  durable: true,
@@ -111,9 +355,22 @@ module.exports = {
111
355
  },
112
356
 
113
357
  /**
114
- * Business queue templates
115
- * These are templates for service-specific queues ({service}.workflow, {service}.queue, {service}.dlq)
116
- * Used by QueueManager.setupServiceQueues() to ensure consistent configuration
358
+ * Business queue templates - every queue a business service OWNS.
359
+ *
360
+ * A key here is the queue name's tail after the service name, and it is read as a
361
+ * PATTERN: a `{placeholder}` segment matches any one part. That is what makes
362
+ * `isBusinessQueue()`/`parseBusinessQueue()` derive from this section instead of
363
+ * repeating a list of types by hand, and it is the shape
364
+ * docs/standards/queue-ownership.md prescribes - "infrastructure knows them by
365
+ * that pattern, never by a list of names".
366
+ *
367
+ * Every one of them dead-letters over `dlx` to `{service}.dlq`, the queue each
368
+ * service already has and the one binding d.198a left behind
369
+ * (api/docs/governance/confirmations/mq-consumer-contract.md 003).
370
+ *
371
+ * `{service}.workflow`, `{service}.queue` and `{service}.dlq` are created by
372
+ * QueueManager.setupServiceQueues(); the registry-client queues by
373
+ * @onlineapps/conn-orch-registry.
117
374
  */
118
375
  business: {
119
376
  /**
@@ -125,7 +382,7 @@ module.exports = {
125
382
  arguments: {
126
383
  'x-message-ttl': 300000, // 5 minutes TTL
127
384
  'x-max-length': 10000, // Max 10k messages
128
- 'x-dead-letter-exchange': 'dlx',
385
+ 'x-dead-letter-exchange': DEAD_LETTER_EXCHANGE,
129
386
  'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
130
387
  }
131
388
  },
@@ -139,7 +396,7 @@ module.exports = {
139
396
  arguments: {
140
397
  'x-message-ttl': 30000, // 30 seconds TTL
141
398
  'x-max-length': 10000, // Max 10k messages
142
- 'x-dead-letter-exchange': 'dlx',
399
+ 'x-dead-letter-exchange': DEAD_LETTER_EXCHANGE,
143
400
  'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
144
401
  }
145
402
  },
@@ -154,6 +411,41 @@ module.exports = {
154
411
  // No TTL for DLQ - messages should persist
155
412
  'x-max-length': 50000 // Higher limit for DLQ
156
413
  }
414
+ },
415
+
416
+ /**
417
+ * {service}.registry - The service's own queue for registry control messages
418
+ * (register.confirmed and the rest), asserted by @onlineapps/conn-orch-registry.
419
+ *
420
+ * No TTL and no length cap: that is what the queues carry on the broker today
421
+ * (measured 2026-09-12, every `<service>.registry` reports `arguments = {}`), and
422
+ * a value invented here would be a second truth about a running queue. What the
423
+ * template adds is the dead-letter route the queue never had.
424
+ */
425
+ registry: {
426
+ durable: true,
427
+ arguments: {
428
+ 'x-dead-letter-exchange': DEAD_LETTER_EXCHANGE,
429
+ 'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
430
+ }
431
+ },
432
+
433
+ /**
434
+ * {service}.registry.events - The service's copy of the registry event fanout,
435
+ * bound to the `registry.changes` exchange by @onlineapps/conn-orch-registry.
436
+ *
437
+ * TTL and cap are the ones that connector asserts today, kept to the value, so
438
+ * moving it onto this template cannot produce a 406 on a queue that already
439
+ * exists with them.
440
+ */
441
+ 'registry.events': {
442
+ durable: true,
443
+ arguments: {
444
+ 'x-message-ttl': 60000, // 1 minute TTL - a stale index event is useless
445
+ 'x-max-length': 1000,
446
+ 'x-dead-letter-exchange': DEAD_LETTER_EXCHANGE,
447
+ 'x-dead-letter-routing-key': '{service}.dlq'
448
+ }
157
449
  }
158
450
  },
159
451
 
@@ -169,7 +461,9 @@ module.exports = {
169
461
  retry: {
170
462
  durable: true,
171
463
  arguments: {
172
- 'x-max-length': 10000 // Allow up to 10k pending retry messages
464
+ 'x-max-length': 10000, // Allow up to 10k pending retry messages
465
+ 'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
466
+ 'x-dead-letter-routing-key': 'delivery.dlq'
173
467
  }
174
468
  },
175
469
 
@@ -188,7 +482,28 @@ module.exports = {
188
482
  durable: true,
189
483
  arguments: {
190
484
  'x-message-ttl': 60000, // Notifications older than 60s are irrelevant
191
- 'x-max-length': 10000
485
+ 'x-max-length': 10000,
486
+ 'x-dead-letter-exchange': '',
487
+ 'x-dead-letter-routing-key': 'delivery.dlq'
488
+ }
489
+ },
490
+
491
+ /**
492
+ * delivery.dlq - Dead letter queue of the whole delivery family
493
+ *
494
+ * Every delivery queue — the dispatcher's retry and websocket queues, and the
495
+ * four event queues the endpoint consumes — rejects into this one, over the
496
+ * default exchange, so no binding has to exist for the route to work. Declared
497
+ * with the arguments every other dead-letter queue carries: durable, capped, no
498
+ * TTL, no onward route. The operator's dashboard matches `*.dlq`, which is why
499
+ * the family gets its own queue rather than sharing one with the rest of the
500
+ * infrastructure (confirmation mq-consumer-contract 003, variant A2 rejected).
501
+ */
502
+ dlq: {
503
+ durable: true,
504
+ arguments: {
505
+ // No TTL for DLQ - messages should persist
506
+ 'x-max-length': 50000 // Higher limit for DLQ
192
507
  }
193
508
  }
194
509
  },
@@ -233,6 +548,23 @@ module.exports = {
233
548
  'x-message-ttl': 10000, // 10 seconds TTL (prevent stale data)
234
549
  'x-max-length': 1000
235
550
  }
551
+ },
552
+
553
+ /**
554
+ * infrastructure.health.events - Fanout exchange for infrastructure health events
555
+ *
556
+ * Registry publishes; the monitoring consumer and the delivery endpoint each bind a
557
+ * queue of their own (`monitoring.infrastructure.health.events`,
558
+ * `delivery.health.events`). The name is a contract between THREE sides of one
559
+ * wire, which is why batch 243g-C removed the per-service env override: an override
560
+ * on one side loses the events silently. It then left the name as a private
561
+ * constant in each of the three services, so d.270 gave it an owner here and
562
+ * `src/index.js` publishes it.
563
+ */
564
+ 'health.events': {
565
+ type: 'exchange',
566
+ exchangeType: 'fanout',
567
+ durable: true
236
568
  }
237
569
  },
238
570
 
@@ -298,6 +630,56 @@ module.exports = {
298
630
  type: 'exchange',
299
631
  exchangeType: 'fanout',
300
632
  durable: true
633
+ },
634
+
635
+ /**
636
+ * monitoring.infrastructure.health.events - the monitoring consumer's own copy of
637
+ * the infrastructure health events, bound to the infrastructure.health.events
638
+ * fanout exchange. Declared with `durable` and nothing else, exactly as the
639
+ * consumer creates it.
640
+ */
641
+ 'infrastructure.health.events': {
642
+ durable: true
643
+ }
644
+ },
645
+
646
+ /**
647
+ * Telemetry queue configurations
648
+ *
649
+ * The OpenTelemetry intake of the monitoring consumer: every service publishes
650
+ * logs, metrics and spans to `telemetry.exchange` with a routing key carrying its
651
+ * own name, and these three queues are bound to it
652
+ * (`docs/standards/monitoring-queues.md`, `docs/architecture/monitoring.md`).
653
+ *
654
+ * The values are the ones the queues are created with today; this config is their
655
+ * declaration, so whoever asserts them reads it here rather than retyping it.
656
+ */
657
+ telemetry: {
658
+ /** telemetry.logs.queue - OpenTelemetry log records, drained into Loki. */
659
+ 'logs.queue': {
660
+ durable: true,
661
+ arguments: {
662
+ 'x-message-ttl': 60000, // 1 minute TTL for unprocessed messages
663
+ 'x-max-length': 100000 // Max 100k messages in queue
664
+ }
665
+ },
666
+
667
+ /** telemetry.metrics.queue - OpenTelemetry metric samples. */
668
+ 'metrics.queue': {
669
+ durable: true,
670
+ arguments: {
671
+ 'x-message-ttl': 60000,
672
+ 'x-max-length': 100000
673
+ }
674
+ },
675
+
676
+ /** telemetry.traces.queue - OpenTelemetry spans. */
677
+ 'traces.queue': {
678
+ durable: true,
679
+ arguments: {
680
+ 'x-message-ttl': 60000,
681
+ 'x-max-length': 100000
682
+ }
301
683
  }
302
684
  },
303
685
 
@@ -314,7 +696,9 @@ module.exports = {
314
696
  durable: true,
315
697
  arguments: {
316
698
  'x-message-ttl': 60000, // 60s TTL - real-time events, stale ones are useless
317
- 'x-max-length': 10000
699
+ 'x-max-length': 10000,
700
+ 'x-dead-letter-exchange': '', // Default exchange: routing key IS the queue name
701
+ 'x-dead-letter-routing-key': 'delivery.dlq'
318
702
  }
319
703
  },
320
704
 
@@ -326,7 +710,9 @@ module.exports = {
326
710
  durable: true,
327
711
  arguments: {
328
712
  'x-message-ttl': 60000,
329
- 'x-max-length': 5000
713
+ 'x-max-length': 5000,
714
+ 'x-dead-letter-exchange': '',
715
+ 'x-dead-letter-routing-key': 'delivery.dlq'
330
716
  }
331
717
  },
332
718
 
@@ -338,7 +724,33 @@ module.exports = {
338
724
  durable: true,
339
725
  arguments: {
340
726
  'x-message-ttl': 300000, // 5 minutes - alerts are more important
341
- 'x-max-length': 5000
727
+ 'x-max-length': 5000,
728
+ 'x-dead-letter-exchange': '',
729
+ 'x-dead-letter-routing-key': 'delivery.dlq'
730
+ }
731
+ },
732
+
733
+ /**
734
+ * delivery.resource.events - Resource-changed events for WS clients
735
+ * Bound to the monitoring.resource exchange by the routing pattern
736
+ * `resource.changed.#` - the one binding of this family that is not a fanout
737
+ * (docs/standards/queue-ownership.md).
738
+ *
739
+ * The queue has existed on the broker since the endpoint started asserting it
740
+ * with `{ durable: true }` and nothing else; it was the only queue of the
741
+ * family this configuration did not declare, so the d.259 gate had nothing to
742
+ * read for it. The arguments are its sibling's — same TTL, same cap — because
743
+ * it carries the same kind of real-time event.
744
+ *
745
+ * @see api/docs/governance/confirmations/mq-consumer-contract.md 003
746
+ */
747
+ 'resource.events': {
748
+ durable: true,
749
+ arguments: {
750
+ 'x-message-ttl': 60000,
751
+ 'x-max-length': 10000,
752
+ 'x-dead-letter-exchange': '',
753
+ 'x-dead-letter-routing-key': 'delivery.dlq'
342
754
  }
343
755
  }
344
756
  },
@@ -398,6 +810,42 @@ module.exports = {
398
810
  }
399
811
  },
400
812
 
813
+ /**
814
+ * The full wire name of a declared entry: the section's prefix + the entry's key.
815
+ *
816
+ * Composed here, never retyped by a caller — and never composed from the section
817
+ * NAME, because a section name is not automatically a prefix (`deliveryEvents`
818
+ * declares `delivery.` names, `alerts` declares `alert.`). The entry must exist:
819
+ * a name this configuration does not declare would be asserted against the broker
820
+ * by whoever imported it, which is how a typo becomes a second queue.
821
+ *
822
+ * @param {string} section - Section holding the entry (e.g. 'deliveryEvents').
823
+ * @param {string} key - The entry's key within that section (e.g. 'health.events').
824
+ * @returns {string} The full name (e.g. 'delivery.health.events').
825
+ * @throws {ValidationError} If the section composes no wire name, or the entry is undeclared.
826
+ */
827
+ queueName(section, key) {
828
+ const prefix = SECTION_PREFIXES[section];
829
+
830
+ if (prefix === undefined) {
831
+ throw new ValidationError(
832
+ `[queueConfig] Cannot compose a name: section "${section}" declares no wire prefix - `
833
+ + `Expected: one of ${Object.keys(SECTION_PREFIXES).join(', ')}. `
834
+ + 'Fix: name a section that declares full queue names; "business" holds templates, not names.'
835
+ );
836
+ }
837
+
838
+ if (!this[section]?.[key]) {
839
+ throw new ValidationError(
840
+ `[queueConfig] Cannot compose a name: queueConfig declares no "${key}" under "${section}" - `
841
+ + `Expected: the entry to exist before its name is published. `
842
+ + 'Fix: declare it in src/config/queueConfig.js, or use the key the owning service declares.'
843
+ );
844
+ }
845
+
846
+ return `${prefix}.${key}`;
847
+ },
848
+
401
849
  /**
402
850
  * Get infrastructure queue configuration by type and name
403
851
  * @param {string} type - Queue type: 'workflow', 'registry'
@@ -407,7 +855,11 @@ module.exports = {
407
855
  getQueueConfig(type, name) {
408
856
  const config = this[type]?.[name];
409
857
  if (!config) {
410
- throw new Error(`Infrastructure queue config not found: ${type}.${name}`);
858
+ throw new ValidationError(
859
+ `[queueConfig] Infrastructure queue config not found: ${type}.${name} - `
860
+ + `Expected: queueConfig to define the "${name}" entry under "${type}". `
861
+ + waysOut(this.business, `${type}.${name}`)
862
+ );
411
863
  }
412
864
 
413
865
  // Deep clone to avoid modifying original
@@ -422,7 +874,10 @@ module.exports = {
422
874
  getWorkflowQueueConfig(queueName) {
423
875
  const parts = queueName.split('.');
424
876
  if (parts.length !== 2 || parts[0] !== 'workflow') {
425
- throw new Error(`Invalid workflow queue name: ${queueName}. Expected format: workflow.{name}`);
877
+ throw new ValidationError(
878
+ `[queueConfig] Invalid workflow queue name: ${queueName}. Expected format: workflow.{name} - `
879
+ + 'Fix: pass exactly two dot-separated parts whose first part is "workflow", e.g. "workflow.init".'
880
+ );
426
881
  }
427
882
  return this.getQueueConfig('workflow', parts[1]);
428
883
  },
@@ -435,37 +890,103 @@ module.exports = {
435
890
  getRegistryQueueConfig(queueName) {
436
891
  const parts = queueName.split('.');
437
892
  if (parts.length !== 2 || parts[0] !== 'registry') {
438
- throw new Error(`Invalid registry queue name: ${queueName}. Expected format: registry.{name}`);
893
+ throw new ValidationError(
894
+ `[queueConfig] Invalid registry queue name: ${queueName}. Expected format: registry.{name} - `
895
+ + 'Fix: pass exactly two dot-separated parts whose first part is "registry", e.g. "registry.register".'
896
+ );
439
897
  }
440
898
  return this.getQueueConfig('registry', parts[1]);
441
899
  },
442
900
 
443
901
  /**
444
- * Check if queue name is an infrastructure queue
902
+ * The infrastructure prefixes this configuration classifies by, in declaration
903
+ * order. Published so a caller — `initInfrastructureQueues()`, a queue-ownership
904
+ * probe, a document generator — reads the list instead of keeping a copy of it.
905
+ * @returns {ReadonlyArray<string>} Prefixes WITHOUT the trailing dot.
906
+ */
907
+ infrastructurePrefixes() {
908
+ return INFRASTRUCTURE_PREFIXES;
909
+ },
910
+
911
+ /**
912
+ * Check if queue name is an infrastructure queue.
913
+ *
914
+ * Three questions, in this order, because a prefix alone answers the wrong one:
915
+ *
916
+ * 1. **Does an infrastructure section declare this exact name?** Then it is
917
+ * infrastructure, whatever else it resembles — the declaration is the platform's
918
+ * own statement about that queue (`workflow.dlq`, `delivery.dlq`).
919
+ * 2. **Does it match a business template?** Then a service owns it. `registry` is a
920
+ * legal service name — `docs/biz/60-templates/naming.md` § Infrastructure Services
921
+ * gives it as the example — and the service by that name has `registry.workflow`,
922
+ * `registry.queue` and `registry.dlq` on the broker today, with exactly the
923
+ * arguments the templates prescribe (measured 2026-09-13). Before d.279 the
924
+ * `registry.` prefix claimed them for the platform, the registry lookup then had no
925
+ * entry for `queue`/`workflow`/`dlq`, and so `getDeadLetterRoute()` threw for a
926
+ * queue whose route is declared — which the d.259 consume gate reads as "no route",
927
+ * refusing the owner's own consumer.
928
+ * 3. **Otherwise, does it carry an infrastructure prefix?** Then it is infrastructure
929
+ * TERRITORY: declared nowhere, but a publisher must still refuse to auto-create it
930
+ * with default arguments rather than treat it as an ordinary name
931
+ * (`rabbitmqClient.js`, the 404 branch). The lookup that owns the prefix is what
932
+ * reports the missing declaration.
933
+ *
445
934
  * @param {string} queueName - Queue name to check
446
935
  * @returns {boolean} True if infrastructure queue
447
936
  */
448
937
  isInfrastructureQueue(queueName) {
449
- return queueName.startsWith('workflow.') ||
450
- queueName.startsWith('registry.') ||
451
- queueName.startsWith('infrastructure.') ||
452
- queueName.startsWith('validation.') ||
453
- queueName.startsWith('monitoring.') ||
454
- queueName.startsWith('telemetry.') ||
455
- queueName.startsWith('delivery.');
938
+ if (declaredInfrastructureNames(this).has(queueName)) {
939
+ return true;
940
+ }
941
+
942
+ if (matchBusinessTemplate(this.business, queueName) !== null) {
943
+ return false;
944
+ }
945
+
946
+ return INFRASTRUCTURE_PREFIXES.some((prefix) => queueName.startsWith(`${prefix}.`));
456
947
  },
457
948
 
458
949
  /**
459
- * Get delivery queue configuration
460
- * @param {string} queueName - Queue name (e.g., 'delivery.retry')
950
+ * Get delivery queue configuration.
951
+ *
952
+ * The `delivery.` prefix is declared by TWO sections — `delivery` (queues the
953
+ * dispatcher owns) and `deliveryEvents` (queues the delivery endpoint consumes) —
954
+ * so the lookup searches both. Which section holds a name is authoring history;
955
+ * the prefix is what travels on the wire, and a caller knows only that.
956
+ *
957
+ * @param {string} queueName - Queue name (e.g., 'delivery.retry', 'delivery.health.events')
461
958
  * @returns {Object} Queue configuration
959
+ * @throws {ValidationError} If no section declares the name, or — a defect, not a
960
+ * runtime condition — if more than one does.
462
961
  */
463
962
  getDeliveryQueueConfig(queueName) {
464
963
  if (!queueName.startsWith('delivery.')) {
465
- throw new Error(`Queue ${queueName} is not a delivery queue`);
964
+ throw new ValidationError(
965
+ `[queueConfig] Queue ${queueName} is not a delivery queue - Expected: a name starting with "delivery.". `
966
+ + 'Fix: call getInfrastructureQueueConfig(), which routes each prefix to the right lookup.'
967
+ );
968
+ }
969
+ const name = queueName.slice('delivery.'.length);
970
+ const sections = DELIVERY_SECTIONS.filter((section) => this[section]?.[name]);
971
+
972
+ if (sections.length > 1) {
973
+ throw new ValidationError(
974
+ `[queueConfig] Delivery queue ${queueName} is declared in more than one section: ${sections.join(', ')} - `
975
+ + 'Expected: exactly one section declares a name, so one queue has one definition. '
976
+ + 'Fix: delete the duplicate entry in src/config/queueConfig.js.'
977
+ );
978
+ }
979
+
980
+ if (sections.length === 0) {
981
+ throw new ValidationError(
982
+ `[queueConfig] Infrastructure queue config not found: ${queueName} - `
983
+ + `Expected: queueConfig to define the "${name}" entry under one of the delivery sections `
984
+ + `(${DELIVERY_SECTIONS.join(', ')}). `
985
+ + waysOut(this.business, queueName)
986
+ );
466
987
  }
467
- const name = queueName.replace('delivery.', '');
468
- return this.getQueueConfig('delivery', name);
988
+
989
+ return this.getQueueConfig(sections[0], name);
469
990
  },
470
991
 
471
992
  /**
@@ -475,39 +996,48 @@ module.exports = {
475
996
  */
476
997
  getValidationQueueConfig(queueName) {
477
998
  if (!queueName.startsWith('validation.')) {
478
- throw new Error(`Queue ${queueName} is not a validation queue`);
999
+ throw new ValidationError(
1000
+ `[queueConfig] Queue ${queueName} is not a validation queue - Expected: a name starting with "validation.". `
1001
+ + 'Fix: call getInfrastructureQueueConfig(), which routes each prefix to the right lookup.'
1002
+ );
479
1003
  }
480
1004
  const name = queueName.replace('validation.', '');
481
1005
  return this.getQueueConfig('validation', name);
482
1006
  },
483
1007
 
484
1008
  /**
485
- * Check if queue name is a business queue
1009
+ * Check if queue name is a business queue - a queue a business service OWNS.
1010
+ *
1011
+ * Answered from the templates the `business` section declares, so the classifier
1012
+ * and the arguments can never disagree: `{service}.workflow`, `{service}.queue`,
1013
+ * `{service}.dlq` and the two registry-client queues.
1014
+ *
486
1015
  * @param {string} queueName - Queue name to check
487
1016
  * @returns {boolean} True if business queue
488
1017
  */
489
1018
  isBusinessQueue(queueName) {
490
- // Business queues follow pattern: {service}.{type}
491
- // where type is: workflow, queue, dlq
492
- const parts = queueName.split('.');
493
- if (parts.length !== 2) return false;
494
- return ['workflow', 'queue', 'dlq'].includes(parts[1]);
1019
+ return this.parseBusinessQueue(queueName) !== null;
495
1020
  },
496
1021
 
497
1022
  /**
498
1023
  * Parse business queue name to extract service name and queue type
1024
+ *
1025
+ * A name an infrastructure section DECLARES is never a business queue, however well
1026
+ * it fits a template: `workflow.dlq`, `delivery.dlq` and `monitoring.workflow` all
1027
+ * match one, and all three are declared platform queues. Until d.279 both classifiers
1028
+ * answered `true` for them, so which arguments such a queue got was decided by the
1029
+ * order of branches in whichever caller asked.
1030
+ *
499
1031
  * @param {string} queueName - Business queue name (e.g., 'hello-service.workflow')
500
- * @returns {Object} { serviceName, queueType } or null if not a business queue
1032
+ * @returns {{serviceName: string, queueType: string}|null} The service and the
1033
+ * template key to pass to getBusinessQueueConfig(), or null if not a business queue
501
1034
  */
502
1035
  parseBusinessQueue(queueName) {
503
- if (!this.isBusinessQueue(queueName)) {
1036
+ if (declaredInfrastructureNames(this).has(queueName)) {
504
1037
  return null;
505
1038
  }
506
- const parts = queueName.split('.');
507
- return {
508
- serviceName: parts[0],
509
- queueType: parts[1]
510
- };
1039
+
1040
+ return matchBusinessTemplate(this.business, queueName);
511
1041
  },
512
1042
 
513
1043
  /**
@@ -519,7 +1049,12 @@ module.exports = {
519
1049
  getBusinessQueueConfig(queueType, serviceName) {
520
1050
  const template = this.business?.[queueType];
521
1051
  if (!template) {
522
- throw new Error(`Business queue template not found: ${queueType}`);
1052
+ throw new ValidationError(
1053
+ `[queueConfig] Business queue template not found: ${queueType} - `
1054
+ + `Expected: one of the declared business queue templates (${Object.keys(this.business).join(', ')}). `
1055
+ + 'Fix: pass a declared template key - parseBusinessQueue() returns it for any business queue name - '
1056
+ + 'or add the template to the `business` section of src/config/queueConfig.js.'
1057
+ );
523
1058
  }
524
1059
 
525
1060
  // Deep clone to avoid modifying original
@@ -544,7 +1079,10 @@ module.exports = {
544
1079
  getInfrastructureHealthQueueConfig(queueName) {
545
1080
  const parts = queueName.split('.');
546
1081
  if (parts.length < 2 || parts[0] !== 'infrastructure') {
547
- throw new Error(`Invalid infrastructure health queue name: ${queueName}. Expected format: infrastructure.{name}`);
1082
+ throw new ValidationError(
1083
+ `[queueConfig] Invalid infrastructure health queue name: ${queueName}. Expected format: infrastructure.{name} - `
1084
+ + 'Fix: pass at least two dot-separated parts whose first part is "infrastructure", e.g. "infrastructure.health.checks".'
1085
+ );
548
1086
  }
549
1087
  const name = parts.slice(1).join('.');
550
1088
  return this.getQueueConfig('infrastructure', name);
@@ -557,32 +1095,168 @@ module.exports = {
557
1095
  */
558
1096
  getMonitoringQueueConfig(queueName) {
559
1097
  if (!queueName.startsWith('monitoring.')) {
560
- throw new Error(`Queue ${queueName} is not a monitoring queue`);
1098
+ throw new ValidationError(
1099
+ `[queueConfig] Queue ${queueName} is not a monitoring queue - Expected: a name starting with "monitoring.". `
1100
+ + 'Fix: call getInfrastructureQueueConfig(), which routes each prefix to the right lookup.'
1101
+ );
561
1102
  }
562
1103
  const name = queueName.replace('monitoring.', '');
563
1104
  return this.getQueueConfig('monitoring', name);
564
1105
  },
565
1106
 
1107
+ /**
1108
+ * Get telemetry queue configuration
1109
+ * @param {string} queueName - Queue name (e.g., 'telemetry.logs.queue')
1110
+ * @returns {Object} Queue configuration
1111
+ */
1112
+ getTelemetryQueueConfig(queueName) {
1113
+ if (!queueName.startsWith('telemetry.')) {
1114
+ throw new ValidationError(
1115
+ `[queueConfig] Queue ${queueName} is not a telemetry queue - Expected: a name starting with "telemetry.". `
1116
+ + 'Fix: call getInfrastructureQueueConfig(), which routes each prefix to the right lookup.'
1117
+ );
1118
+ }
1119
+ const name = queueName.slice('telemetry.'.length);
1120
+ return this.getQueueConfig('telemetry', name);
1121
+ },
1122
+
1123
+ /**
1124
+ * The COMPLETE declaration a queue is brought into being with — `durable` and
1125
+ * every argument, dead-letter route included.
1126
+ *
1127
+ * ONE owner of the mapping name → options. Every path that declares a queue
1128
+ * reads it here and nowhere else: the public `assertQueue()` of the transport
1129
+ * (and through it `RecoveryWorker.createQueue()`, the cookbook-router's
1130
+ * QueueManager and the publish path's 404 branch), and the consumer's
1131
+ * `_prepareQueueForConsume()`.
1132
+ *
1133
+ * Until d.410 those two worked the same three lines out separately —
1134
+ * `{ durable: cfg.durable !== false, arguments: { ...cfg.arguments } }`, once
1135
+ * per path — and the consumer then declared on the raw queue channel instead of
1136
+ * the public rail. Two implementations of one concern
1137
+ * (`change-discipline.md` § One rail per concern). They agreed on the config of
1138
+ * the day, so nothing failed; what they cost is the guarantee: edit one copy and
1139
+ * the other keeps declaring the old arguments, and whichever declarer the broker
1140
+ * hears second is answered `406 PRECONDITION_FAILED` — the exact failure the
1141
+ * central config exists to prevent, and the one a service hits at boot, when its
1142
+ * consumer meets a queue somebody else already declared differently.
1143
+ *
1144
+ * Ownership is NOT decided here. This answers only "what does the declaration
1145
+ * say", never "who may declare it": the publish path still refuses to create an
1146
+ * infrastructure or business queue (`docs/standards/queue-ownership.md`), and
1147
+ * the consumer still CHECKS an infrastructure queue rather than asserting it.
1148
+ *
1149
+ * @param {string} queueName - Full queue name.
1150
+ * @returns {{durable: boolean, arguments: Object}|null} A fresh copy of the
1151
+ * declaration, or `null` when no section declares this name — there is no
1152
+ * declaration to hand out, and the caller that needs one says so itself.
1153
+ * @throws {ValidationError} If the name belongs to a section that has no entry
1154
+ * for it. A missing DEFINITION is reported by the lookup that owns it, never
1155
+ * flattened into "nothing declares it" — same contract as
1156
+ * `getDeadLetterRoute()` below.
1157
+ */
1158
+ declarationOptions(queueName) {
1159
+ let config;
1160
+
1161
+ if (this.isInfrastructureQueue(queueName)) {
1162
+ config = this.getInfrastructureQueueConfig(queueName);
1163
+ } else if (this.isBusinessQueue(queueName)) {
1164
+ const parsed = this.parseBusinessQueue(queueName);
1165
+ config = this.getBusinessQueueConfig(parsed.queueType, parsed.serviceName);
1166
+ } else {
1167
+ return null;
1168
+ }
1169
+
1170
+ return {
1171
+ durable: config.durable !== false,
1172
+ arguments: { ...config.arguments }
1173
+ };
1174
+ },
1175
+
1176
+ /**
1177
+ * The dead-letter route this configuration declares for a queue.
1178
+ *
1179
+ * The DECLARATION is the only source of truth available on the consumer path.
1180
+ * The broker cannot be asked: AMQP answers `checkQueue()` with `queue.declare-ok`,
1181
+ * which carries `{ queue, messageCount, consumerCount }` and no arguments at all
1182
+ * (measured against the live broker, 2026-09-12), so a consumer has no way to
1183
+ * read a running queue's `x-dead-letter-*` over the channel it consumes on.
1184
+ * Whoever declares a queue here declares its route here.
1185
+ *
1186
+ * A route is BOTH parts. With `x-dead-letter-exchange: ''` — the default
1187
+ * exchange, which the shared workflow queues use — the routing key IS the
1188
+ * destination queue name, so a declaration without it would send a rejected
1189
+ * message back to the queue it came from: a loop, not a dead-letter route.
1190
+ *
1191
+ * @param {string} queueName - Full queue name.
1192
+ * @returns {{exchange: string, routingKey: string}|null} The declared route, or
1193
+ * `null` when this configuration declares none — including a name no section
1194
+ * classifies, such as a temporary `rpc.reply.*` queue.
1195
+ * @throws {ValidationError} If the name belongs to a section that has no entry
1196
+ * for it. A missing DEFINITION is reported by the lookup that owns it, never
1197
+ * flattened into "declares no route".
1198
+ */
1199
+ getDeadLetterRoute(queueName) {
1200
+ let args;
1201
+
1202
+ if (this.isInfrastructureQueue(queueName)) {
1203
+ args = this.getInfrastructureQueueConfig(queueName).arguments;
1204
+ } else if (this.isBusinessQueue(queueName)) {
1205
+ const parsed = this.parseBusinessQueue(queueName);
1206
+ args = this.getBusinessQueueConfig(parsed.queueType, parsed.serviceName).arguments;
1207
+ } else {
1208
+ return null;
1209
+ }
1210
+
1211
+ if (args === null || typeof args !== 'object') {
1212
+ return null;
1213
+ }
1214
+
1215
+ const exchange = args['x-dead-letter-exchange'];
1216
+ const routingKey = args['x-dead-letter-routing-key'];
1217
+
1218
+ if (typeof exchange !== 'string' || typeof routingKey !== 'string' || routingKey === '') {
1219
+ return null;
1220
+ }
1221
+
1222
+ return { exchange, routingKey };
1223
+ },
1224
+
566
1225
  /**
567
1226
  * Get infrastructure queue configuration by queue name (auto-detect type)
1227
+ *
1228
+ * The prefix decides the lookup, read from `INFRASTRUCTURE_PREFIX_LOOKUPS` — the
1229
+ * same map `isInfrastructureQueue()` classifies by, so the two can never disagree.
1230
+ *
568
1231
  * @param {string} queueName - Full queue name (e.g., 'workflow.init', 'registry.register', 'infrastructure.health.checks', 'monitoring.workflow')
569
1232
  * @returns {Object} Queue configuration
570
1233
  */
571
1234
  getInfrastructureQueueConfig(queueName) {
572
- if (queueName.startsWith('workflow.')) {
573
- return this.getWorkflowQueueConfig(queueName);
574
- } else if (queueName.startsWith('registry.')) {
575
- return this.getRegistryQueueConfig(queueName);
576
- } else if (queueName.startsWith('infrastructure.')) {
577
- return this.getInfrastructureHealthQueueConfig(queueName);
578
- } else if (queueName.startsWith('monitoring.')) {
579
- return this.getMonitoringQueueConfig(queueName);
580
- } else if (queueName.startsWith('validation.')) {
581
- return this.getValidationQueueConfig(queueName);
582
- } else if (queueName.startsWith('delivery.')) {
583
- return this.getDeliveryQueueConfig(queueName);
584
- } else {
585
- throw new Error(`Queue ${queueName} is not an infrastructure queue. Infrastructure queues must start with 'workflow.', 'registry.', 'infrastructure.', or 'monitoring.'`);
1235
+ const prefix = INFRASTRUCTURE_PREFIXES.find((candidate) => queueName.startsWith(`${candidate}.`));
1236
+
1237
+ if (prefix === undefined) {
1238
+ throw new ValidationError(
1239
+ `[queueConfig] Queue ${queueName} is not an infrastructure queue. Infrastructure queues must start with `
1240
+ + `${INFRASTRUCTURE_PREFIXES.map((name) => `'${name}.'`).join(', ')} - `
1241
+ + 'Fix: use isBusinessQueue()/getBusinessQueueConfig() for a service-owned queue, or correct the prefix.'
1242
+ );
586
1243
  }
1244
+
1245
+ return this[INFRASTRUCTURE_PREFIX_LOOKUPS[prefix]](queueName);
1246
+ },
1247
+
1248
+ /**
1249
+ * The platform's dead-letter exchange, as the one thing to import instead of
1250
+ * the literal.
1251
+ *
1252
+ * Every business template routes here (`x-dead-letter-exchange`), and so does
1253
+ * `getDeadLetterRoute()` for a business queue. `@onlineapps/conn-infra-mq` binds
1254
+ * `<service>.dlq` to it and had to read the name back out of a queue's arguments
1255
+ * for want of an export.
1256
+ *
1257
+ * @returns {string}
1258
+ */
1259
+ deadLetterExchange() {
1260
+ return DEAD_LETTER_EXCHANGE;
587
1261
  }
588
1262
  };