@attraccess/plugin-rabbitmq 0.1.0-nightly.938.1 → 0.1.0-nightly.940.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +182 -123
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -42,9 +42,139 @@ var import_common2 = require("@nestjs/common");
42
42
 
43
43
  // backend/rabbitmq-detection.service.ts
44
44
  var import_common = require("@nestjs/common");
45
+ var import_node_crypto2 = require("node:crypto");
46
+
47
+ // backend/rabbitmq-management-transport.ts
48
+ var import_node_crypto = require("node:crypto");
49
+ var import_node_https = require("node:https");
50
+ var import_node_net = require("node:net");
51
+ var TrustConfigurationError = class extends Error {
52
+ };
53
+ function managementApiBase(config) {
54
+ const host = (0, import_node_net.isIP)(config.host) === 6 ? `[${config.host}]` : config.host;
55
+ return `${config.useTls ? "https" : "http"}://${host}:${config.useTls ? 15671 : 15672}`;
56
+ }
57
+ function trustOptions(config) {
58
+ if (config.tlsInsecure) {
59
+ throw new TrustConfigurationError(
60
+ "RabbitMQ management requires certificate verification. Disable tlsInsecure and configure a trusted CA certificate."
61
+ );
62
+ }
63
+ const servername = config.tlsServername;
64
+ if (servername != null && (typeof servername !== "string" || servername.length > 253 || !servername.split(".").every((label) => /^[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?$/i.test(label)) || (0, import_node_net.isIP)(servername))) {
65
+ throw new TrustConfigurationError(
66
+ "TLS server name must be a DNS hostname matching the management certificate, without a scheme or port."
67
+ );
68
+ }
69
+ if (config.caCert != null) {
70
+ try {
71
+ const pem = config.caCert;
72
+ const certificates = typeof pem === "string" ? pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) : null;
73
+ if (!certificates?.length || pem.replace(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g, "").trim()) {
74
+ throw new Error();
75
+ }
76
+ for (const certificate of certificates) new import_node_crypto.X509Certificate(certificate);
77
+ } catch {
78
+ throw new TrustConfigurationError(
79
+ "Invalid CA certificate. Configure a PEM certificate or PEM certificate bundle."
80
+ );
81
+ }
82
+ }
83
+ return {
84
+ rejectUnauthorized: true,
85
+ ...config.caCert != null ? { ca: config.caCert } : {},
86
+ ...servername != null ? { servername } : {}
87
+ };
88
+ }
89
+ async function managementRequest(config, url, method, body, timeoutMs) {
90
+ const headers = { accept: "application/json" };
91
+ if (config.username !== null) {
92
+ headers.authorization = `Basic ${Buffer.from(`${config.username}:${config.password ?? ""}`, "utf8").toString("base64")}`;
93
+ }
94
+ const payload = body === void 0 ? void 0 : JSON.stringify(body);
95
+ if (payload !== void 0) headers["content-type"] = "application/json";
96
+ if (!config.useTls) {
97
+ const controller = new AbortController();
98
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
99
+ try {
100
+ return await fetch(url, { method, headers, body: payload, signal: controller.signal });
101
+ } finally {
102
+ clearTimeout(timeout);
103
+ }
104
+ }
105
+ const options = trustOptions(config);
106
+ if (new URL(url).protocol !== "https:") {
107
+ throw new TrustConfigurationError("TLS is enabled: the RabbitMQ management endpoint must use HTTPS.");
108
+ }
109
+ return new Promise((resolve, reject) => {
110
+ const req = (0, import_node_https.request)(url, { ...options, agent: false, method, headers }, (res) => {
111
+ const chunks = [];
112
+ let bytes = 0;
113
+ const maximumBytes = 8 * 1024 * 1024;
114
+ res.on("data", (chunk) => {
115
+ bytes += chunk.length;
116
+ if (bytes > maximumBytes) {
117
+ chunks.length = 0;
118
+ req.destroy(Object.assign(new Error(), { code: "RESPONSE_TOO_LARGE" }));
119
+ return;
120
+ }
121
+ chunks.push(Buffer.from(chunk));
122
+ });
123
+ res.on("error", reject);
124
+ res.on("aborted", () => reject(new Error("Response aborted")));
125
+ res.on("end", () => {
126
+ if (bytes > maximumBytes) return;
127
+ const status = res.statusCode ?? 502;
128
+ try {
129
+ if (status < 200 || status > 599) throw new Error();
130
+ resolve(
131
+ new Response([204, 205, 304].includes(status) ? null : Buffer.concat(chunks).toString("utf8"), { status })
132
+ );
133
+ } catch {
134
+ reject(Object.assign(new Error(), { code: "INVALID_RESPONSE_STATUS" }));
135
+ }
136
+ });
137
+ });
138
+ const timeout = setTimeout(() => req.destroy(Object.assign(new Error(), { code: "ETIMEDOUT" })), timeoutMs);
139
+ req.on("error", reject);
140
+ req.on("close", () => clearTimeout(timeout));
141
+ req.end(payload);
142
+ });
143
+ }
144
+ function describeManagementError(error, timeoutMs) {
145
+ if (error instanceof TrustConfigurationError) return error.message;
146
+ const outer = error;
147
+ const code = outer?.cause?.code ?? outer?.code;
148
+ if (outer?.name === "AbortError" || code === "ETIMEDOUT")
149
+ return `Management API did not respond within ${timeoutMs}ms.`;
150
+ switch (code) {
151
+ case "RESPONSE_TOO_LARGE":
152
+ return "Management API response exceeded the 8 MiB limit.";
153
+ case "INVALID_RESPONSE_STATUS":
154
+ return "Management API returned an invalid HTTP status.";
155
+ case "CERT_HAS_EXPIRED":
156
+ case "CERT_NOT_YET_VALID":
157
+ return "TLS certificate is expired or not yet valid. Check the Attraccess host clock and the certificate validity dates; renew the certificate if necessary.";
158
+ case "ERR_TLS_CERT_ALTNAME_INVALID":
159
+ return "TLS certificate hostname mismatch. Configure tlsServername to match the management certificate DNS name or correct the certificate.";
160
+ case "DEPTH_ZERO_SELF_SIGNED_CERT":
161
+ case "SELF_SIGNED_CERT_IN_CHAIN":
162
+ case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
163
+ case "UNABLE_TO_GET_ISSUER_CERT":
164
+ case "UNABLE_TO_GET_ISSUER_CERT_LOCALLY":
165
+ return "TLS certificate is not trusted. Configure the issuing CA PEM bundle and ensure the management server sends its intermediate certificates.";
166
+ case "ECONNREFUSED":
167
+ return "Management connection refused. Check the management listener and port.";
168
+ case "ENOTFOUND":
169
+ case "EAI_AGAIN":
170
+ return "Management hostname could not be resolved. Check DNS and the configured host.";
171
+ default:
172
+ return "Failed to reach the RabbitMQ management API. Check connectivity and, for HTTPS, the CA, certificate validity, host clock, and TLS server name.";
173
+ }
174
+ }
175
+
176
+ // backend/rabbitmq-detection.service.ts
45
177
  var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
