@attraccess/plugin-rabbitmq 0.1.0-nightly.925.2

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/index.js ADDED
@@ -0,0 +1,1047 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var __decorateClass = (decorators, target, key, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
+ if (decorator = decorators[i])
23
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
24
+ if (kind && result) __defProp(target, key, result);
25
+ return result;
26
+ };
27
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
28
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
29
+
30
+ // backend/plugin.ts
31
+ var plugin_exports = {};
32
+ __export(plugin_exports, {
33
+ default: () => plugin_default
34
+ });
35
+ module.exports = __toCommonJS(plugin_exports);
36
+ var import_plugins_backend_sdk3 = require("@attraccess/plugins-backend-sdk");
37
+ var import_common7 = require("@nestjs/common");
38
+
39
+ // backend/rabbitmq-detection.controller.ts
40
+ var import_plugins_backend_sdk = require("@attraccess/plugins-backend-sdk");
41
+ var import_common2 = require("@nestjs/common");
42
+
43
+ // backend/rabbitmq-detection.service.ts
44
+ var import_common = require("@nestjs/common");
45
+ var PLUGIN_CONTEXT = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
46
+ var MGMT_PORT_HTTP = 15672;
47
+ var MGMT_PORT_HTTPS = 15671;
48
+ var CACHE_TTL_MS = 6e4;
49
+ var PROBE_TIMEOUT_MS = 5e3;
50
+ var RabbitmqDetectionService = class {
51
+ constructor(context) {
52
+ __publicField(this, "context", context);
53
+ // Verdicts keyed by MQTT server id. In-memory only; a restart re-probes.
54
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
55
+ }
56
+ // Returns a cached verdict when fresh, otherwise probes. `forceRefresh`
57
+ // bypasses the cache (used by the UI's manual refresh).
58
+ async detect(mqttServerId, forceRefresh = false) {
59
+ if (!forceRefresh) {
60
+ const cached = this.cache.get(mqttServerId);
61
+ if (cached && cached.expiresAt > this.now()) {
62
+ return cached.result;
63
+ }
64
+ }
65
+ const result = await this.probe(mqttServerId);
66
+ this.cache.set(mqttServerId, { result, expiresAt: this.now() + CACHE_TTL_MS });
67
+ return result;
68
+ }
69
+ async probe(mqttServerId) {
70
+ const config = await this.context.getMqttServerConfig(mqttServerId);
71
+ const checkedAt = this.nowIso();
72
+ if (!config) {
73
+ return {
74
+ mqttServerId,
75
+ isRabbitMQ: false,
76
+ reachable: false,
77
+ authOk: false,
78
+ rabbitmqVersion: null,
79
+ managementVersion: null,
80
+ managementApi: "",
81
+ checkedAt,
82
+ error: "MQTT server not found."
83
+ };
84
+ }
85
+ const managementApi = this.managementApiBase(config);
86
+ let response;
87
+ try {
88
+ response = await this.fetchOverview(managementApi, config);
89
+ } catch (error) {
90
+ return {
91
+ mqttServerId,
92
+ isRabbitMQ: false,
93
+ reachable: false,
94
+ authOk: false,
95
+ rabbitmqVersion: null,
96
+ managementVersion: null,
97
+ managementApi,
98
+ checkedAt,
99
+ error: this.describeError(error)
100
+ };
101
+ }
102
+ if (response.status === 401) {
103
+ return {
104
+ mqttServerId,
105
+ isRabbitMQ: true,
106
+ reachable: true,
107
+ authOk: false,
108
+ rabbitmqVersion: null,
109
+ managementVersion: null,
110
+ managementApi,
111
+ checkedAt,
112
+ error: "Authentication failed (401) against the RabbitMQ management API."
113
+ };
114
+ }
115
+ if (!response.ok) {
116
+ return {
117
+ mqttServerId,
118
+ isRabbitMQ: false,
119
+ reachable: true,
120
+ authOk: false,
121
+ rabbitmqVersion: null,
122
+ managementVersion: null,
123
+ managementApi,
124
+ checkedAt,
125
+ error: `Unexpected response (HTTP ${response.status}) \u2014 not a RabbitMQ management API.`
126
+ };
127
+ }
128
+ const overview = await this.parseOverview(response);
129
+ const isRabbitMQ = overview !== null && typeof overview.rabbitmq_version === "string";
130
+ return {
131
+ mqttServerId,
132
+ isRabbitMQ,
133
+ reachable: true,
134
+ authOk: isRabbitMQ,
135
+ rabbitmqVersion: overview?.rabbitmq_version ?? null,
136
+ managementVersion: overview?.management_version ?? null,
137
+ managementApi,
138
+ checkedAt,
139
+ error: isRabbitMQ ? null : "Reachable, but the response is not a RabbitMQ management API."
140
+ };
141
+ }
142
+ // Builds the management API base URL from the generic MQTT config. The
143
+ // management API runs on its own port, so we substitute the conventional
144
+ // management port for the MQTT one and pick the scheme from useTls.
145
+ 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}`;
149
+ }
150
+ 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}` };
172
+ }
173
+ async parseOverview(response) {
174
+ try {
175
+ return await response.json();
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+ 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.";
188
+ }
189
+ now() {
190
+ return Date.now();
191
+ }
192
+ nowIso() {
193
+ return (/* @__PURE__ */ new Date()).toISOString();
194
+ }
195
+ };
196
+ RabbitmqDetectionService = __decorateClass([
197
+ (0, import_common.Injectable)(),
198
+ __decorateParam(0, (0, import_common.Inject)(PLUGIN_CONTEXT))
199
+ ], RabbitmqDetectionService);
200
+
201
+ // backend/rabbitmq-detection.controller.ts
202
+ var RabbitmqDetectionController = class {
203
+ // esbuild does not emit decorator metadata, so Nest cannot infer constructor
204
+ // types for injection — always inject by an explicit token.
205
+ constructor(detection) {
206
+ __publicField(this, "detection", detection);
207
+ }
208
+ detect(mqttServerId, refresh) {
209
+ return this.detection.detect(mqttServerId, refresh === "true");
210
+ }
211
+ };
212
+ __decorateClass([
213
+ (0, import_common2.Get)("detection/:mqttServerId"),
214
+ __decorateParam(0, (0, import_common2.Param)("mqttServerId", import_common2.ParseIntPipe)),
215
+ __decorateParam(1, (0, import_common2.Query)("refresh"))
216
+ ], RabbitmqDetectionController.prototype, "detect", 1);
217
+ RabbitmqDetectionController = __decorateClass([
218
+ (0, import_plugins_backend_sdk.Auth)("resources.update"),
219
+ (0, import_common2.Controller)("rabbitmq"),
220
+ __decorateParam(0, (0, import_common2.Inject)(RabbitmqDetectionService))
221
+ ], RabbitmqDetectionController);
222
+
223
+ // backend/rabbitmq-management-client.ts
224
+ var import_common3 = require("@nestjs/common");
225
+ var MGMT_PORT_HTTP2 = 15672;
226
+ var MGMT_PORT_HTTPS2 = 15671;
227
+ var REQUEST_TIMEOUT_MS = 1e4;
228
+ var RabbitmqManagementClient = class {
229
+ // Builds the management API base URL from the generic MQTT config (same
230
+ // convention as RabbitmqDetectionService).
231
+ 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}`;
235
+ }
236
+ // Performs one management API request. `path` is relative to `/api`, with
237
+ // every variable segment already URL-encoded by the caller. Returns the
238
+ // parsed JSON body (or null for empty responses, e.g. 201/204).
239
+ async request(config, method, path, body) {
240
+ const base = this.managementApiBase(config);
241
+ const controller = new AbortController();
242
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
243
+ let response;
244
+ 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
+ });
255
+ } 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);
262
+ }
263
+ if (!response.ok) {
264
+ throw await this.toHttpException(response);
265
+ }
266
+ return this.parseBody(response);
267
+ }
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
+ // Maps an upstream error response to an exception with an actionable
277
+ // message. Auth failures surface as 502 (not 401/403) so the host frontend
278
+ // doesn't mistake them for an expired Attraccess session.
279
+ async toHttpException(response) {
280
+ const upstream = await this.parseErrorBody(response);
281
+ const reason = upstream?.reason || upstream?.error || "";
282
+ if (response.status === 401) {
283
+ return new import_common3.HttpException(
284
+ "The RabbitMQ management API rejected the configured MQTT server credentials (401). Check the username/password configured for this MQTT server.",
285
+ import_common3.HttpStatus.BAD_GATEWAY
286
+ );
287
+ }
288
+ if (response.status === 403 || /not.?authori[sz]ed/i.test(reason)) {
289
+ 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.',
291
+ import_common3.HttpStatus.BAD_GATEWAY
292
+ );
293
+ }
294
+ 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
+ );
299
+ }
300
+ if (response.status === 400) {
301
+ return new import_common3.HttpException(
302
+ `RabbitMQ rejected the request${reason ? `: ${reason}` : "."}`,
303
+ import_common3.HttpStatus.BAD_REQUEST
304
+ );
305
+ }
306
+ return new import_common3.HttpException(
307
+ `RabbitMQ management API request failed (HTTP ${response.status})${reason ? `: ${reason}` : "."}`,
308
+ import_common3.HttpStatus.BAD_GATEWAY
309
+ );
310
+ }
311
+ async parseBody(response) {
312
+ const text = await response.text();
313
+ if (text.length === 0) {
314
+ return null;
315
+ }
316
+ try {
317
+ return JSON.parse(text);
318
+ } catch {
319
+ throw new import_common3.HttpException(
320
+ "The RabbitMQ management API returned a response that is not valid JSON.",
321
+ import_common3.HttpStatus.BAD_GATEWAY
322
+ );
323
+ }
324
+ }
325
+ async parseErrorBody(response) {
326
+ try {
327
+ return await response.json();
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
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
+ };
346
+ RabbitmqManagementClient = __decorateClass([
347
+ (0, import_common3.Injectable)()
348
+ ], RabbitmqManagementClient);
349
+
350
+ // backend/rabbitmq-users.controller.ts
351
+ var import_plugins_backend_sdk2 = require("@attraccess/plugins-backend-sdk");
352
+ var import_common5 = require("@nestjs/common");
353
+
354
+ // backend/rabbitmq-users.service.ts
355
+ var import_common4 = require("@nestjs/common");
356
+ var PLUGIN_CONTEXT2 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
357
+ var RabbitmqUsersService = class {
358
+ constructor(context, client) {
359
+ __publicField(this, "context", context);
360
+ __publicField(this, "client", client);
361
+ }
362
+ // Lists all users with their per-vhost permissions, plus the vhost list for
363
+ // the UI's vhost choices. Three management calls, joined here.
364
+ async listUsers(mqttServerId) {
365
+ const config = await this.requireConfig(mqttServerId);
366
+ const [rawUsers, rawPermissions, rawVhosts] = await Promise.all([
367
+ this.client.request(config, "GET", "/users"),
368
+ this.client.request(config, "GET", "/permissions"),
369
+ this.client.request(config, "GET", "/vhosts")
370
+ ]);
371
+ const permissionsByUser = /* @__PURE__ */ new Map();
372
+ for (const permission of rawPermissions ?? []) {
373
+ const list = permissionsByUser.get(permission.user) ?? [];
374
+ list.push({
375
+ vhost: permission.vhost,
376
+ configure: permission.configure,
377
+ write: permission.write,
378
+ read: permission.read
379
+ });
380
+ permissionsByUser.set(permission.user, list);
381
+ }
382
+ const users = (rawUsers ?? []).map((user) => ({
383
+ name: user.name,
384
+ tags: this.normalizeTags(user.tags),
385
+ permissions: permissionsByUser.get(user.name) ?? []
386
+ }));
387
+ users.sort((a, b) => a.name.localeCompare(b.name));
388
+ return {
389
+ mqttServerId,
390
+ users,
391
+ vhosts: (rawVhosts ?? []).map((vhost) => vhost.name).sort((a, b) => a.localeCompare(b))
392
+ };
393
+ }
394
+ // Creates or updates one user, optionally applying per-vhost permissions in
395
+ // the same operation (used by the create form's "default MQTT permissions").
396
+ async upsertUser(mqttServerId, username, dto) {
397
+ this.assertValidName(username, "Username");
398
+ const config = await this.requireConfig(mqttServerId);
399
+ const existing = await this.findUser(config, username);
400
+ const password = dto.password?.length ? dto.password : void 0;
401
+ if (!existing && !password) {
402
+ throw new import_common4.HttpException("Password is required when creating a new user.", import_common4.HttpStatus.BAD_REQUEST);
403
+ }
404
+ const body = { tags: (dto.tags ?? this.normalizeTags(existing?.tags)).join(",") };
405
+ if (password) {
406
+ body.password = password;
407
+ } else if (existing?.password_hash) {
408
+ body.password_hash = existing.password_hash;
409
+ if (existing.hashing_algorithm) {
410
+ body.hashing_algorithm = existing.hashing_algorithm;
411
+ }
412
+ }
413
+ await this.client.request(config, "PUT", `/users/${encodeURIComponent(username)}`, body);
414
+ for (const permission of dto.permissions ?? []) {
415
+ await this.putPermission(config, username, permission);
416
+ }
417
+ }
418
+ async deleteUser(mqttServerId, username) {
419
+ this.assertValidName(username, "Username");
420
+ const config = await this.requireConfig(mqttServerId);
421
+ if (config.username !== null && config.username === username) {
422
+ throw new import_common4.HttpException(
423
+ "Refusing to delete the user the MQTT server connection is configured with.",
424
+ import_common4.HttpStatus.BAD_REQUEST
425
+ );
426
+ }
427
+ await this.client.request(config, "DELETE", `/users/${encodeURIComponent(username)}`);
428
+ }
429
+ async setPermissions(mqttServerId, username, dto) {
430
+ this.assertValidName(username, "Username");
431
+ this.assertValidName(dto.vhost, "Vhost");
432
+ const config = await this.requireConfig(mqttServerId);
433
+ await this.putPermission(config, username, dto);
434
+ }
435
+ async clearPermissions(mqttServerId, username, vhost) {
436
+ this.assertValidName(username, "Username");
437
+ this.assertValidName(vhost, "Vhost");
438
+ const config = await this.requireConfig(mqttServerId);
439
+ await this.client.request(
440
+ config,
441
+ "DELETE",
442
+ `/permissions/${encodeURIComponent(vhost)}/${encodeURIComponent(username)}`
443
+ );
444
+ }
445
+ async putPermission(config, username, permission) {
446
+ await this.client.request(
447
+ config,
448
+ "PUT",
449
+ `/permissions/${encodeURIComponent(permission.vhost)}/${encodeURIComponent(username)}`,
450
+ {
451
+ configure: permission.configure ?? "",
452
+ write: permission.write ?? "",
453
+ read: permission.read ?? ""
454
+ }
455
+ );
456
+ }
457
+ // Reads one user; null when the broker doesn't know the name.
458
+ async findUser(config, username) {
459
+ try {
460
+ return await this.client.request(config, "GET", `/users/${encodeURIComponent(username)}`);
461
+ } catch (error) {
462
+ if (error instanceof import_common4.HttpException && error.getStatus() === import_common4.HttpStatus.NOT_FOUND) {
463
+ return null;
464
+ }
465
+ throw error;
466
+ }
467
+ }
468
+ async requireConfig(mqttServerId) {
469
+ const config = await this.context.getMqttServerConfig(mqttServerId);
470
+ if (!config) {
471
+ throw new import_common4.HttpException("MQTT server not found.", import_common4.HttpStatus.NOT_FOUND);
472
+ }
473
+ return config;
474
+ }
475
+ normalizeTags(tags) {
476
+ if (Array.isArray(tags)) {
477
+ return tags.filter((tag) => tag.length > 0);
478
+ }
479
+ if (typeof tags === "string" && tags.length > 0) {
480
+ return tags.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
481
+ }
482
+ return [];
483
+ }
484
+ assertValidName(value, label) {
485
+ if (typeof value !== "string" || value.trim().length === 0) {
486
+ throw new import_common4.HttpException(`${label} must not be empty.`, import_common4.HttpStatus.BAD_REQUEST);
487
+ }
488
+ if (value.length > 255) {
489
+ throw new import_common4.HttpException(`${label} must be at most 255 characters.`, import_common4.HttpStatus.BAD_REQUEST);
490
+ }
491
+ }
492
+ };
493
+ RabbitmqUsersService = __decorateClass([
494
+ (0, import_common4.Injectable)(),
495
+ __decorateParam(0, (0, import_common4.Inject)(PLUGIN_CONTEXT2)),
496
+ __decorateParam(1, (0, import_common4.Inject)(RabbitmqManagementClient))
497
+ ], RabbitmqUsersService);
498
+
499
+ // backend/rabbitmq-users.controller.ts
500
+ var RabbitmqUsersController = class {
501
+ // esbuild does not emit decorator metadata, so Nest cannot infer constructor
502
+ // types for injection — always inject by an explicit token.
503
+ constructor(users) {
504
+ __publicField(this, "users", users);
505
+ }
506
+ list(mqttServerId) {
507
+ return this.users.listUsers(mqttServerId);
508
+ }
509
+ upsert(mqttServerId, username, dto) {
510
+ return this.users.upsertUser(mqttServerId, username, dto ?? {});
511
+ }
512
+ remove(mqttServerId, username) {
513
+ return this.users.deleteUser(mqttServerId, username);
514
+ }
515
+ setPermissions(mqttServerId, username, dto) {
516
+ if (!dto || typeof dto.vhost !== "string") {
517
+ throw new import_common5.HttpException("A vhost is required.", import_common5.HttpStatus.BAD_REQUEST);
518
+ }
519
+ return this.users.setPermissions(mqttServerId, username, dto);
520
+ }
521
+ clearPermissions(mqttServerId, username, vhost) {
522
+ if (typeof vhost !== "string" || vhost.length === 0) {
523
+ throw new import_common5.HttpException("A vhost query parameter is required.", import_common5.HttpStatus.BAD_REQUEST);
524
+ }
525
+ return this.users.clearPermissions(mqttServerId, username, vhost);
526
+ }
527
+ };
528
+ __decorateClass([
529
+ (0, import_common5.Get)("users/:mqttServerId"),
530
+ __decorateParam(0, (0, import_common5.Param)("mqttServerId", import_common5.ParseIntPipe))
531
+ ], RabbitmqUsersController.prototype, "list", 1);
532
+ __decorateClass([
533
+ (0, import_common5.Put)("users/:mqttServerId/:username"),
534
+ __decorateParam(0, (0, import_common5.Param)("mqttServerId", import_common5.ParseIntPipe)),
535
+ __decorateParam(1, (0, import_common5.Param)("username")),
536
+ __decorateParam(2, (0, import_common5.Body)())
537
+ ], RabbitmqUsersController.prototype, "upsert", 1);
538
+ __decorateClass([
539
+ (0, import_common5.Delete)("users/:mqttServerId/:username"),
540
+ __decorateParam(0, (0, import_common5.Param)("mqttServerId", import_common5.ParseIntPipe)),
541
+ __decorateParam(1, (0, import_common5.Param)("username"))
542
+ ], RabbitmqUsersController.prototype, "remove", 1);
543
+ __decorateClass([
544
+ (0, import_common5.Put)("users/:mqttServerId/:username/permissions"),
545
+ __decorateParam(0, (0, import_common5.Param)("mqttServerId", import_common5.ParseIntPipe)),
546
+ __decorateParam(1, (0, import_common5.Param)("username")),
547
+ __decorateParam(2, (0, import_common5.Body)())
548
+ ], RabbitmqUsersController.prototype, "setPermissions", 1);
549
+ __decorateClass([
550
+ (0, import_common5.Delete)("users/:mqttServerId/:username/permissions"),
551
+ __decorateParam(0, (0, import_common5.Param)("mqttServerId", import_common5.ParseIntPipe)),
552
+ __decorateParam(1, (0, import_common5.Param)("username")),
553
+ __decorateParam(2, (0, import_common5.Query)("vhost"))
554
+ ], RabbitmqUsersController.prototype, "clearPermissions", 1);
555
+ RabbitmqUsersController = __decorateClass([
556
+ (0, import_plugins_backend_sdk2.Auth)("resources.update"),
557
+ (0, import_common5.Controller)("rabbitmq"),
558
+ __decorateParam(0, (0, import_common5.Inject)(RabbitmqUsersService))
559
+ ], RabbitmqUsersController);
560
+
561
+ // backend/rabbitmq-credential-provisioning.provider.ts
562
+ var import_crypto = require("crypto");
563
+ var import_common6 = require("@nestjs/common");
564
+
565
+ // ../../../node_modules/.pnpm/async-mutex@0.5.0/node_modules/async-mutex/index.mjs
566
+ var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
567
+ var E_ALREADY_LOCKED = new Error("mutex already locked");
568
+ var E_CANCELED = new Error("request for lock canceled");
569
+ var __awaiter$2 = function(thisArg, _arguments, P, generator) {
570
+ function adopt(value) {
571
+ return value instanceof P ? value : new P(function(resolve) {
572
+ resolve(value);
573
+ });
574
+ }
575
+ return new (P || (P = Promise))(function(resolve, reject) {
576
+ function fulfilled(value) {
577
+ try {
578
+ step(generator.next(value));
579
+ } catch (e) {
580
+ reject(e);
581
+ }
582
+ }
583
+ function rejected(value) {
584
+ try {
585
+ step(generator["throw"](value));
586
+ } catch (e) {
587
+ reject(e);
588
+ }
589
+ }
590
+ function step(result) {
591
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
592
+ }
593
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
594
+ });
595
+ };
596
+ var Semaphore = class {
597
+ constructor(_value, _cancelError = E_CANCELED) {
598
+ this._value = _value;
599
+ this._cancelError = _cancelError;
600
+ this._queue = [];
601
+ this._weightedWaiters = [];
602
+ }
603
+ acquire(weight = 1, priority = 0) {
604
+ if (weight <= 0)
605
+ throw new Error(`invalid weight ${weight}: must be positive`);
606
+ return new Promise((resolve, reject) => {
607
+ const task = { resolve, reject, weight, priority };
608
+ const i = findIndexFromEnd(this._queue, (other) => priority <= other.priority);
609
+ if (i === -1 && weight <= this._value) {
610
+ this._dispatchItem(task);
611
+ } else {
612
+ this._queue.splice(i + 1, 0, task);
613
+ }
614
+ });
615
+ }
616
+ runExclusive(callback_1) {
617
+ return __awaiter$2(this, arguments, void 0, function* (callback, weight = 1, priority = 0) {
618
+ const [value, release] = yield this.acquire(weight, priority);
619
+ try {
620
+ return yield callback(value);
621
+ } finally {
622
+ release();
623
+ }
624
+ });
625
+ }
626
+ waitForUnlock(weight = 1, priority = 0) {
627
+ if (weight <= 0)
628
+ throw new Error(`invalid weight ${weight}: must be positive`);
629
+ if (this._couldLockImmediately(weight, priority)) {
630
+ return Promise.resolve();
631
+ } else {
632
+ return new Promise((resolve) => {
633
+ if (!this._weightedWaiters[weight - 1])
634
+ this._weightedWaiters[weight - 1] = [];
635
+ insertSorted(this._weightedWaiters[weight - 1], { resolve, priority });
636
+ });
637
+ }
638
+ }
639
+ isLocked() {
640
+ return this._value <= 0;
641
+ }
642
+ getValue() {
643
+ return this._value;
644
+ }
645
+ setValue(value) {
646
+ this._value = value;
647
+ this._dispatchQueue();
648
+ }
649
+ release(weight = 1) {
650
+ if (weight <= 0)
651
+ throw new Error(`invalid weight ${weight}: must be positive`);
652
+ this._value += weight;
653
+ this._dispatchQueue();
654
+ }
655
+ cancel() {
656
+ this._queue.forEach((entry) => entry.reject(this._cancelError));
657
+ this._queue = [];
658
+ }
659
+ _dispatchQueue() {
660
+ this._drainUnlockWaiters();
661
+ while (this._queue.length > 0 && this._queue[0].weight <= this._value) {
662
+ this._dispatchItem(this._queue.shift());
663
+ this._drainUnlockWaiters();
664
+ }
665
+ }
666
+ _dispatchItem(item) {
667
+ const previousValue = this._value;
668
+ this._value -= item.weight;
669
+ item.resolve([previousValue, this._newReleaser(item.weight)]);
670
+ }
671
+ _newReleaser(weight) {
672
+ let called = false;
673
+ return () => {
674
+ if (called)
675
+ return;
676
+ called = true;
677
+ this.release(weight);
678
+ };
679
+ }
680
+ _drainUnlockWaiters() {
681
+ if (this._queue.length === 0) {
682
+ for (let weight = this._value; weight > 0; weight--) {
683
+ const waiters = this._weightedWaiters[weight - 1];
684
+ if (!waiters)
685
+ continue;
686
+ waiters.forEach((waiter) => waiter.resolve());
687
+ this._weightedWaiters[weight - 1] = [];
688
+ }
689
+ } else {
690
+ const queuedPriority = this._queue[0].priority;
691
+ for (let weight = this._value; weight > 0; weight--) {
692
+ const waiters = this._weightedWaiters[weight - 1];
693
+ if (!waiters)
694
+ continue;
695
+ const i = waiters.findIndex((waiter) => waiter.priority <= queuedPriority);
696
+ (i === -1 ? waiters : waiters.splice(0, i)).forEach(((waiter) => waiter.resolve()));
697
+ }
698
+ }
699
+ }
700
+ _couldLockImmediately(weight, priority) {
701
+ return (this._queue.length === 0 || this._queue[0].priority < priority) && weight <= this._value;
702
+ }
703
+ };
704
+ function insertSorted(a, v) {
705
+ const i = findIndexFromEnd(a, (other) => v.priority <= other.priority);
706
+ a.splice(i + 1, 0, v);
707
+ }
708
+ function findIndexFromEnd(a, predicate) {
709
+ for (let i = a.length - 1; i >= 0; i--) {
710
+ if (predicate(a[i])) {
711
+ return i;
712
+ }
713
+ }
714
+ return -1;
715
+ }
716
+ var __awaiter$1 = function(thisArg, _arguments, P, generator) {
717
+ function adopt(value) {
718
+ return value instanceof P ? value : new P(function(resolve) {
719
+ resolve(value);
720
+ });
721
+ }
722
+ return new (P || (P = Promise))(function(resolve, reject) {
723
+ function fulfilled(value) {
724
+ try {
725
+ step(generator.next(value));
726
+ } catch (e) {
727
+ reject(e);
728
+ }
729
+ }
730
+ function rejected(value) {
731
+ try {
732
+ step(generator["throw"](value));
733
+ } catch (e) {
734
+ reject(e);
735
+ }
736
+ }
737
+ function step(result) {
738
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
739
+ }
740
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
741
+ });
742
+ };
743
+ var Mutex = class {
744
+ constructor(cancelError) {
745
+ this._semaphore = new Semaphore(1, cancelError);
746
+ }
747
+ acquire() {
748
+ return __awaiter$1(this, arguments, void 0, function* (priority = 0) {
749
+ const [, releaser] = yield this._semaphore.acquire(1, priority);
750
+ return releaser;
751
+ });
752
+ }
753
+ runExclusive(callback, priority = 0) {
754
+ return this._semaphore.runExclusive(() => callback(), 1, priority);
755
+ }
756
+ isLocked() {
757
+ return this._semaphore.isLocked();
758
+ }
759
+ waitForUnlock(priority = 0) {
760
+ return this._semaphore.waitForUnlock(1, priority);
761
+ }
762
+ release() {
763
+ if (this._semaphore.isLocked())
764
+ this._semaphore.release();
765
+ }
766
+ cancel() {
767
+ return this._semaphore.cancel();
768
+ }
769
+ };
770
+
771
+ // backend/rabbitmq-credential-provisioning.provider.ts
772
+ var _RabbitmqCredentialProvisioningProvider = class _RabbitmqCredentialProvisioningProvider {
773
+ constructor(context) {
774
+ __publicField(this, "context", context);
775
+ __publicField(this, "id", "rabbitmq");
776
+ __publicField(this, "displayName", "RabbitMQ Management API");
777
+ __publicField(this, "client", new RabbitmqManagementClient());
778
+ __publicField(this, "detection");
779
+ this.detection = new RabbitmqDetectionService(context);
780
+ }
781
+ async supports(config) {
782
+ return (await this.detection.detect(config.id)).isRabbitMQ;
783
+ }
784
+ provision(request) {
785
+ return this.writeCredential(request);
786
+ }
787
+ rotate(request) {
788
+ return this.writeCredential(request);
789
+ }
790
+ async revoke(request) {
791
+ this.assertName(request.username, "Username");
792
+ const config = await this.requireConfig(request.mqttServerId);
793
+ this.assertNotManagementUser(config, request.username);
794
+ try {
795
+ await this.client.request(config, "DELETE", `/users/${encodeURIComponent(request.username)}`);
796
+ } catch (error) {
797
+ if (error instanceof import_common6.HttpException && error.getStatus() === import_common6.HttpStatus.NOT_FOUND) return;
798
+ throw error;
799
+ }
800
+ }
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}`;
806
+ let lock = _RabbitmqCredentialProvisioningProvider.vhostLocks.get(lockKey);
807
+ if (!lock) {
808
+ lock = { mutex: new Mutex(), users: 0 };
809
+ _RabbitmqCredentialProvisioningProvider.vhostLocks.set(lockKey, lock);
810
+ }
811
+ lock.users += 1;
812
+ try {
813
+ return await lock.mutex.runExclusive(() => this.writeCredentialLocked(request, config));
814
+ } finally {
815
+ lock.users -= 1;
816
+ if (lock.users === 0 && _RabbitmqCredentialProvisioningProvider.vhostLocks.get(lockKey) === lock) {
817
+ _RabbitmqCredentialProvisioningProvider.vhostLocks.delete(lockKey);
818
+ }
819
+ }
820
+ }
821
+ async writeCredentialLocked(request, config) {
822
+ const password = (0, import_crypto.randomBytes)(24).toString("base64url");
823
+ const vhost = encodeURIComponent(request.vhost);
824
+ const username = encodeURIComponent(request.username);
825
+ const existing = await this.userExists(config, username);
826
+ const vhostExisted = await this.exists(config, `/vhosts/${vhost}`);
827
+ await this.client.request(config, "PUT", `/vhosts/${vhost}`);
828
+ let previousPermissions = null;
829
+ let previousTopicPermissions = null;
830
+ let permissionsCaptured = false;
831
+ try {
832
+ if (existing) {
833
+ previousPermissions = await this.getPermissions(config, vhost, username);
834
+ previousTopicPermissions = await this.getTopicPermissions(config, vhost, username);
835
+ permissionsCaptured = true;
836
+ await this.writePermissions(config, request, vhost, username);
837
+ await this.client.request(config, "PUT", `/users/${username}`, { password, tags: "" });
838
+ } else {
839
+ await this.client.request(config, "PUT", `/users/${username}`, { password, tags: "" });
840
+ await this.writePermissions(config, request, vhost, username);
841
+ }
842
+ } catch (error) {
843
+ const rollback = [
844
+ ...existing && permissionsCaptured ? [() => this.restorePermissions(config, vhost, username, previousPermissions, previousTopicPermissions)] : !existing ? [() => this.client.request(config, "DELETE", `/users/${username}`)] : [],
845
+ ...!vhostExisted ? [() => this.client.request(config, "DELETE", `/vhosts/${vhost}`)] : []
846
+ ];
847
+ await this.failAfterRollback(error, rollback);
848
+ }
849
+ return {
850
+ providerId: this.id,
851
+ identity: request.identity,
852
+ username: request.username,
853
+ vhost: request.vhost,
854
+ password
855
+ };
856
+ }
857
+ async writePermissions(config, request, vhost, username) {
858
+ const subscriptionQueue = `mqtt-subscription-${escapeRegex(request.identity)}.*`;
859
+ await this.client.request(config, "PUT", `/permissions/${vhost}/${username}`, {
860
+ configure: `^${subscriptionQueue}$`,
861
+ write: `^(amq\\.topic|${subscriptionQueue})$`,
862
+ read: `^(amq\\.topic|${subscriptionQueue})$`
863
+ });
864
+ await this.client.request(config, "PUT", `/topic-permissions/${vhost}/${username}`, {
865
+ exchange: "amq.topic",
866
+ write: mqttFiltersToRegex(request.topicPolicy.publish),
867
+ read: mqttFiltersToRegex(request.topicPolicy.subscribe)
868
+ });
869
+ }
870
+ async userExists(config, username) {
871
+ return this.exists(config, `/users/${username}`);
872
+ }
873
+ async exists(config, path) {
874
+ try {
875
+ await this.client.request(config, "GET", path);
876
+ return true;
877
+ } catch (error) {
878
+ if (error instanceof import_common6.HttpException && error.getStatus() === import_common6.HttpStatus.NOT_FOUND) {
879
+ return false;
880
+ }
881
+ throw error;
882
+ }
883
+ }
884
+ async getPermissions(config, vhost, username) {
885
+ return this.getOptional(config, `/permissions/${vhost}/${username}`);
886
+ }
887
+ async getTopicPermissions(config, vhost, username) {
888
+ return this.getOptional(config, `/topic-permissions/${vhost}/${username}`);
889
+ }
890
+ async getOptional(config, path) {
891
+ try {
892
+ return await this.client.request(config, "GET", path);
893
+ } catch (error) {
894
+ if (error instanceof import_common6.HttpException && error.getStatus() === import_common6.HttpStatus.NOT_FOUND) {
895
+ return null;
896
+ }
897
+ throw error;
898
+ }
899
+ }
900
+ async restorePermissions(config, vhost, username, permissions, topicPermissions) {
901
+ const results = await Promise.allSettled([
902
+ this.restorePermission(config, `/permissions/${vhost}/${username}`, permissions),
903
+ this.restorePermission(config, `/topic-permissions/${vhost}/${username}`, topicPermissions)
904
+ ]);
905
+ const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
906
+ if (failures.length > 0) {
907
+ throw new AggregateError(failures, "Failed to restore RabbitMQ permissions.");
908
+ }
909
+ }
910
+ async restorePermission(config, path, permissions) {
911
+ await this.client.request(config, permissions ? "PUT" : "DELETE", path, permissions ?? void 0);
912
+ }
913
+ async failAfterRollback(error, rollback) {
914
+ const results = await Promise.allSettled(rollback.map((operation) => operation()));
915
+ const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
916
+ if (failures.length > 0) {
917
+ throw new AggregateError([error, ...failures], "Credential provisioning failed and rollback was incomplete.");
918
+ }
919
+ throw error;
920
+ }
921
+ async requireConfig(mqttServerId) {
922
+ const config = await this.context.getMqttServerConfig(mqttServerId);
923
+ if (!config) {
924
+ throw new import_common6.BadRequestException("MQTT server not found.");
925
+ }
926
+ return config;
927
+ }
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)) {
933
+ throw new import_common6.BadRequestException("Publish and subscribe topic policies must be arrays.");
934
+ }
935
+ for (const filter of [...request.topicPolicy.publish, ...request.topicPolicy.subscribe]) {
936
+ if (typeof filter !== "string" || filter.length === 0 || filter.length > 1024) {
937
+ throw new import_common6.BadRequestException("MQTT topic filters must be non-empty strings no longer than 1024 characters.");
938
+ }
939
+ this.assertTopicFilter(filter);
940
+ }
941
+ }
942
+ assertTopicFilter(filter) {
943
+ if (filter.includes(".")) {
944
+ throw new import_common6.BadRequestException("RabbitMQ MQTT topic policies cannot contain dots.");
945
+ }
946
+ const segments = filter.split("/");
947
+ for (const [index, segment] of segments.entries()) {
948
+ if (segment.includes("+") && segment !== "+" || segment.includes("#") && (segment !== "#" || index !== segments.length - 1)) {
949
+ throw new import_common6.BadRequestException(
950
+ "MQTT wildcards must occupy a complete topic level, and # must be the final level."
951
+ );
952
+ }
953
+ }
954
+ }
955
+ assertName(value, label) {
956
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > 255) {
957
+ throw new import_common6.BadRequestException(`${label} must be a non-empty string no longer than 255 characters.`);
958
+ }
959
+ }
960
+ assertNotManagementUser(config, username) {
961
+ if (config.username === username) {
962
+ throw new import_common6.BadRequestException("Refusing to modify the MQTT server management identity.");
963
+ }
964
+ }
965
+ };
966
+ __publicField(_RabbitmqCredentialProvisioningProvider, "vhostLocks", /* @__PURE__ */ new Map());
967
+ var RabbitmqCredentialProvisioningProvider = _RabbitmqCredentialProvisioningProvider;
968
+ function mqttFiltersToRegex(filters) {
969
+ if (filters.length === 0) {
970
+ return "$(?!)";
971
+ }
972
+ return `^(?:${filters.map(mqttFilterToRegex).join("|")})$`;
973
+ }
974
+ function mqttFilterToRegex(filter) {
975
+ const terminalMultiLevel = filter.endsWith("/#");
976
+ const prefix = terminalMultiLevel ? filter.slice(0, -2) : filter;
977
+ const translated = prefix.split("/").map((segment) => {
978
+ if (segment === "+") {
979
+ return "[^.]+";
980
+ }
981
+ if (segment === "#") {
982
+ return ".*";
983
+ }
984
+ return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
985
+ }).join("\\.");
986
+ return terminalMultiLevel ? `${translated}(?:\\..*)?` : translated;
987
+ }
988
+ function escapeRegex(value) {
989
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
990
+ }
991
+
992
+ // backend/plugin.ts
993
+ var PLUGIN_CONTEXT3 = /* @__PURE__ */ Symbol.for("attraccess.plugin.context");
994
+ var RabbitmqService = class {
995
+ constructor(context) {
996
+ __publicField(this, "context", context);
997
+ }
998
+ onModuleInit() {
999
+ this.context.logger.log("RabbitMQ plugin loaded \u2014 MQTT-server RabbitMQ detection active.");
1000
+ }
1001
+ getStatus() {
1002
+ return { plugin: "rabbitmq", status: "ok" };
1003
+ }
1004
+ };
1005
+ RabbitmqService = __decorateClass([
1006
+ (0, import_common7.Injectable)(),
1007
+ __decorateParam(0, (0, import_common7.Inject)(PLUGIN_CONTEXT3))
1008
+ ], RabbitmqService);
1009
+ var RabbitmqController = class {
1010
+ // esbuild does not emit decorator metadata, so Nest cannot infer constructor
1011
+ // types for injection — always inject by an explicit token.
1012
+ constructor(service) {
1013
+ __publicField(this, "service", service);
1014
+ }
1015
+ status() {
1016
+ return this.service.getStatus();
1017
+ }
1018
+ };
1019
+ __decorateClass([
1020
+ (0, import_common7.Get)("status")
1021
+ ], RabbitmqController.prototype, "status", 1);
1022
+ RabbitmqController = __decorateClass([
1023
+ (0, import_plugins_backend_sdk3.Auth)("resources.update"),
1024
+ (0, import_common7.Controller)("rabbitmq"),
1025
+ __decorateParam(0, (0, import_common7.Inject)(RabbitmqService))
1026
+ ], RabbitmqController);
1027
+ var RabbitmqPluginModule = class {
1028
+ };
1029
+ var plugin = {
1030
+ credentialProvisioningProvider(context) {
1031
+ return new RabbitmqCredentialProvisioningProvider(context);
1032
+ },
1033
+ register(context) {
1034
+ return {
1035
+ module: RabbitmqPluginModule,
1036
+ controllers: [RabbitmqController, RabbitmqDetectionController, RabbitmqUsersController],
1037
+ providers: [
1038
+ { provide: PLUGIN_CONTEXT3, useValue: context },
1039
+ RabbitmqService,
1040
+ RabbitmqDetectionService,
1041
+ RabbitmqManagementClient,
1042
+ RabbitmqUsersService
1043
+ ]
1044
+ };
1045
+ }
1046
+ };
1047
+ var plugin_default = plugin;