@heavybit/shared-lib 0.0.0-stage → 1.1.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/dist/cjs/access/index.js +118 -0
- package/dist/cjs/access/index.js.map +1 -0
- package/dist/cjs/alerting/index.js +56 -0
- package/dist/cjs/alerting/index.js.map +1 -0
- package/dist/cjs/cache/index.js +167 -0
- package/dist/cjs/cache/index.js.map +1 -0
- package/dist/cjs/common/async.js +40 -0
- package/dist/cjs/common/async.js.map +1 -0
- package/dist/cjs/common/audit.js +21 -0
- package/dist/cjs/common/audit.js.map +1 -0
- package/dist/cjs/common/crypto.js +75 -0
- package/dist/cjs/common/crypto.js.map +1 -0
- package/dist/cjs/common/errors.js +90 -0
- package/dist/cjs/common/errors.js.map +1 -0
- package/dist/cjs/common/id.js +40 -0
- package/dist/cjs/common/id.js.map +1 -0
- package/dist/cjs/common/index.js +28 -0
- package/dist/cjs/common/index.js.map +1 -0
- package/dist/cjs/common/jwt.js +59 -0
- package/dist/cjs/common/jwt.js.map +1 -0
- package/dist/cjs/common/password.js +38 -0
- package/dist/cjs/common/password.js.map +1 -0
- package/dist/cjs/common/phone.js +42 -0
- package/dist/cjs/common/phone.js.map +1 -0
- package/dist/cjs/common/response.js +32 -0
- package/dist/cjs/common/response.js.map +1 -0
- package/dist/cjs/common/text.js +46 -0
- package/dist/cjs/common/text.js.map +1 -0
- package/dist/cjs/common/time.js +109 -0
- package/dist/cjs/common/time.js.map +1 -0
- package/dist/cjs/config/index.js +96 -0
- package/dist/cjs/config/index.js.map +1 -0
- package/dist/cjs/contracts/editorial-draft.js +111 -0
- package/dist/cjs/contracts/editorial-draft.js.map +1 -0
- package/dist/cjs/contracts/index.js +20 -0
- package/dist/cjs/contracts/index.js.map +1 -0
- package/dist/cjs/contracts/notification-dispatch.js +92 -0
- package/dist/cjs/contracts/notification-dispatch.js.map +1 -0
- package/dist/cjs/contracts/upload-import.js +54 -0
- package/dist/cjs/contracts/upload-import.js.map +1 -0
- package/dist/cjs/database/index.js +57 -0
- package/dist/cjs/database/index.js.map +1 -0
- package/dist/cjs/express/auth.js +72 -0
- package/dist/cjs/express/auth.js.map +1 -0
- package/dist/cjs/express/health.js +31 -0
- package/dist/cjs/express/health.js.map +1 -0
- package/dist/cjs/express/index.js +20 -0
- package/dist/cjs/express/index.js.map +1 -0
- package/dist/cjs/express/middleware.js +95 -0
- package/dist/cjs/express/middleware.js.map +1 -0
- package/dist/cjs/http/index.js +90 -0
- package/dist/cjs/http/index.js.map +1 -0
- package/dist/cjs/http/safe-fetch.js +199 -0
- package/dist/cjs/http/safe-fetch.js.map +1 -0
- package/dist/cjs/identity/index.js +117 -0
- package/dist/cjs/identity/index.js.map +1 -0
- package/dist/cjs/index.js +29 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/knex/index.js +76 -0
- package/dist/cjs/knex/index.js.map +1 -0
- package/dist/cjs/logging/index.js +75 -0
- package/dist/cjs/logging/index.js.map +1 -0
- package/dist/cjs/messaging/connection.js +115 -0
- package/dist/cjs/messaging/connection.js.map +1 -0
- package/dist/cjs/messaging/consumer.js +134 -0
- package/dist/cjs/messaging/consumer.js.map +1 -0
- package/dist/cjs/messaging/envelope.js +23 -0
- package/dist/cjs/messaging/envelope.js.map +1 -0
- package/dist/cjs/messaging/idempotency.js +57 -0
- package/dist/cjs/messaging/idempotency.js.map +1 -0
- package/dist/cjs/messaging/index.js +62 -0
- package/dist/cjs/messaging/index.js.map +1 -0
- package/dist/cjs/messaging/monitor.js +48 -0
- package/dist/cjs/messaging/monitor.js.map +1 -0
- package/dist/cjs/messaging/outbox.js +135 -0
- package/dist/cjs/messaging/outbox.js.map +1 -0
- package/dist/cjs/messaging/publisher.js +149 -0
- package/dist/cjs/messaging/publisher.js.map +1 -0
- package/dist/cjs/messaging/rpc.js +125 -0
- package/dist/cjs/messaging/rpc.js.map +1 -0
- package/dist/cjs/messaging/staff-notifier.js +78 -0
- package/dist/cjs/messaging/staff-notifier.js.map +1 -0
- package/dist/cjs/messaging/topology.js +22 -0
- package/dist/cjs/messaging/topology.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/rate-limit/index.js +61 -0
- package/dist/cjs/rate-limit/index.js.map +1 -0
- package/dist/cjs/scheduling/index.js +88 -0
- package/dist/cjs/scheduling/index.js.map +1 -0
- package/dist/cjs/security/index.js +39 -0
- package/dist/cjs/security/index.js.map +1 -0
- package/dist/cjs/socket/index.js +53 -0
- package/dist/cjs/socket/index.js.map +1 -0
- package/dist/cjs/testing/index.js +73 -0
- package/dist/cjs/testing/index.js.map +1 -0
- package/dist/cjs/types/index.js +3 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/validation/index.js +41 -0
- package/dist/cjs/validation/index.js.map +1 -0
- package/dist/esm/access/index.js +104 -0
- package/dist/esm/access/index.js.map +1 -0
- package/dist/esm/alerting/index.js +52 -0
- package/dist/esm/alerting/index.js.map +1 -0
- package/dist/esm/cache/index.js +163 -0
- package/dist/esm/cache/index.js.map +1 -0
- package/dist/esm/common/async.js +33 -0
- package/dist/esm/common/async.js.map +1 -0
- package/dist/esm/common/audit.js +18 -0
- package/dist/esm/common/audit.js.map +1 -0
- package/dist/esm/common/crypto.js +64 -0
- package/dist/esm/common/crypto.js.map +1 -0
- package/dist/esm/common/errors.js +75 -0
- package/dist/esm/common/errors.js.map +1 -0
- package/dist/esm/common/id.js +33 -0
- package/dist/esm/common/id.js.map +1 -0
- package/dist/esm/common/index.js +12 -0
- package/dist/esm/common/index.js.map +1 -0
- package/dist/esm/common/jwt.js +49 -0
- package/dist/esm/common/jwt.js.map +1 -0
- package/dist/esm/common/password.js +30 -0
- package/dist/esm/common/password.js.map +1 -0
- package/dist/esm/common/phone.js +33 -0
- package/dist/esm/common/phone.js.map +1 -0
- package/dist/esm/common/response.js +24 -0
- package/dist/esm/common/response.js.map +1 -0
- package/dist/esm/common/text.js +39 -0
- package/dist/esm/common/text.js.map +1 -0
- package/dist/esm/common/time.js +96 -0
- package/dist/esm/common/time.js.map +1 -0
- package/dist/esm/config/index.js +88 -0
- package/dist/esm/config/index.js.map +1 -0
- package/dist/esm/contracts/editorial-draft.js +108 -0
- package/dist/esm/contracts/editorial-draft.js.map +1 -0
- package/dist/esm/contracts/index.js +4 -0
- package/dist/esm/contracts/index.js.map +1 -0
- package/dist/esm/contracts/notification-dispatch.js +89 -0
- package/dist/esm/contracts/notification-dispatch.js.map +1 -0
- package/dist/esm/contracts/upload-import.js +51 -0
- package/dist/esm/contracts/upload-import.js.map +1 -0
- package/dist/esm/database/index.js +48 -0
- package/dist/esm/database/index.js.map +1 -0
- package/dist/esm/express/auth.js +63 -0
- package/dist/esm/express/auth.js.map +1 -0
- package/dist/esm/express/health.js +27 -0
- package/dist/esm/express/health.js.map +1 -0
- package/dist/esm/express/index.js +4 -0
- package/dist/esm/express/index.js.map +1 -0
- package/dist/esm/express/middleware.js +84 -0
- package/dist/esm/express/middleware.js.map +1 -0
- package/dist/esm/http/index.js +73 -0
- package/dist/esm/http/index.js.map +1 -0
- package/dist/esm/http/safe-fetch.js +193 -0
- package/dist/esm/http/safe-fetch.js.map +1 -0
- package/dist/esm/identity/index.js +111 -0
- package/dist/esm/identity/index.js.map +1 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/knex/index.js +66 -0
- package/dist/esm/knex/index.js.map +1 -0
- package/dist/esm/logging/index.js +67 -0
- package/dist/esm/logging/index.js.map +1 -0
- package/dist/esm/messaging/connection.js +108 -0
- package/dist/esm/messaging/connection.js.map +1 -0
- package/dist/esm/messaging/consumer.js +130 -0
- package/dist/esm/messaging/consumer.js.map +1 -0
- package/dist/esm/messaging/envelope.js +19 -0
- package/dist/esm/messaging/envelope.js.map +1 -0
- package/dist/esm/messaging/idempotency.js +52 -0
- package/dist/esm/messaging/idempotency.js.map +1 -0
- package/dist/esm/messaging/index.js +45 -0
- package/dist/esm/messaging/index.js.map +1 -0
- package/dist/esm/messaging/monitor.js +44 -0
- package/dist/esm/messaging/monitor.js.map +1 -0
- package/dist/esm/messaging/outbox.js +129 -0
- package/dist/esm/messaging/outbox.js.map +1 -0
- package/dist/esm/messaging/publisher.js +142 -0
- package/dist/esm/messaging/publisher.js.map +1 -0
- package/dist/esm/messaging/rpc.js +120 -0
- package/dist/esm/messaging/rpc.js.map +1 -0
- package/dist/esm/messaging/staff-notifier.js +73 -0
- package/dist/esm/messaging/staff-notifier.js.map +1 -0
- package/dist/esm/messaging/topology.js +16 -0
- package/dist/esm/messaging/topology.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/rate-limit/index.js +56 -0
- package/dist/esm/rate-limit/index.js.map +1 -0
- package/dist/esm/scheduling/index.js +83 -0
- package/dist/esm/scheduling/index.js.map +1 -0
- package/dist/esm/security/index.js +36 -0
- package/dist/esm/security/index.js.map +1 -0
- package/dist/esm/socket/index.js +47 -0
- package/dist/esm/socket/index.js.map +1 -0
- package/dist/esm/testing/index.js +66 -0
- package/dist/esm/testing/index.js.map +1 -0
- package/dist/esm/types/index.js +2 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/validation/index.js +36 -0
- package/dist/esm/validation/index.js.map +1 -0
- package/dist/types/access/index.d.ts +50 -0
- package/dist/types/access/index.d.ts.map +1 -0
- package/dist/types/alerting/index.d.ts +43 -0
- package/dist/types/alerting/index.d.ts.map +1 -0
- package/dist/types/cache/index.d.ts +45 -0
- package/dist/types/cache/index.d.ts.map +1 -0
- package/dist/types/common/async.d.ts +14 -0
- package/dist/types/common/async.d.ts.map +1 -0
- package/dist/types/common/audit.d.ts +17 -0
- package/dist/types/common/audit.d.ts.map +1 -0
- package/dist/types/common/crypto.d.ts +18 -0
- package/dist/types/common/crypto.d.ts.map +1 -0
- package/dist/types/common/errors.d.ts +71 -0
- package/dist/types/common/errors.d.ts.map +1 -0
- package/dist/types/common/id.d.ts +14 -0
- package/dist/types/common/id.d.ts.map +1 -0
- package/dist/types/common/index.d.ts +12 -0
- package/dist/types/common/index.d.ts.map +1 -0
- package/dist/types/common/jwt.d.ts +28 -0
- package/dist/types/common/jwt.d.ts.map +1 -0
- package/dist/types/common/password.d.ts +4 -0
- package/dist/types/common/password.d.ts.map +1 -0
- package/dist/types/common/phone.d.ts +10 -0
- package/dist/types/common/phone.d.ts.map +1 -0
- package/dist/types/common/response.d.ts +12 -0
- package/dist/types/common/response.d.ts.map +1 -0
- package/dist/types/common/text.d.ts +10 -0
- package/dist/types/common/text.d.ts.map +1 -0
- package/dist/types/common/time.d.ts +40 -0
- package/dist/types/common/time.d.ts.map +1 -0
- package/dist/types/config/index.d.ts +115 -0
- package/dist/types/config/index.d.ts.map +1 -0
- package/dist/types/contracts/editorial-draft.d.ts +577 -0
- package/dist/types/contracts/editorial-draft.d.ts.map +1 -0
- package/dist/types/contracts/index.d.ts +4 -0
- package/dist/types/contracts/index.d.ts.map +1 -0
- package/dist/types/contracts/notification-dispatch.d.ts +175 -0
- package/dist/types/contracts/notification-dispatch.d.ts.map +1 -0
- package/dist/types/contracts/upload-import.d.ts +96 -0
- package/dist/types/contracts/upload-import.d.ts.map +1 -0
- package/dist/types/database/index.d.ts +25 -0
- package/dist/types/database/index.d.ts.map +1 -0
- package/dist/types/express/auth.d.ts +27 -0
- package/dist/types/express/auth.d.ts.map +1 -0
- package/dist/types/express/health.d.ts +14 -0
- package/dist/types/express/health.d.ts.map +1 -0
- package/dist/types/express/index.d.ts +4 -0
- package/dist/types/express/index.d.ts.map +1 -0
- package/dist/types/express/middleware.d.ts +20 -0
- package/dist/types/express/middleware.d.ts.map +1 -0
- package/dist/types/http/index.d.ts +34 -0
- package/dist/types/http/index.d.ts.map +1 -0
- package/dist/types/http/safe-fetch.d.ts +58 -0
- package/dist/types/http/safe-fetch.d.ts.map +1 -0
- package/dist/types/identity/index.d.ts +51 -0
- package/dist/types/identity/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/knex/index.d.ts +25 -0
- package/dist/types/knex/index.d.ts.map +1 -0
- package/dist/types/logging/index.d.ts +22 -0
- package/dist/types/logging/index.d.ts.map +1 -0
- package/dist/types/messaging/connection.d.ts +48 -0
- package/dist/types/messaging/connection.d.ts.map +1 -0
- package/dist/types/messaging/consumer.d.ts +56 -0
- package/dist/types/messaging/consumer.d.ts.map +1 -0
- package/dist/types/messaging/envelope.d.ts +16 -0
- package/dist/types/messaging/envelope.d.ts.map +1 -0
- package/dist/types/messaging/idempotency.d.ts +29 -0
- package/dist/types/messaging/idempotency.d.ts.map +1 -0
- package/dist/types/messaging/index.d.ts +39 -0
- package/dist/types/messaging/index.d.ts.map +1 -0
- package/dist/types/messaging/monitor.d.ts +23 -0
- package/dist/types/messaging/monitor.d.ts.map +1 -0
- package/dist/types/messaging/outbox.d.ts +51 -0
- package/dist/types/messaging/outbox.d.ts.map +1 -0
- package/dist/types/messaging/publisher.d.ts +84 -0
- package/dist/types/messaging/publisher.d.ts.map +1 -0
- package/dist/types/messaging/rpc.d.ts +46 -0
- package/dist/types/messaging/rpc.d.ts.map +1 -0
- package/dist/types/messaging/staff-notifier.d.ts +56 -0
- package/dist/types/messaging/staff-notifier.d.ts.map +1 -0
- package/dist/types/messaging/topology.d.ts +23 -0
- package/dist/types/messaging/topology.d.ts.map +1 -0
- package/dist/types/rate-limit/index.d.ts +28 -0
- package/dist/types/rate-limit/index.d.ts.map +1 -0
- package/dist/types/scheduling/index.d.ts +33 -0
- package/dist/types/scheduling/index.d.ts.map +1 -0
- package/dist/types/security/index.d.ts +20 -0
- package/dist/types/security/index.d.ts.map +1 -0
- package/dist/types/socket/index.d.ts +30 -0
- package/dist/types/socket/index.d.ts.map +1 -0
- package/dist/types/testing/index.d.ts +41 -0
- package/dist/types/testing/index.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +65 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/validation/index.d.ts +34 -0
- package/dist/types/validation/index.d.ts.map +1 -0
- package/package.json +189 -4
- package/README.md +0 -3
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NoopMessageBus = exports.MessagePublisher = exports.UnroutableMessageError = void 0;
|
|
4
|
+
exports.isUnroutableError = isUnroutableError;
|
|
5
|
+
const errors_js_1 = require("../common/errors.js");
|
|
6
|
+
const id_js_1 = require("../common/id.js");
|
|
7
|
+
const index_js_1 = require("../logging/index.js");
|
|
8
|
+
const envelope_js_1 = require("./envelope.js");
|
|
9
|
+
/**
|
|
10
|
+
* The broker accepted a `mandatory` message but no queue was bound to receive it (the consumer has never
|
|
11
|
+
* started, or its binding is wrong). Retryable: the message is not lost — the caller (or the outbox relay)
|
|
12
|
+
* keeps it and tries again, and it goes through once the owner declares its queue.
|
|
13
|
+
*/
|
|
14
|
+
class UnroutableMessageError extends errors_js_1.ServiceUnavailableError {
|
|
15
|
+
exchange;
|
|
16
|
+
routingKey;
|
|
17
|
+
constructor(exchange, routingKey) {
|
|
18
|
+
super(`No queue is bound for ${exchange || '(default exchange)'} → ${routingKey}`, { code: 'UNROUTABLE' });
|
|
19
|
+
this.exchange = exchange;
|
|
20
|
+
this.routingKey = routingKey;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.UnroutableMessageError = UnroutableMessageError;
|
|
24
|
+
function isUnroutableError(error) {
|
|
25
|
+
return error instanceof UnroutableMessageError;
|
|
26
|
+
}
|
|
27
|
+
const envelopeProps = (envelope, mandatory) => ({
|
|
28
|
+
mandatory, messageId: envelope.eventId, correlationId: envelope.correlationId, type: envelope.type,
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Event and command publisher on a confirm channel: `publish()` resolves only once the broker
|
|
32
|
+
* has persisted the message, and rejects with 503 when the broker is unreachable so callers
|
|
33
|
+
* (or the outbox relay) can retry.
|
|
34
|
+
*
|
|
35
|
+
* Commands and direct queue sends are `mandatory`: a command has exactly one owner, so one that no
|
|
36
|
+
* queue receives is a fault, not a no-op. The broker returns it (`basic.return`, always before its
|
|
37
|
+
* confirm), and the send rejects with `UnroutableMessageError` instead of resolving as if delivered.
|
|
38
|
+
* Events stay non-mandatory: a topic event with no subscriber yet is normal fan-out.
|
|
39
|
+
*/
|
|
40
|
+
class MessagePublisher {
|
|
41
|
+
options;
|
|
42
|
+
handle = null;
|
|
43
|
+
logger;
|
|
44
|
+
/** Message ids the broker returned as unroutable, awaiting their confirm. */
|
|
45
|
+
returned = new Set();
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.options = options;
|
|
48
|
+
this.logger = options.logger ?? index_js_1.silentLogger;
|
|
49
|
+
}
|
|
50
|
+
async init() {
|
|
51
|
+
const { topology } = this.options;
|
|
52
|
+
this.handle = await this.options.connection.registerChannel('publisher', async (channel) => {
|
|
53
|
+
await channel.assertExchange(topology.eventsExchange, 'topic', { durable: true });
|
|
54
|
+
await channel.assertExchange(topology.commandsExchange, 'direct', { durable: true });
|
|
55
|
+
channel.on('return', (message) => {
|
|
56
|
+
const id = message.properties.messageId;
|
|
57
|
+
if (typeof id === 'string')
|
|
58
|
+
this.returned.add(id);
|
|
59
|
+
});
|
|
60
|
+
}, { confirm: true });
|
|
61
|
+
}
|
|
62
|
+
async publish(type, data, meta = {}) {
|
|
63
|
+
const envelope = (0, envelope_js_1.buildEnvelope)(type, data, this.options.source, meta);
|
|
64
|
+
await this.publishEnvelope(envelope);
|
|
65
|
+
return envelope;
|
|
66
|
+
}
|
|
67
|
+
async publishEnvelope(envelope) {
|
|
68
|
+
await this.write(this.options.topology.eventsExchange, envelope.type, envelope, envelopeProps(envelope, false));
|
|
69
|
+
}
|
|
70
|
+
async send(command, data, meta = {}) {
|
|
71
|
+
const envelope = (0, envelope_js_1.buildEnvelope)(command, data, this.options.source, meta);
|
|
72
|
+
await this.sendEnvelope(envelope);
|
|
73
|
+
return envelope;
|
|
74
|
+
}
|
|
75
|
+
async sendEnvelope(envelope) {
|
|
76
|
+
await this.write(this.options.topology.commandsExchange, this.options.topology.queue(envelope.type), envelope, envelopeProps(envelope, true));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sends `payload` as-is (no envelope) to `queue` through the default exchange. The queue is the
|
|
80
|
+
* consumer's to declare, so it is not asserted here; a missing queue rejects as unroutable.
|
|
81
|
+
*/
|
|
82
|
+
async sendToQueue(queue, payload, options = {}) {
|
|
83
|
+
await this.write('', queue, payload, { mandatory: true, messageId: options.messageId ?? (0, id_js_1.newId)(), correlationId: options.correlationId });
|
|
84
|
+
}
|
|
85
|
+
write(exchange, routingKey, body, props) {
|
|
86
|
+
const channel = this.handle?.current();
|
|
87
|
+
if (!channel) {
|
|
88
|
+
return Promise.reject(new errors_js_1.ServiceUnavailableError('Message broker unavailable'));
|
|
89
|
+
}
|
|
90
|
+
const { messageId } = props;
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
channel.publish(exchange, routingKey, Buffer.from(JSON.stringify(body)), {
|
|
93
|
+
persistent: true,
|
|
94
|
+
mandatory: props.mandatory,
|
|
95
|
+
contentType: 'application/json',
|
|
96
|
+
messageId,
|
|
97
|
+
correlationId: props.correlationId,
|
|
98
|
+
type: props.type,
|
|
99
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
100
|
+
appId: this.options.source,
|
|
101
|
+
}, (error) => {
|
|
102
|
+
// The broker sends `basic.return` before the confirm, so a returned message is already recorded here.
|
|
103
|
+
const unroutable = this.returned.delete(messageId);
|
|
104
|
+
if (error) {
|
|
105
|
+
this.logger.error('Broker rejected message', { routingKey, messageId, error: String(error) });
|
|
106
|
+
reject(new errors_js_1.ServiceUnavailableError('Message broker rejected the message', { cause: error }));
|
|
107
|
+
}
|
|
108
|
+
else if (unroutable) {
|
|
109
|
+
this.logger.warn('Broker could not route message: no queue is bound', { exchange, routingKey, messageId });
|
|
110
|
+
reject(new UnroutableMessageError(exchange, routingKey));
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
resolve();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.MessagePublisher = MessagePublisher;
|
|
120
|
+
/** Stand-in when messaging is disabled (local tooling, tests): logs and drops. */
|
|
121
|
+
class NoopMessageBus {
|
|
122
|
+
source;
|
|
123
|
+
logger;
|
|
124
|
+
constructor(source, logger = index_js_1.silentLogger) {
|
|
125
|
+
this.source = source;
|
|
126
|
+
this.logger = logger;
|
|
127
|
+
}
|
|
128
|
+
async publish(type, data, meta = {}) {
|
|
129
|
+
const envelope = (0, envelope_js_1.buildEnvelope)(type, data, this.source, meta);
|
|
130
|
+
this.logger.debug('Messaging disabled; event dropped', { type, eventId: envelope.eventId });
|
|
131
|
+
return envelope;
|
|
132
|
+
}
|
|
133
|
+
async publishEnvelope(envelope) {
|
|
134
|
+
this.logger.debug('Messaging disabled; event dropped', { type: envelope.type, eventId: envelope.eventId });
|
|
135
|
+
}
|
|
136
|
+
async send(command, data, meta = {}) {
|
|
137
|
+
const envelope = (0, envelope_js_1.buildEnvelope)(command, data, this.source, meta);
|
|
138
|
+
this.logger.debug('Messaging disabled; command dropped', { command, eventId: envelope.eventId });
|
|
139
|
+
return envelope;
|
|
140
|
+
}
|
|
141
|
+
async sendEnvelope(envelope) {
|
|
142
|
+
this.logger.debug('Messaging disabled; command dropped', { command: envelope.type, eventId: envelope.eventId });
|
|
143
|
+
}
|
|
144
|
+
async sendToQueue(queue) {
|
|
145
|
+
this.logger.debug('Messaging disabled; queue message dropped', { queue });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.NoopMessageBus = NoopMessageBus;
|
|
149
|
+
//# sourceMappingURL=publisher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publisher.js","sourceRoot":"","sources":["../../../src/messaging/publisher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RpcClient = exports.RpcServer = void 0;
|
|
4
|
+
const errors_js_1 = require("../common/errors.js");
|
|
5
|
+
const id_js_1 = require("../common/id.js");
|
|
6
|
+
const index_js_1 = require("../logging/index.js");
|
|
7
|
+
const REPLY_TO = 'amq.rabbitmq.reply-to';
|
|
8
|
+
/**
|
|
9
|
+
* Request/response over RabbitMQ for the few synchronous cross-service calls that must fail
|
|
10
|
+
* cleanly (e.g. "create the profile before committing the account"). Uses direct reply-to, so
|
|
11
|
+
* no reply queue is declared per call, and sets the request TTL to the caller's timeout so a
|
|
12
|
+
* request the caller has given up on is never executed late.
|
|
13
|
+
*/
|
|
14
|
+
class RpcServer {
|
|
15
|
+
connection;
|
|
16
|
+
topology;
|
|
17
|
+
handles = [];
|
|
18
|
+
logger;
|
|
19
|
+
constructor(connection, topology, logger) {
|
|
20
|
+
this.connection = connection;
|
|
21
|
+
this.topology = topology;
|
|
22
|
+
this.logger = logger ?? index_js_1.silentLogger;
|
|
23
|
+
}
|
|
24
|
+
async handle(method, handler, options = {}) {
|
|
25
|
+
const queue = this.topology.queue(method);
|
|
26
|
+
this.handles.push(await this.connection.registerChannel(`rpc-server:${queue}`, async (channel) => {
|
|
27
|
+
await channel.assertQueue(queue, { durable: true });
|
|
28
|
+
await channel.prefetch(options.prefetch ?? 5);
|
|
29
|
+
await channel.consume(queue, (msg) => { if (msg)
|
|
30
|
+
void this.respond(channel, msg, handler); });
|
|
31
|
+
}));
|
|
32
|
+
this.logger.info('RPC method registered', { queue });
|
|
33
|
+
}
|
|
34
|
+
async respond(channel, msg, handler) {
|
|
35
|
+
let reply;
|
|
36
|
+
try {
|
|
37
|
+
const payload = JSON.parse(msg.content.toString('utf8'));
|
|
38
|
+
const data = await handler(payload, { correlationId: msg.properties.correlationId, callerService: msg.properties.appId });
|
|
39
|
+
reply = { ok: true, data };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const status = (0, errors_js_1.isAppError)(error) ? error.statusCode : 500;
|
|
43
|
+
const message = (0, errors_js_1.isAppError)(error) && status < 500 ? error.message : 'RPC handler failed';
|
|
44
|
+
if (status >= 500)
|
|
45
|
+
this.logger.error('RPC handler error', { queue: msg.fields.routingKey, error: error.message });
|
|
46
|
+
reply = { ok: false, error: { status, code: (0, errors_js_1.isAppError)(error) ? error.code : 'INTERNAL_ERROR', message } };
|
|
47
|
+
}
|
|
48
|
+
if (msg.properties.replyTo) {
|
|
49
|
+
channel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify(reply)), { correlationId: msg.properties.correlationId, contentType: 'application/json' });
|
|
50
|
+
}
|
|
51
|
+
channel.ack(msg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.RpcServer = RpcServer;
|
|
55
|
+
class RpcClient {
|
|
56
|
+
connection;
|
|
57
|
+
topology;
|
|
58
|
+
callerService;
|
|
59
|
+
handle = null;
|
|
60
|
+
pending = new Map();
|
|
61
|
+
logger;
|
|
62
|
+
constructor(connection, topology, callerService, logger) {
|
|
63
|
+
this.connection = connection;
|
|
64
|
+
this.topology = topology;
|
|
65
|
+
this.callerService = callerService;
|
|
66
|
+
this.logger = logger ?? index_js_1.silentLogger;
|
|
67
|
+
}
|
|
68
|
+
async init() {
|
|
69
|
+
this.handle = await this.connection.registerChannel('rpc-client', async (channel) => {
|
|
70
|
+
// A reconnect invalidates every in-flight reply.
|
|
71
|
+
this.failAll(new errors_js_1.ServiceUnavailableError('RPC channel reset'));
|
|
72
|
+
await channel.consume(REPLY_TO, (msg) => { if (msg)
|
|
73
|
+
this.onReply(msg); }, { noAck: true });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
call(method, payload, options = {}) {
|
|
77
|
+
const channel = this.handle?.current();
|
|
78
|
+
if (!channel)
|
|
79
|
+
return Promise.reject(new errors_js_1.ServiceUnavailableError('Message broker unavailable'));
|
|
80
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
81
|
+
const id = (0, id_js_1.newId)();
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
this.pending.delete(id);
|
|
85
|
+
this.logger.warn('RPC timed out', { method, timeoutMs });
|
|
86
|
+
reject(new errors_js_1.ServiceUnavailableError(`Upstream did not respond in time (${method})`));
|
|
87
|
+
}, timeoutMs);
|
|
88
|
+
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
89
|
+
channel.sendToQueue(this.topology.queue(method), Buffer.from(JSON.stringify(payload)), {
|
|
90
|
+
correlationId: id,
|
|
91
|
+
replyTo: REPLY_TO,
|
|
92
|
+
contentType: 'application/json',
|
|
93
|
+
expiration: String(timeoutMs),
|
|
94
|
+
appId: this.callerService,
|
|
95
|
+
headers: options.correlationId ? { 'x-correlation-id': options.correlationId } : undefined,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
onReply(msg) {
|
|
100
|
+
const entry = this.pending.get(msg.properties.correlationId);
|
|
101
|
+
if (!entry)
|
|
102
|
+
return;
|
|
103
|
+
this.pending.delete(msg.properties.correlationId);
|
|
104
|
+
clearTimeout(entry.timer);
|
|
105
|
+
try {
|
|
106
|
+
const reply = JSON.parse(msg.content.toString('utf8'));
|
|
107
|
+
if (reply.ok)
|
|
108
|
+
entry.resolve(reply.data);
|
|
109
|
+
else
|
|
110
|
+
entry.reject(new errors_js_1.AppError(reply.error.status, reply.error.message, { code: reply.error.code }));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
entry.reject(new errors_js_1.ServiceUnavailableError('Malformed RPC reply', { cause: error }));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
failAll(error) {
|
|
117
|
+
for (const [id, entry] of this.pending) {
|
|
118
|
+
clearTimeout(entry.timer);
|
|
119
|
+
entry.reject(error);
|
|
120
|
+
this.pending.delete(id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.RpcClient = RpcClient;
|
|
125
|
+
//# sourceMappingURL=rpc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../../../src/messaging/rpc.ts"],"names":[],"mappings":";;;AACA,mDAAoF;AACpF,2CAAwC;AACxC,kDAAmD;AAKnD,MAAM,QAAQ,GAAG,uBAAuB,CAAC;AASzC;;;;;GAKG;AACH,MAAa,SAAS;IAIS;IAA6C;IAHzD,OAAO,GAAoB,EAAE,CAAC;IAC9B,MAAM,CAAS;IAEhC,YAA6B,UAA0B,EAAmB,QAAkB,EAAE,MAAe;QAAhF,eAAU,GAAV,UAAU,CAAgB;QAAmB,aAAQ,GAAR,QAAQ,CAAU;QAC1F,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,uBAAY,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,MAAM,CAAO,MAAc,EAAE,OAAwD,EAAE,UAAiC,EAAE;QAC9H,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,cAAc,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YAC/F,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACpD,MAAM,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC;YAC9C,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,GAAG;gBAAE,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAChG,CAAC,CAAC,CAAC,CAAC;QACJ,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;IACvD,CAAC;IAEO,KAAK,CAAC,OAAO,CAAO,OAAgB,EAAE,GAAmB,EAAE,OAAoD;QACrH,IAAI,KAAkB,CAAC;QACvB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAM,CAAC;YAC9D,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,aAAa,EAAE,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;YAC1H,KAAK,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,IAAA,sBAAU,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC;YAC1D,MAAM,OAAO,GAAG,IAAA,sBAAU,EAAC,KAAK,CAAC,IAAI,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC;YACzF,IAAI,MAAM,IAAI,GAAG;gBAAE,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,EAAG,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;YAC7H,KAAK,GAAG,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAA,sBAAU,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,gBAAgB,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7G,CAAC;QACD,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;YAC3B,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,aAAa,EAAE,GAAG,CAAC,UAAU,CAAC,aAAa,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC,CAAC;QACpK,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;CACF;AAnCD,8BAmCC;AAMD,MAAa,SAAS;IAKS;IAA6C;IAAqC;IAJvG,MAAM,GAAyB,IAAI,CAAC;IAC3B,OAAO,GAAG,IAAI,GAAG,EAA+G,CAAC;IACjI,MAAM,CAAS;IAEhC,YAA6B,UAA0B,EAAmB,QAAkB,EAAmB,aAAqB,EAAE,MAAe;QAAxH,eAAU,GAAV,UAAU,CAAgB;QAAmB,aAAQ,GAAR,QAAQ,CAAU;QAAmB,kBAAa,GAAb,aAAa,CAAQ;QAClI,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,uBAAY,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YAClF,iDAAiD;YACjD,IAAI,CAAC,OAAO,CAAC,IAAI,mCAAuB,CAAC,mBAAmB,CAAC,CAAC,CAAC;YAC/D,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,GAAG;gBAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7F,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAO,MAAc,EAAE,OAAU,EAAE,UAA0D,EAAE;QACjG,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;QACvC,IAAI,CAAC,OAAO;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,mCAAuB,CAAC,4BAA4B,CAAC,CAAC,CAAC;QAC/F,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;QAC5C,MAAM,EAAE,GAAG,IAAA,aAAK,GAAE,CAAC;QACnB,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACxC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACxB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;gBACzD,MAAM,CAAC,IAAI,mCAAuB,CAAC,qCAAqC,MAAM,GAAG,CAAC,CAAC,CAAC;YACtF,CAAC,EAAE,SAAS,CAAC,CAAC;YACd,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,OAA+B,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;YAClF,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE;gBACrF,aAAa,EAAE,EAAE;gBACjB,OAAO,EAAE,QAAQ;gBACjB,WAAW,EAAE,kBAAkB;gBAC/B,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC;gBAC7B,KAAK,EAAE,IAAI,CAAC,aAAa;gBACzB,OAAO,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,SAAS;aAC3F,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,OAAO,CAAC,GAAmB;QACjC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QAC7D,IAAI,CAAC,KAAK;YAAE,OAAO;QACnB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;QAClD,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAsB,CAAC;YAC5E,IAAI,KAAK,CAAC,EAAE;gBAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;;gBACnC,KAAK,CAAC,MAAM,CAAC,IAAI,oBAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACvG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,MAAM,CAAC,IAAI,mCAAuB,CAAC,qBAAqB,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QACrF,CAAC;IACH,CAAC;IAEO,OAAO,CAAC,KAAY;QAC1B,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACvC,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1B,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;CACF;AA7DD,8BA6DC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.staffEmailListSchema = exports.StaffNotifier = exports.ConfiguredStaffRecipients = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const notification_dispatch_js_1 = require("../contracts/notification-dispatch.js");
|
|
6
|
+
const index_js_1 = require("../logging/index.js");
|
|
7
|
+
/** Recipients from configuration (env), keyed by the permission an alert targets. */
|
|
8
|
+
class ConfiguredStaffRecipients {
|
|
9
|
+
byPermission;
|
|
10
|
+
constructor(byPermission) {
|
|
11
|
+
this.byPermission = byPermission;
|
|
12
|
+
}
|
|
13
|
+
async resolve(permission) {
|
|
14
|
+
return [...(this.byPermission[permission] ?? [])];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ConfiguredStaffRecipients = ConfiguredStaffRecipients;
|
|
18
|
+
/**
|
|
19
|
+
* Stages one `notification:dispatch` per recipient through the outbox, in the caller's transaction, so an
|
|
20
|
+
* alert is sent if and only if the state change that raised it commits. Never sends email itself.
|
|
21
|
+
* An alert that resolves to nobody is logged as a warning (an unconfigured list must not fail silently).
|
|
22
|
+
*/
|
|
23
|
+
class StaffNotifier {
|
|
24
|
+
outbox;
|
|
25
|
+
recipients;
|
|
26
|
+
logger;
|
|
27
|
+
constructor(outbox, recipients, logger = index_js_1.silentLogger) {
|
|
28
|
+
this.outbox = outbox;
|
|
29
|
+
this.recipients = recipients;
|
|
30
|
+
this.logger = logger;
|
|
31
|
+
}
|
|
32
|
+
/** Returns how many commands were staged. */
|
|
33
|
+
async alert(executor, alert, meta = {}) {
|
|
34
|
+
const targets = await this.recipients.resolve(alert.permission);
|
|
35
|
+
if (targets.length === 0) {
|
|
36
|
+
this.logger.warn('Staff alert not sent: no recipients are configured', { template: alert.template, permission: alert.permission });
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
for (const recipient of targets) {
|
|
40
|
+
const dispatch = {
|
|
41
|
+
template: alert.template,
|
|
42
|
+
category: 'staff',
|
|
43
|
+
recipient,
|
|
44
|
+
channels: alert.channels ?? ['email'],
|
|
45
|
+
data: alert.data,
|
|
46
|
+
bypassPreferences: true,
|
|
47
|
+
targetPermission: alert.permission,
|
|
48
|
+
};
|
|
49
|
+
await this.outbox.command(executor, notification_dispatch_js_1.NOTIFICATION_DISPATCH, dispatch, { ...meta, actorId: null });
|
|
50
|
+
}
|
|
51
|
+
return targets.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.StaffNotifier = StaffNotifier;
|
|
55
|
+
const emailSchema = zod_1.z.string().email().max(notification_dispatch_js_1.DISPATCH_LIMITS.email);
|
|
56
|
+
/**
|
|
57
|
+
* A comma-separated staff address list from env (`"a@x.io, b@y.io"`): trimmed, blanks dropped,
|
|
58
|
+
* de-duplicated case-insensitively. An invalid address fails validation, so a typo is caught at boot
|
|
59
|
+
* instead of dead-lettering in notification-service when the alert finally fires.
|
|
60
|
+
*/
|
|
61
|
+
exports.staffEmailListSchema = zod_1.z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.transform((csv) => {
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
return (csv ?? '')
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((entry) => entry.trim())
|
|
69
|
+
.filter((email) => email && !seen.has(email.toLowerCase()) && seen.add(email.toLowerCase()));
|
|
70
|
+
})
|
|
71
|
+
.superRefine((emails, ctx) => {
|
|
72
|
+
for (const email of emails) {
|
|
73
|
+
if (!emailSchema.safeParse(email).success)
|
|
74
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: `Not a valid email address: ${email}` });
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
.transform((emails) => emails.map((email) => ({ email })));
|
|
78
|
+
//# sourceMappingURL=staff-notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staff-notifier.js","sourceRoot":"","sources":["../../../src/messaging/staff-notifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deadLetterQueueName = exports.retryQueueName = void 0;
|
|
4
|
+
exports.createTopology = createTopology;
|
|
5
|
+
function createTopology(options) {
|
|
6
|
+
const app = options.app.trim();
|
|
7
|
+
const prefix = options.prefix.trim();
|
|
8
|
+
if (!app || !prefix)
|
|
9
|
+
throw new Error('Topology requires both app and prefix');
|
|
10
|
+
return {
|
|
11
|
+
app,
|
|
12
|
+
prefix,
|
|
13
|
+
eventsExchange: `${app}.${prefix}.events`,
|
|
14
|
+
commandsExchange: `${app}.${prefix}.commands`,
|
|
15
|
+
queue: (name) => (name.startsWith(`${prefix}.`) ? name : `${prefix}.${name}`),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const retryQueueName = (queue, delayMs) => `${queue}.retry.${delayMs}`;
|
|
19
|
+
exports.retryQueueName = retryQueueName;
|
|
20
|
+
const deadLetterQueueName = (queue) => `${queue}.dlq`;
|
|
21
|
+
exports.deadLetterQueueName = deadLetterQueueName;
|
|
22
|
+
//# sourceMappingURL=topology.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topology.js","sourceRoot":"","sources":["../../../src/messaging/topology.ts"],"names":[],"mappings":";;;AAiBA,wCAWC;AAXD,SAAgB,cAAc,CAAC,OAAwC;IACrE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACrC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC9E,OAAO;QACL,GAAG;QACH,MAAM;QACN,cAAc,EAAE,GAAG,GAAG,IAAI,MAAM,SAAS;QACzC,gBAAgB,EAAE,GAAG,GAAG,IAAI,MAAM,WAAW;QAC7C,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC;KACtF,CAAC;AACJ,CAAC;AAEM,MAAM,cAAc,GAAG,CAAC,KAAa,EAAE,OAAe,EAAE,EAAE,CAAC,GAAG,KAAK,UAAU,OAAO,EAAE,CAAC;AAAjF,QAAA,cAAc,kBAAmE;AACvF,MAAM,mBAAmB,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC;AAAxD,QAAA,mBAAmB,uBAAqC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLimiter = createLimiter;
|
|
4
|
+
exports.consumeOrThrow = consumeOrThrow;
|
|
5
|
+
exports.rateLimitMiddleware = rateLimitMiddleware;
|
|
6
|
+
const rate_limiter_flexible_1 = require("rate-limiter-flexible");
|
|
7
|
+
const response_js_1 = require("../common/response.js");
|
|
8
|
+
const errors_js_1 = require("../common/errors.js");
|
|
9
|
+
const index_js_1 = require("../logging/index.js");
|
|
10
|
+
/** Redis limiter with an in-memory insurance limiter so a Redis outage never blocks traffic. */
|
|
11
|
+
function createLimiter(options) {
|
|
12
|
+
const memory = new rate_limiter_flexible_1.RateLimiterMemory({ keyPrefix: options.keyPrefix, points: options.points, duration: options.durationSec, blockDuration: options.blockDurationSec ?? 0 });
|
|
13
|
+
if (!options.redis)
|
|
14
|
+
return memory;
|
|
15
|
+
return new rate_limiter_flexible_1.RateLimiterRedis({
|
|
16
|
+
storeClient: options.redis,
|
|
17
|
+
keyPrefix: options.keyPrefix,
|
|
18
|
+
points: options.points,
|
|
19
|
+
duration: options.durationSec,
|
|
20
|
+
blockDuration: options.blockDurationSec ?? 0,
|
|
21
|
+
insuranceLimiter: memory,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Consumes one point for `key`; throws TooManyRequestsError (429) when exhausted.
|
|
26
|
+
* For in-service checks such as per-identifier login throttling.
|
|
27
|
+
*/
|
|
28
|
+
async function consumeOrThrow(limiter, key, message) {
|
|
29
|
+
try {
|
|
30
|
+
await limiter.consume(key);
|
|
31
|
+
}
|
|
32
|
+
catch (rejection) {
|
|
33
|
+
if (typeof rejection?.msBeforeNext === 'number')
|
|
34
|
+
throw new errors_js_1.TooManyRequestsError(message);
|
|
35
|
+
throw rejection;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function rateLimitMiddleware(options) {
|
|
39
|
+
const limiter = createLimiter(options);
|
|
40
|
+
const logger = options.logger ?? index_js_1.silentLogger;
|
|
41
|
+
const keyOf = options.keyOf ?? ((req) => req.user?.id ?? req.ip ?? 'unknown');
|
|
42
|
+
return async (req, res, next) => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await limiter.consume(keyOf(req));
|
|
45
|
+
res.setHeader('X-RateLimit-Limit', options.points);
|
|
46
|
+
res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
|
|
47
|
+
next();
|
|
48
|
+
}
|
|
49
|
+
catch (rejection) {
|
|
50
|
+
const ms = rejection?.msBeforeNext;
|
|
51
|
+
if (typeof ms !== 'number') {
|
|
52
|
+
logger.error('Rate limiter failure; allowing request', { keyPrefix: options.keyPrefix, error: String(rejection) });
|
|
53
|
+
next();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
res.setHeader('Retry-After', Math.ceil(ms / 1000));
|
|
57
|
+
res.status(429).json((0, response_js_1.errorResponse)('Too many requests. Please try again later.', undefined, 'RATE_LIMITED'));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rate-limit/index.ts"],"names":[],"mappings":";;AAoBA,sCAWC;AAMD,wCAOC;AAOD,kDAsBC;AAvED,iEAAsG;AACtG,uDAAsD;AACtD,mDAA2D;AAC3D,kDAAmD;AAcnD,gGAAgG;AAChG,SAAgB,aAAa,CAAC,OAAyB;IACrD,MAAM,MAAM,GAAG,IAAI,yCAAiB,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,WAAW,EAAE,aAAa,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5K,IAAI,CAAC,OAAO,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAClC,OAAO,IAAI,wCAAgB,CAAC;QAC1B,WAAW,EAAE,OAAO,CAAC,KAAK;QAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,OAAO,CAAC,WAAW;QAC7B,aAAa,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC;QAC5C,gBAAgB,EAAE,MAAM;KACzB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAAC,OAA4B,EAAE,GAAW,EAAE,OAAgB;IAC9F,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,SAAS,EAAE,CAAC;QACnB,IAAI,OAAQ,SAAuC,EAAE,YAAY,KAAK,QAAQ;YAAE,MAAM,IAAI,gCAAoB,CAAC,OAAO,CAAC,CAAC;QACxH,MAAM,SAAS,CAAC;IAClB,CAAC;AACH,CAAC;AAOD,SAAgB,mBAAmB,CAAC,OAAmC;IACrE,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,uBAAY,CAAC;IAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC,CAAC;IAEvF,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAiB,EAAE;QAC9E,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YACjD,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACnD,GAAG,CAAC,SAAS,CAAC,uBAAuB,EAAE,MAAM,CAAC,eAAe,CAAC,CAAC;YAC/D,IAAI,EAAE,CAAC;QACT,CAAC;QAAC,OAAO,SAAS,EAAE,CAAC;YACnB,MAAM,EAAE,GAAI,SAAuC,EAAE,YAAY,CAAC;YAClE,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;gBAC3B,MAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBACnH,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YACD,GAAG,CAAC,SAAS,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;YACnD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAA,2BAAa,EAAC,4CAA4C,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC;QAC/G,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScheduledJob = void 0;
|
|
4
|
+
exports.createLock = createLock;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const index_js_1 = require("../logging/index.js");
|
|
7
|
+
const RELEASE_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
|
|
8
|
+
/** Redis lock (SET NX PX + token-checked release). Without Redis, a per-process lock. */
|
|
9
|
+
function createLock(redis, prefix = 'lock:') {
|
|
10
|
+
const local = new Set();
|
|
11
|
+
return {
|
|
12
|
+
async runExclusive(key, ttlMs, fn) {
|
|
13
|
+
if (!redis) {
|
|
14
|
+
if (local.has(key))
|
|
15
|
+
return false;
|
|
16
|
+
local.add(key);
|
|
17
|
+
try {
|
|
18
|
+
await fn();
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
local.delete(key);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
const token = (0, node_crypto_1.randomUUID)();
|
|
26
|
+
const acquired = await redis.set(prefix + key, token, 'PX', ttlMs, 'NX').catch(() => null);
|
|
27
|
+
if (acquired !== 'OK')
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
await fn();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await redis.eval(RELEASE_SCRIPT, 1, prefix + key, token).catch(() => undefined);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Interval job that never overlaps itself, can be triggered early by events (`trigger()`), and
|
|
41
|
+
* optionally runs on one replica at a time.
|
|
42
|
+
*/
|
|
43
|
+
class ScheduledJob {
|
|
44
|
+
options;
|
|
45
|
+
timer = null;
|
|
46
|
+
running = false;
|
|
47
|
+
logger;
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.logger = options.logger ?? index_js_1.silentLogger;
|
|
51
|
+
}
|
|
52
|
+
start() {
|
|
53
|
+
if (this.timer)
|
|
54
|
+
return;
|
|
55
|
+
this.timer = setInterval(() => { void this.trigger(); }, this.options.intervalMs);
|
|
56
|
+
this.timer.unref?.();
|
|
57
|
+
if (this.options.runOnStart)
|
|
58
|
+
void this.trigger();
|
|
59
|
+
}
|
|
60
|
+
stop() {
|
|
61
|
+
if (this.timer)
|
|
62
|
+
clearInterval(this.timer);
|
|
63
|
+
this.timer = null;
|
|
64
|
+
}
|
|
65
|
+
get isRunning() {
|
|
66
|
+
return this.running;
|
|
67
|
+
}
|
|
68
|
+
async trigger() {
|
|
69
|
+
if (this.running)
|
|
70
|
+
return;
|
|
71
|
+
this.running = true;
|
|
72
|
+
try {
|
|
73
|
+
const run = async () => { await this.options.work(); };
|
|
74
|
+
if (this.options.lock)
|
|
75
|
+
await this.options.lock.runExclusive(`job:${this.options.name}`, Math.max(this.options.intervalMs, 30_000), run);
|
|
76
|
+
else
|
|
77
|
+
await run();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.logger.error('Scheduled job failed', { job: this.options.name, error: error.message });
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
this.running = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.ScheduledJob = ScheduledJob;
|
|
88
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/scheduling/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createUrlSigner = createUrlSigner;
|
|
4
|
+
const crypto_js_1 = require("../common/crypto.js");
|
|
5
|
+
/** HMAC-SHA256 expiring URL signatures for private files (streams, downloads, originals). */
|
|
6
|
+
function createUrlSigner(secret) {
|
|
7
|
+
if (!secret || secret.length < 16)
|
|
8
|
+
throw new Error('URL signing secret must be at least 16 characters');
|
|
9
|
+
const expiry = (ttlSec) => Math.floor(Date.now() / 1000) + ttlSec;
|
|
10
|
+
const mac = (target, exp) => (0, crypto_js_1.hmacSha256)(secret, `${target}|${exp}`);
|
|
11
|
+
const signer = {
|
|
12
|
+
sign(path, ttlSec) {
|
|
13
|
+
const exp = expiry(ttlSec);
|
|
14
|
+
return { exp, sig: mac(path, exp) };
|
|
15
|
+
},
|
|
16
|
+
signPrefix(prefix, ttlSec) {
|
|
17
|
+
const exp = expiry(ttlSec);
|
|
18
|
+
return { exp, sig: mac(`prefix:${prefix}`, exp), sp: prefix };
|
|
19
|
+
},
|
|
20
|
+
signedPath(path, ttlSec, options = {}) {
|
|
21
|
+
const params = options.prefix ? signer.signPrefix(options.prefix, ttlSec) : signer.sign(path, ttlSec);
|
|
22
|
+
const qs = new URLSearchParams({ exp: String(params.exp), sig: params.sig, ...(params.sp ? { sp: params.sp } : {}) });
|
|
23
|
+
return `${path}${path.includes('?') ? '&' : '?'}${qs.toString()}`;
|
|
24
|
+
},
|
|
25
|
+
verify(path, params, now = Date.now()) {
|
|
26
|
+
const exp = Number(params.exp);
|
|
27
|
+
if (!params.sig || !Number.isFinite(exp) || exp * 1000 < now)
|
|
28
|
+
return false;
|
|
29
|
+
if (params.sp) {
|
|
30
|
+
if (!path.startsWith(params.sp))
|
|
31
|
+
return false;
|
|
32
|
+
return (0, crypto_js_1.safeEqual)(params.sig, mac(`prefix:${params.sp}`, exp));
|
|
33
|
+
}
|
|
34
|
+
return (0, crypto_js_1.safeEqual)(params.sig, mac(path, exp));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
return signer;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/security/index.ts"],"names":[],"mappings":";;AAoBA,0CA8BC;AAlDD,mDAA4D;AAmB5D,6FAA6F;AAC7F,SAAgB,eAAe,CAAC,MAAc;IAC5C,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACxG,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC;IAC1E,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,GAAW,EAAE,EAAE,CAAC,IAAA,sBAAU,EAAC,MAAM,EAAE,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;IAEpF,MAAM,MAAM,GAAc;QACxB,IAAI,CAAC,IAAI,EAAE,MAAM;YACf,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACtC,CAAC;QACD,UAAU,CAAC,MAAM,EAAE,MAAM;YACvB,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3B,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,UAAU,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;QAChE,CAAC;QACD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAG,EAAE;YACnC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YACtG,MAAM,EAAE,GAAG,IAAI,eAAe,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;YACtH,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;QACpE,CAAC;QACD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;YACnC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG;gBAAE,OAAO,KAAK,CAAC;YAC3E,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;gBACd,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;oBAAE,OAAO,KAAK,CAAC;gBAC9C,OAAO,IAAA,qBAAS,EAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,UAAU,MAAM,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;YAChE,CAAC;YACD,OAAO,IAAA,qBAAS,EAAC,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;QAC/C,CAAC;KACF,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC"}
|