46
- var MGMT_PORT_HTTP = 15672;
47
- var MGMT_PORT_HTTPS = 15671;
48
178
  var CACHE_TTL_MS = 6e4;
49
179
  var PROBE_TIMEOUT_MS = 5e3;
50
180
  var RabbitmqDetectionService = class {
@@ -56,18 +186,19 @@ var RabbitmqDetectionService = class {
56
186
  // Returns a cached verdict when fresh, otherwise probes. `forceRefresh`
57
187
  // bypasses the cache (used by the UI's manual refresh).
58
188
  async detect(mqttServerId, forceRefresh = false) {
189
+ const config = await this.context.getMqttServerConfig(mqttServerId);
190
+ const configKey = (0, import_node_crypto2.createHash)("sha256").update(JSON.stringify(config ?? null)).digest("hex");
59
191
  if (!forceRefresh) {
60
192
  const cached = this.cache.get(mqttServerId);
61
- if (cached && cached.expiresAt > this.now()) {
193
+ if (cached && cached.configKey === configKey && cached.expiresAt > this.now()) {
62
194
  return cached.result;
63
195
  }
64
196
  }
65
- const result = await this.probe(mqttServerId);
66
- this.cache.set(mqttServerId, { result, expiresAt: this.now() + CACHE_TTL_MS });
197
+ const result = await this.probe(mqttServerId, config);
198
+ this.cache.set(mqttServerId, { result, configKey, expiresAt: this.now() + CACHE_TTL_MS });
67
199
  return result;
68
200
  }
69
- async probe(mqttServerId) {
70
- const config = await this.context.getMqttServerConfig(mqttServerId);
201
+ async probe(mqttServerId, config) {
71
202
  const checkedAt = this.nowIso();
72
203
  if (!config) {
73
204
  return {
@@ -143,32 +274,10 @@ var RabbitmqDetectionService = class {
143
274
  // management API runs on its own port, so we substitute the conventional
144
275
  // management port for the MQTT one and pick the scheme from useTls.
145
276
  managementApiBase(config) {
146
- const scheme = config.useTls ? "https" : "http";
147
- const port = config.useTls ? MGMT_PORT_HTTPS : MGMT_PORT_HTTP;
148
- return `${scheme}://${config.host}:${port}`;
277
+ return managementApiBase(config);
149
278
  }
150
279
  async fetchOverview(managementApi, config) {
151
- const controller = new AbortController();
152
- const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
153
- try {
154
- return await fetch(`${managementApi}/api/overview`, {
155
- headers: {
156
- accept: "application/json",
157
- ...this.authHeader(config)
158
- },
159
- signal: controller.signal
160
- });
161
- } finally {
162
- clearTimeout(timeout);
163
- }
164
- }
165
- authHeader(config) {
166
- if (config.username === null) {
167
- return {};
168
- }
169
- const credentials = `${config.username}:${config.password ?? ""}`;
170
- const encoded = Buffer.from(credentials, "utf8").toString("base64");
171
- return { authorization: `Basic ${encoded}` };
280
+ return managementRequest(config, `${managementApi}/api/overview`, "GET", void 0, PROBE_TIMEOUT_MS);
172
281
  }
173
282
  async parseOverview(response) {
174
283
  try {
@@ -178,13 +287,7 @@ var RabbitmqDetectionService = class {
178
287
  }
179
288
  }
180
289
  describeError(error) {
181
- if (error instanceof Error) {
182
- if (error.name === "AbortError") {
183
- return `Management API did not respond within ${PROBE_TIMEOUT_MS}ms.`;
184
- }
185
- return error.message;
186
- }
187
- return "Failed to reach the RabbitMQ management API.";
290
+ return describeManagementError(error, PROBE_TIMEOUT_MS);
188
291
  }
189
292
  now() {
190
293
  return Date.now();
@@ -222,63 +325,35 @@ RabbitmqDetectionController = __decorateClass([
222
325
 
223
326
  // backend/rabbitmq-management-client.ts
224
327
  var import_common3 = require("@nestjs/common");
225
- var MGMT_PORT_HTTP2 = 15672;
226
- var MGMT_PORT_HTTPS2 = 15671;
227
328
  var REQUEST_TIMEOUT_MS = 1e4;
228
329
  var RabbitmqManagementClient = class {
229
330
  // Builds the management API base URL from the generic MQTT config (same
230
331
  // convention as RabbitmqDetectionService).
231
332
  managementApiBase(config) {
232
- const scheme = config.useTls ? "https" : "http";
233
- const port = config.useTls ? MGMT_PORT_HTTPS2 : MGMT_PORT_HTTP2;
234
- return `${scheme}://${config.host}:${port}`;
333
+ return managementApiBase(config);
235
334
  }
236
335
  // Performs one management API request. `path` is relative to `/api`, with
237
336
  // every variable segment already URL-encoded by the caller. Returns the
238
337
  // parsed JSON body (or null for empty responses, e.g. 201/204).
239
338
  async request(config, method, path, body) {
240
339
  const base = this.managementApiBase(config);
241
- const controller = new AbortController();
242
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
243
340
  let response;
244
341
  try {
245
- response = await fetch(`${base}/api${path}`, {
246
- method,
247
- headers: {
248
- accept: "application/json",
249
- ...body !== void 0 ? { "content-type": "application/json" } : {},
250
- ...this.authHeader(config)
251
- },
252
- body: body !== void 0 ? JSON.stringify(body) : void 0,
253
- signal: controller.signal
254
- });
342
+ response = await managementRequest(config, `${base}/api${path}`, method, body, REQUEST_TIMEOUT_MS);
255
343
  } catch (error) {
256
- throw new import_common3.HttpException(
257
- `Cannot reach the RabbitMQ management API at ${base}: ${this.describeNetworkError(error)}`,
258
- import_common3.HttpStatus.BAD_GATEWAY
259
- );
260
- } finally {
261
- clearTimeout(timeout);
344
+ throw new import_common3.HttpException(describeManagementError(error, REQUEST_TIMEOUT_MS), import_common3.HttpStatus.BAD_GATEWAY);
262
345
  }
263
346
  if (!response.ok) {
264
347
  throw await this.toHttpException(response);
265
348
  }
266
349
  return this.parseBody(response);
267
350
  }
268
- authHeader(config) {
269
- if (config.username === null) {
270
- return {};
271
- }
272
- const credentials = `${config.username}:${config.password ?? ""}`;
273
- const encoded = Buffer.from(credentials, "utf8").toString("base64");
274
- return { authorization: `Basic ${encoded}` };
275
- }
276
351
  // Maps an upstream error response to an exception with an actionable
277
352
  // message. Auth failures surface as 502 (not 401/403) so the host frontend
278
353
  // doesn't mistake them for an expired Attraccess session.
279
354
  async toHttpException(response) {
280
355
  const upstream = await this.parseErrorBody(response);
281
- const reason = upstream?.reason || upstream?.error || "";
356
+ const reason = typeof upstream?.reason === "string" ? upstream.reason : typeof upstream?.error === "string" ? upstream.error : "";
282
357
  if (response.status === 401) {
283
358
  return new import_common3.HttpException(
284
359
  "The RabbitMQ management API rejected the configured MQTT server credentials (401). Check the username/password configured for this MQTT server.",
@@ -287,24 +362,21 @@ var RabbitmqManagementClient = class {
287
362
  }
288
363
  if (response.status === 403 || /not.?authori[sz]ed/i.test(reason)) {
289
364
  return new import_common3.HttpException(
290
- "The configured MQTT server user lacks management privileges on RabbitMQ" + (reason ? ` (${reason})` : "") + '. User management requires a user with the "administrator" tag.',
365
+ 'The configured MQTT server user lacks management privileges on RabbitMQ. User management requires a user with the "administrator" tag.',
291
366
  import_common3.HttpStatus.BAD_GATEWAY
292
367
  );
293
368
  }
294
369
  if (response.status === 404) {
295
- return new import_common3.HttpException(
296
- `Not found on the RabbitMQ side${reason ? `: ${reason}` : "."}`,
297
- import_common3.HttpStatus.NOT_FOUND
298
- );
370
+ return new import_common3.HttpException("Not found on the RabbitMQ side.", import_common3.HttpStatus.NOT_FOUND);
299
371
  }
300
372
  if (response.status === 400) {
301
373
  return new import_common3.HttpException(
302
- `RabbitMQ rejected the request${reason ? `: ${reason}` : "."}`,
374
+ "RabbitMQ rejected the request. Check the supplied management parameters.",
303
375
  import_common3.HttpStatus.BAD_REQUEST
304
376
  );
305
377
  }
306
378
  return new import_common3.HttpException(
307
- `RabbitMQ management API request failed (HTTP ${response.status})${reason ? `: ${reason}` : "."}`,
379
+ `RabbitMQ management API request failed (HTTP ${response.status}).`,
308
380
  import_common3.HttpStatus.BAD_GATEWAY
309
381
  );
310
382
  }
@@ -329,19 +401,6 @@ var RabbitmqManagementClient = class {
329
401
  return null;
330
402
  }
331
403
  }
332
- describeNetworkError(error) {
333
- if (error instanceof Error) {
334
- if (error.name === "AbortError") {
335
- return `no response within ${REQUEST_TIMEOUT_MS}ms`;
336
- }
337
- const cause = error.cause;
338
- if (cause instanceof Error && cause.message) {
339
- return cause.message;
340
- }
341
- return error.message;
342
- }
343
- return "unknown network error";
344
- }
345
404
  };
346
405
  RabbitmqManagementClient = __decorateClass([
347
406
  (0, import_common3.Injectable)()
@@ -781,28 +840,28 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
781
840
  async supports(config) {
782
841
  return (await this.detection.detect(config.id)).isRabbitMQ;
783
842
  }
784
- provision(request) {
785
- return this.writeCredential(request);
843
+ provision(request2) {
844
+ return this.writeCredential(request2);
786
845
  }
787
- rotate(request) {
788
- return this.writeCredential(request);
846
+ rotate(request2) {
847
+ return this.writeCredential(request2);
789
848
  }
790
- async revoke(request) {
791
- this.assertName(request.username, "Username");
792
- const config = await this.requireConfig(request.mqttServerId);
793
- this.assertNotManagementUser(config, request.username);
849
+ async revoke(request2) {
850
+ this.assertName(request2.username, "Username");
851
+ const config = await this.requireConfig(request2.mqttServerId);
852
+ this.assertNotManagementUser(config, request2.username);
794
853
  try {
795
- await this.client.request(config, "DELETE", `/users/${encodeURIComponent(request.username)}`);
854
+ await this.client.request(config, "DELETE", `/users/${encodeURIComponent(request2.username)}`);
796
855
  } catch (error) {
797
856
  if (error instanceof import_common6.HttpException && error.getStatus() === import_common6.HttpStatus.NOT_FOUND) return;
798
857
  throw error;
799
858
  }
800
859
  }
801
- async writeCredential(request) {
802
- this.assertRequest(request);
803
- const config = await this.requireConfig(request.mqttServerId);
804
- this.assertNotManagementUser(config, request.username);
805
- const lockKey = `${config.id}:${request.vhost}`;
860
+ async writeCredential(request2) {
861
+ this.assertRequest(request2);
862
+ const config = await this.requireConfig(request2.mqttServerId);
863
+ this.assertNotManagementUser(config, request2.username);
864
+ const lockKey = `${config.id}:${request2.vhost}`;
806
865
  let lock = _RabbitmqCredentialProvisioningProvider.vhostLocks.get(lockKey);
807
866
  if (!lock) {
808
867
  lock = { mutex: new Mutex(), users: 0 };
@@ -810,7 +869,7 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
810
869
  }
811
870
  lock.users += 1;
812
871
  try {
813
- return await lock.mutex.runExclusive(() => this.writeCredentialLocked(request, config));
872
+ return await lock.mutex.runExclusive(() => this.writeCredentialLocked(request2, config));
814
873
  } finally {
815
874
  lock.users -= 1;
816
875
  if (lock.users === 0 && _RabbitmqCredentialProvisioningProvider.vhostLocks.get(lockKey) === lock) {
@@ -818,10 +877,10 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
818
877
  }
819
878
  }
820
879
  }
821
- async writeCredentialLocked(request, config) {
880
+ async writeCredentialLocked(request2, config) {
822
881
  const password = (0, import_crypto.randomBytes)(24).toString("base64url");
823
- const vhost = encodeURIComponent(request.vhost);
824
- const username = encodeURIComponent(request.username);
882
+ const vhost = encodeURIComponent(request2.vhost);
883
+ const username = encodeURIComponent(request2.username);
825
884
  const existing = await this.userExists(config, username);
826
885
  const vhostExisted = await this.exists(config, `/vhosts/${vhost}`);
827
886
  await this.client.request(config, "PUT", `/vhosts/${vhost}`);
@@ -833,11 +892,11 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
833
892
  previousPermissions = await this.getPermissions(config, vhost, username);
834
893
  previousTopicPermissions = await this.getTopicPermissions(config, vhost, username);
835
894
  permissionsCaptured = true;
836
- await this.writePermissions(config, request, vhost, username);
895
+ await this.writePermissions(config, request2, vhost, username);
837
896
  await this.client.request(config, "PUT", `/users/${username}`, { password, tags: "" });
838
897
  } else {
839
898
  await this.client.request(config, "PUT", `/users/${username}`, { password, tags: "" });
840
- await this.writePermissions(config, request, vhost, username);
899
+ await this.writePermissions(config, request2, vhost, username);
841
900
  }
842
901
  } catch (error) {
843
902
  const rollback = [
@@ -848,14 +907,14 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
848
907
  }
849
908
  return {
850
909
  providerId: this.id,
851
- identity: request.identity,
852
- username: request.username,
853
- vhost: request.vhost,
910
+ identity: request2.identity,
911
+ username: request2.username,
912
+ vhost: request2.vhost,
854
913
  password
855
914
  };
856
915
  }
857
- async writePermissions(config, request, vhost, username) {
858
- const subscriptionQueue = `mqtt-subscription-${escapeRegex(request.identity)}.*`;
916
+ async writePermissions(config, request2, vhost, username) {
917
+ const subscriptionQueue = `mqtt-subscription-${escapeRegex(request2.identity)}.*`;
859
918
  await this.client.request(config, "PUT", `/permissions/${vhost}/${username}`, {
860
919
  configure: `^${subscriptionQueue}$`,
861
920
  write: `^(amq\\.topic|${subscriptionQueue})$`,
@@ -863,8 +922,8 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
863
922
  });
864
923
  await this.client.request(config, "PUT", `/topic-permissions/${vhost}/${username}`, {
865
924
  exchange: "amq.topic",
866
- write: mqttFiltersToRegex(request.topicPolicy.publish),
867
- read: mqttFiltersToRegex(request.topicPolicy.subscribe)
925
+ write: mqttFiltersToRegex(request2.topicPolicy.publish),
926
+ read: mqttFiltersToRegex(request2.topicPolicy.subscribe)
868
927
  });
869
928
  }
870
929
  async userExists(config, username) {
@@ -925,14 +984,14 @@ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvision
925
984
  }
926
985
  return config;
927
986
  }
928
- assertRequest(request) {
929
- this.assertName(request.identity, "Identity");
930
- this.assertName(request.username, "Username");
931
- this.assertName(request.vhost, "Vhost");
932
- if (!Array.isArray(request.topicPolicy.publish) || !Array.isArray(request.topicPolicy.subscribe)) {
987
+ assertRequest(request2) {
988
+ this.assertName(request2.identity, "Identity");
989
+ this.assertName(request2.username, "Username");
990
+ this.assertName(request2.vhost, "Vhost");
991
+ if (!Array.isArray(request2.topicPolicy.publish) || !Array.isArray(request2.topicPolicy.subscribe)) {
933
992
  throw new import_common6.BadRequestException("Publish and subscribe topic policies must be arrays.");
934
993
  }
935
- for (const filter of [...request.topicPolicy.publish, ...request.topicPolicy.subscribe]) {
994
+ for (const filter of [...request2.topicPolicy.publish, ...request2.topicPolicy.subscribe]) {
936
995
  if (typeof filter !== "string" || filter.length === 0 || filter.length > 1024) {
937
996
  throw new import_common6.BadRequestException("MQTT topic filters must be non-empty strings no longer than 1024 characters.");
938
997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@attraccess/plugin-rabbitmq",
3
- "version": "0.1.0-nightly.938.1",
3
+ "version": "0.1.0-nightly.940.1",
4
4
  "description": "Official Attraccess integration for RabbitMQ.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {