acl-next 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,667 @@
1
+ // src/middleware.ts
2
+ var HttpError = class extends Error {
3
+ errorCode;
4
+ name = "HttpError";
5
+ constructor(errorCode, message) {
6
+ super(message);
7
+ this.errorCode = errorCode;
8
+ }
9
+ };
10
+ function aclMiddleware(acl, numPathComponents, userId, actions) {
11
+ return (req, res, next) => {
12
+ var _a, _b, _c;
13
+ let resolvedUserId;
14
+ if (typeof userId === "function") {
15
+ resolvedUserId = userId(req, res);
16
+ } else if (userId !== void 0) {
17
+ resolvedUserId = userId;
18
+ } else {
19
+ resolvedUserId = ((_a = req.session) == null ? void 0 : _a.userId) ?? ((_b = req.user) == null ? void 0 : _b.id);
20
+ }
21
+ if (resolvedUserId === void 0 || resolvedUserId === null) {
22
+ next(new HttpError(401, "User not authenticated"));
23
+ return;
24
+ }
25
+ const fullUrl = (req.originalUrl ?? req.url ?? "").split("?")[0] ?? "";
26
+ const resource = numPathComponents ? fullUrl.split("/").slice(0, numPathComponents + 1).join("/") : fullUrl;
27
+ const resolvedActions = actions ?? req.method.toLowerCase();
28
+ (_c = acl.logger) == null ? void 0 : _c.debug(`Requesting ${resolvedActions} on ${resource} by user ${resolvedUserId}`);
29
+ acl.isAllowed(resolvedUserId, resource, resolvedActions).then(
30
+ (allowed) => {
31
+ var _a2, _b2;
32
+ if (allowed) {
33
+ (_a2 = acl.logger) == null ? void 0 : _a2.debug(`Allowed ${resolvedActions} on ${resource} by user ${resolvedUserId}`);
34
+ next();
35
+ } else {
36
+ (_b2 = acl.logger) == null ? void 0 : _b2.debug(
37
+ `Not allowed ${resolvedActions} on ${resource} by user ${resolvedUserId}`
38
+ );
39
+ next(new HttpError(403, "Insufficient permissions to access resource"));
40
+ }
41
+ },
42
+ () => next(new Error("Error checking permissions to access resource"))
43
+ );
44
+ };
45
+ }
46
+ function aclErrorHandler(contentType) {
47
+ return (err, _req, res, next) => {
48
+ if (!(err instanceof HttpError) || !err.errorCode) {
49
+ next(err);
50
+ return;
51
+ }
52
+ const response = res.status(err.errorCode);
53
+ if (contentType === "json") {
54
+ response.json({ message: err.message });
55
+ } else if (contentType === "html") {
56
+ response.send(err.message);
57
+ } else {
58
+ response.end(err.message);
59
+ }
60
+ };
61
+ }
62
+
63
+ // src/acl.ts
64
+ var DEFAULT_BUCKETS = {
65
+ meta: "meta",
66
+ parents: "parents",
67
+ permissions: "permissions",
68
+ resources: "resources",
69
+ roles: "roles",
70
+ users: "users"
71
+ };
72
+ var toArray = (value) => Array.isArray(value) ? value : [value];
73
+ var union = (a, b) => [.../* @__PURE__ */ new Set([...a, ...b])];
74
+ var allowsBucket = (resource) => `allows_${resource}`;
75
+ var keyFromAllowsBucket = (bucket) => bucket.replace(/^allows_/, "");
76
+ var Acl = class {
77
+ backend;
78
+ logger;
79
+ buckets;
80
+ constructor(backend, logger, options) {
81
+ this.backend = backend;
82
+ this.logger = logger;
83
+ this.buckets = { ...DEFAULT_BUCKETS, ...options == null ? void 0 : options.buckets };
84
+ }
85
+ /** Adds roles to a given user id. */
86
+ async addUserRoles(userId, roles) {
87
+ const transaction = this.backend.begin();
88
+ this.backend.add(transaction, this.buckets.meta, "users", userId);
89
+ this.backend.add(transaction, this.buckets.users, userId, roles);
90
+ for (const role of toArray(roles)) {
91
+ this.backend.add(transaction, this.buckets.roles, role, userId);
92
+ }
93
+ await this.backend.end(transaction);
94
+ }
95
+ /** Removes roles from a given user id. */
96
+ async removeUserRoles(userId, roles) {
97
+ const transaction = this.backend.begin();
98
+ this.backend.remove(transaction, this.buckets.users, userId, roles);
99
+ for (const role of toArray(roles)) {
100
+ this.backend.remove(transaction, this.buckets.roles, role, userId);
101
+ }
102
+ await this.backend.end(transaction);
103
+ }
104
+ /** Returns all the roles assigned to a given user id. */
105
+ userRoles(userId) {
106
+ return this.backend.get(this.buckets.users, userId);
107
+ }
108
+ /** Returns all users that have the given role. */
109
+ roleUsers(roleName) {
110
+ return this.backend.get(this.buckets.roles, roleName);
111
+ }
112
+ /** Returns whether the user has the given role. */
113
+ async hasRole(userId, role) {
114
+ const roles = await this.userRoles(userId);
115
+ return roles.includes(role);
116
+ }
117
+ /** Adds one or more parent roles to a role. */
118
+ async addRoleParents(role, parents) {
119
+ const transaction = this.backend.begin();
120
+ this.backend.add(transaction, this.buckets.meta, "roles", role);
121
+ this.backend.add(transaction, this.buckets.parents, role, parents);
122
+ await this.backend.end(transaction);
123
+ }
124
+ /** Removes parent role(s) from a role. Omit `parents` to remove all of them. */
125
+ async removeRoleParents(role, parents) {
126
+ const transaction = this.backend.begin();
127
+ if (parents !== void 0) {
128
+ this.backend.remove(transaction, this.buckets.parents, role, parents);
129
+ } else {
130
+ this.backend.del(transaction, this.buckets.parents, role);
131
+ }
132
+ await this.backend.end(transaction);
133
+ }
134
+ /** Removes a role from the system, including all its permissions. */
135
+ async removeRole(role) {
136
+ const resources = await this.backend.get(this.buckets.resources, role);
137
+ const transaction = this.backend.begin();
138
+ for (const resource of resources) {
139
+ this.backend.del(transaction, allowsBucket(resource), role);
140
+ }
141
+ this.backend.del(transaction, this.buckets.resources, role);
142
+ this.backend.del(transaction, this.buckets.parents, role);
143
+ this.backend.del(transaction, this.buckets.roles, role);
144
+ this.backend.remove(transaction, this.buckets.meta, "roles", role);
145
+ await this.backend.end(transaction);
146
+ }
147
+ /** Removes a resource from the system. */
148
+ async removeResource(resource) {
149
+ const roles = await this.backend.get(this.buckets.meta, "roles");
150
+ const transaction = this.backend.begin();
151
+ this.backend.del(transaction, allowsBucket(resource), roles);
152
+ for (const role of roles) {
153
+ this.backend.remove(transaction, this.buckets.resources, role, resource);
154
+ }
155
+ await this.backend.end(transaction);
156
+ }
157
+ async allow(roles, resources, permissions) {
158
+ if (resources === void 0) {
159
+ return this.allowEx(roles);
160
+ }
161
+ const rolesArr = toArray(roles);
162
+ const resourcesArr = toArray(resources);
163
+ const transaction = this.backend.begin();
164
+ this.backend.add(transaction, this.buckets.meta, "roles", rolesArr);
165
+ for (const resource of resourcesArr) {
166
+ for (const role of rolesArr) {
167
+ this.backend.add(
168
+ transaction,
169
+ allowsBucket(resource),
170
+ role,
171
+ permissions
172
+ );
173
+ }
174
+ }
175
+ for (const role of rolesArr) {
176
+ this.backend.add(transaction, this.buckets.resources, role, resourcesArr);
177
+ }
178
+ await this.backend.end(transaction);
179
+ }
180
+ /** Removes permissions from a role over resources. Omit `permissions` to remove all. */
181
+ removeAllow(role, resources, permissions) {
182
+ return this.removePermissions(
183
+ role,
184
+ toArray(resources),
185
+ permissions !== void 0 ? toArray(permissions) : null
186
+ );
187
+ }
188
+ /**
189
+ * Removes permissions from a role over the given resources. When
190
+ * `permissions` is null the resource is fully revoked for the role.
191
+ *
192
+ * Note: loses atomicity when pruning emptied role/resource links.
193
+ */
194
+ async removePermissions(role, resources, permissions) {
195
+ const transaction = this.backend.begin();
196
+ for (const resource of resources) {
197
+ const bucket = allowsBucket(resource);
198
+ if (permissions) {
199
+ this.backend.remove(transaction, bucket, role, permissions);
200
+ } else {
201
+ this.backend.del(transaction, bucket, role);
202
+ this.backend.remove(transaction, this.buckets.resources, role, resource);
203
+ }
204
+ }
205
+ await this.backend.end(transaction);
206
+ const cleanup = this.backend.begin();
207
+ await Promise.all(
208
+ resources.map(async (resource) => {
209
+ const remaining = await this.backend.get(allowsBucket(resource), role);
210
+ if (remaining.length === 0) {
211
+ this.backend.remove(cleanup, this.buckets.resources, role, resource);
212
+ }
213
+ })
214
+ );
215
+ await this.backend.end(cleanup);
216
+ }
217
+ /**
218
+ * Returns, per resource, the permissions a user has. Uses the backend's
219
+ * `unions` optimization when available.
220
+ */
221
+ async allowedPermissions(userId, resources) {
222
+ if (!userId) {
223
+ return {};
224
+ }
225
+ if (this.backend.unions) {
226
+ return this.optimizedAllowedPermissions(userId, resources);
227
+ }
228
+ const resourcesArr = toArray(resources);
229
+ const roles = await this.userRoles(userId);
230
+ const result = {};
231
+ await Promise.all(
232
+ resourcesArr.map(async (resource) => {
233
+ result[resource] = await this.resourcePermissions(roles, resource);
234
+ })
235
+ );
236
+ return result;
237
+ }
238
+ /** `allowedPermissions` variant using the backend `unions` bulk query. */
239
+ async optimizedAllowedPermissions(userId, resources) {
240
+ if (!userId) {
241
+ return {};
242
+ }
243
+ const resourcesArr = toArray(resources);
244
+ const roles = await this.allUserRoles(userId);
245
+ const buckets = resourcesArr.map(allowsBucket);
246
+ const response = roles.length === 0 ? Object.fromEntries(buckets.map((bucket) => [bucket, []])) : (
247
+ // biome-ignore lint/style/noNonNullAssertion: guarded by the `this.backend.unions` caller
248
+ await this.backend.unions(buckets, roles)
249
+ );
250
+ const result = {};
251
+ for (const bucket of Object.keys(response)) {
252
+ result[keyFromAllowsBucket(bucket)] = response[bucket] ?? [];
253
+ }
254
+ return result;
255
+ }
256
+ /** Checks if a user is allowed all of the given permissions on a resource. */
257
+ async isAllowed(userId, resource, permissions) {
258
+ const roles = await this.backend.get(this.buckets.users, userId);
259
+ if (roles.length) {
260
+ return this.areAnyRolesAllowed(roles, resource, permissions);
261
+ }
262
+ return false;
263
+ }
264
+ /** Returns true if any of the roles has all of the given permissions. */
265
+ areAnyRolesAllowed(roles, resource, permissions) {
266
+ const rolesArr = toArray(roles);
267
+ const permsArr = toArray(permissions);
268
+ if (rolesArr.length === 0) {
269
+ return Promise.resolve(false);
270
+ }
271
+ return this.checkPermissions(rolesArr, resource, permsArr);
272
+ }
273
+ whatResources(roles, permissions) {
274
+ const rolesArr = toArray(roles);
275
+ const perms = permissions === void 0 ? void 0 : toArray(permissions);
276
+ return this.permittedResources(rolesArr, perms);
277
+ }
278
+ /** Backing implementation for {@link whatResources}. */
279
+ async permittedResources(roles, permissions) {
280
+ const rolesArr = toArray(roles);
281
+ const resources = await this.rolesResources(rolesArr);
282
+ if (permissions === void 0) {
283
+ const result2 = {};
284
+ await Promise.all(
285
+ resources.map(async (resource) => {
286
+ result2[resource] = await this.resourcePermissions(rolesArr, resource);
287
+ })
288
+ );
289
+ return result2;
290
+ }
291
+ const result = [];
292
+ await Promise.all(
293
+ resources.map(async (resource) => {
294
+ const p = await this.resourcePermissions(rolesArr, resource);
295
+ if (permissions.some((perm) => p.includes(perm))) {
296
+ result.push(resource);
297
+ }
298
+ })
299
+ );
300
+ return result;
301
+ }
302
+ /**
303
+ * Express-style middleware that authorizes the current request against this
304
+ * Acl. See {@link aclMiddleware} for parameter semantics. Pair with
305
+ * {@link aclErrorHandler} to render the resulting 401/403 errors.
306
+ */
307
+ middleware(numPathComponents, userId, actions) {
308
+ return aclMiddleware(this, numPathComponents, userId, actions);
309
+ }
310
+ // ---------------------------------------------------------------------------
311
+ // Private helpers
312
+ // ---------------------------------------------------------------------------
313
+ /** Compact array form of {@link allow}. */
314
+ async allowEx(rules) {
315
+ const demuxed = [];
316
+ for (const rule of toArray(rules)) {
317
+ for (const a of rule.allows) {
318
+ demuxed.push({ roles: rule.roles, resources: a.resources, permissions: a.permissions });
319
+ }
320
+ }
321
+ for (const d of demuxed) {
322
+ await this.allow(d.roles, d.resources, d.permissions);
323
+ }
324
+ }
325
+ /** Direct parents of the given roles. */
326
+ rolesParents(roles) {
327
+ return this.backend.union(this.buckets.parents, roles);
328
+ }
329
+ /** All roles in the hierarchy, including the given roles. */
330
+ async allRoles(roleNames) {
331
+ const parents = await this.rolesParents(roleNames);
332
+ if (parents.length > 0) {
333
+ const parentRoles = await this.allRoles(parents);
334
+ return union(roleNames, parentRoles);
335
+ }
336
+ return roleNames;
337
+ }
338
+ /** All roles in the hierarchy of the given user. */
339
+ async allUserRoles(userId) {
340
+ const roles = await this.userRoles(userId);
341
+ if (roles && roles.length > 0) {
342
+ return this.allRoles(roles);
343
+ }
344
+ return [];
345
+ }
346
+ /** All resources reachable by the given roles (through the hierarchy). */
347
+ async rolesResources(roles) {
348
+ const allRoles = await this.allRoles(toArray(roles));
349
+ const result = [];
350
+ await Promise.all(
351
+ allRoles.map(async (role) => {
352
+ const resources = await this.backend.get(this.buckets.resources, role);
353
+ result.push(...resources);
354
+ })
355
+ );
356
+ return result;
357
+ }
358
+ /** Permissions the given roles (and their parents) have over a resource. */
359
+ async resourcePermissions(roles, resource) {
360
+ if (roles.length === 0) {
361
+ return [];
362
+ }
363
+ const resourcePermissions = await this.backend.union(allowsBucket(resource), roles);
364
+ const parents = await this.rolesParents(roles);
365
+ if (parents == null ? void 0 : parents.length) {
366
+ const morePermissions = await this.resourcePermissions(parents, resource);
367
+ return union(resourcePermissions, morePermissions);
368
+ }
369
+ return resourcePermissions;
370
+ }
371
+ /**
372
+ * Whether the roles (and their parents) satisfy all permissions on a resource.
373
+ *
374
+ * NOTE: does not handle circular role hierarchies.
375
+ */
376
+ async checkPermissions(roles, resource, permissions) {
377
+ const resourcePermissions = await this.backend.union(allowsBucket(resource), roles);
378
+ if (resourcePermissions.includes("*")) {
379
+ return true;
380
+ }
381
+ const remaining = permissions.filter((p) => !resourcePermissions.includes(p));
382
+ if (remaining.length === 0) {
383
+ return true;
384
+ }
385
+ const parents = await this.backend.union(this.buckets.parents, roles);
386
+ if (parents == null ? void 0 : parents.length) {
387
+ return this.checkPermissions(parents, resource, remaining);
388
+ }
389
+ return false;
390
+ }
391
+ };
392
+
393
+ // src/backends/memory.ts
394
+ var toArray2 = (value) => Array.isArray(value) ? value : [value];
395
+ var toStr = (value) => `${value}`;
396
+ var MemoryBackend = class {
397
+ buckets = /* @__PURE__ */ new Map();
398
+ begin() {
399
+ return [];
400
+ }
401
+ async end(transaction) {
402
+ for (const mutation of transaction) {
403
+ mutation();
404
+ }
405
+ }
406
+ async clean() {
407
+ this.buckets.clear();
408
+ }
409
+ async get(bucket, key) {
410
+ var _a;
411
+ const values = (_a = this.buckets.get(bucket)) == null ? void 0 : _a.get(toStr(key));
412
+ return values ? [...values] : [];
413
+ }
414
+ async unions(buckets, keys) {
415
+ const keyStrs = keys.map(toStr);
416
+ const result = {};
417
+ for (const bucket of buckets) {
418
+ const store = this.buckets.get(bucket);
419
+ if (!store) {
420
+ result[bucket] = [];
421
+ continue;
422
+ }
423
+ const union2 = /* @__PURE__ */ new Set();
424
+ for (const key of keyStrs) {
425
+ for (const value of store.get(key) ?? []) {
426
+ union2.add(value);
427
+ }
428
+ }
429
+ result[bucket] = [...union2];
430
+ }
431
+ return result;
432
+ }
433
+ async union(bucket, keys) {
434
+ let store = this.buckets.get(bucket);
435
+ if (!store) {
436
+ for (const name of this.buckets.keys()) {
437
+ if (new RegExp(`^${name}$`).test(bucket)) {
438
+ store = this.buckets.get(name);
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ if (!store) {
444
+ return [];
445
+ }
446
+ const union2 = /* @__PURE__ */ new Set();
447
+ for (const key of keys) {
448
+ for (const value of store.get(toStr(key)) ?? []) {
449
+ union2.add(value);
450
+ }
451
+ }
452
+ return [...union2];
453
+ }
454
+ add(transaction, bucket, key, values) {
455
+ const keyStr = toStr(key);
456
+ const valueStrs = toArray2(values).map(toStr);
457
+ transaction.push(() => {
458
+ let store = this.buckets.get(bucket);
459
+ if (!store) {
460
+ store = /* @__PURE__ */ new Map();
461
+ this.buckets.set(bucket, store);
462
+ }
463
+ const existing = store.get(keyStr);
464
+ store.set(
465
+ keyStr,
466
+ existing ? [.../* @__PURE__ */ new Set([...valueStrs, ...existing])] : [...new Set(valueStrs)]
467
+ );
468
+ });
469
+ }
470
+ del(transaction, bucket, keys) {
471
+ const keyStrs = toArray2(keys).map(toStr);
472
+ transaction.push(() => {
473
+ const store = this.buckets.get(bucket);
474
+ if (!store) {
475
+ return;
476
+ }
477
+ for (const key of keyStrs) {
478
+ store.delete(key);
479
+ }
480
+ });
481
+ }
482
+ remove(transaction, bucket, key, values) {
483
+ const keyStr = toStr(key);
484
+ const toRemove = new Set(toArray2(values).map(toStr));
485
+ transaction.push(() => {
486
+ var _a, _b;
487
+ const existing = (_a = this.buckets.get(bucket)) == null ? void 0 : _a.get(keyStr);
488
+ if (existing) {
489
+ (_b = this.buckets.get(bucket)) == null ? void 0 : _b.set(
490
+ keyStr,
491
+ existing.filter((value) => !toRemove.has(value))
492
+ );
493
+ }
494
+ });
495
+ }
496
+ };
497
+
498
+ // src/backends/redis.ts
499
+ var toArray3 = (value) => Array.isArray(value) ? value : [value];
500
+ var toStr2 = (value) => `${value}`;
501
+ var RedisBackend = class {
502
+ redis;
503
+ prefix;
504
+ constructor(redis, prefix = "acl") {
505
+ this.redis = redis;
506
+ this.prefix = prefix;
507
+ }
508
+ begin() {
509
+ return this.redis.multi();
510
+ }
511
+ async end(transaction) {
512
+ await transaction.exec();
513
+ }
514
+ async clean() {
515
+ const keys = await this.redis.keys(`${this.prefix}*`);
516
+ if (keys.length) {
517
+ await this.redis.del(keys);
518
+ }
519
+ }
520
+ get(bucket, key) {
521
+ return this.redis.sMembers(this.bucketKey(bucket, key));
522
+ }
523
+ async unions(buckets, keys) {
524
+ const result = {};
525
+ await Promise.all(
526
+ buckets.map(async (bucket) => {
527
+ result[bucket] = await this.redis.sUnion(this.bucketKeys(bucket, keys));
528
+ })
529
+ );
530
+ return result;
531
+ }
532
+ union(bucket, keys) {
533
+ return this.redis.sUnion(this.bucketKeys(bucket, keys));
534
+ }
535
+ add(transaction, bucket, key, values) {
536
+ transaction.sAdd(this.bucketKey(bucket, key), toArray3(values).map(toStr2));
537
+ }
538
+ del(transaction, bucket, keys) {
539
+ transaction.del(toArray3(keys).map((key) => this.bucketKey(bucket, key)));
540
+ }
541
+ remove(transaction, bucket, key, values) {
542
+ transaction.sRem(this.bucketKey(bucket, key), toArray3(values).map(toStr2));
543
+ }
544
+ bucketKey(bucket, key) {
545
+ return `${this.prefix}_${bucket}@${key}`;
546
+ }
547
+ bucketKeys(bucket, keys) {
548
+ return keys.map((key) => this.bucketKey(bucket, key));
549
+ }
550
+ };
551
+
552
+ // src/backends/mongodb.ts
553
+ var SINGLE_COLLECTION = "resources";
554
+ var toArray4 = (value) => Array.isArray(value) ? value : [value];
555
+ function encode(text) {
556
+ if (typeof text === "string") {
557
+ return encodeURIComponent(text).replace(/\./g, "%2E");
558
+ }
559
+ return text;
560
+ }
561
+ var decode = (text) => decodeURIComponent(text);
562
+ var MongoDBBackend = class {
563
+ db;
564
+ prefix;
565
+ useSingle;
566
+ useRawCollectionNames;
567
+ constructor(db, options = {}) {
568
+ this.db = db;
569
+ this.prefix = options.prefix ?? "";
570
+ this.useSingle = options.useSingle ?? false;
571
+ this.useRawCollectionNames = options.useRawCollectionNames ?? false;
572
+ }
573
+ begin() {
574
+ return [];
575
+ }
576
+ async end(transaction) {
577
+ for (const mutation of transaction) {
578
+ await mutation();
579
+ }
580
+ }
581
+ async clean() {
582
+ const collections = await this.db.collections();
583
+ await Promise.all(collections.map((collection) => collection.drop().catch(() => false)));
584
+ }
585
+ async get(bucket, key) {
586
+ const collection = this.collection(bucket);
587
+ const doc = await collection.findOne(this.filter(bucket, encode(key)), {
588
+ projection: { _bucketname: 0 }
589
+ });
590
+ if (!doc) {
591
+ return [];
592
+ }
593
+ return this.members(doc);
594
+ }
595
+ async union(bucket, keys) {
596
+ const collection = this.collection(bucket);
597
+ const filter = this.useSingle ? { _bucketname: bucket, key: { $in: keys.map(encode) } } : { key: { $in: keys.map(encode) } };
598
+ const docs = await collection.find(filter, { projection: { _bucketname: 0 } }).toArray();
599
+ const union2 = /* @__PURE__ */ new Set();
600
+ for (const doc of docs) {
601
+ for (const member of this.members(doc)) {
602
+ union2.add(member);
603
+ }
604
+ }
605
+ return [...union2];
606
+ }
607
+ add(transaction, bucket, key, values) {
608
+ if (key === "key") {
609
+ throw new Error("Key name 'key' is not allowed.");
610
+ }
611
+ const filter = this.filter(bucket, encode(key));
612
+ const doc = this.buildDoc(values);
613
+ transaction.push(async () => {
614
+ await this.collection(bucket).updateOne(filter, { $set: doc }, { upsert: true });
615
+ });
616
+ transaction.push(async () => {
617
+ await this.collection(bucket).createIndex({ _bucketname: 1, key: 1 });
618
+ });
619
+ }
620
+ del(transaction, bucket, keys) {
621
+ const encoded = toArray4(keys).map(encode);
622
+ const filter = this.useSingle ? { _bucketname: bucket, key: { $in: encoded } } : { key: { $in: encoded } };
623
+ transaction.push(async () => {
624
+ await this.collection(bucket).deleteMany(filter);
625
+ });
626
+ }
627
+ remove(transaction, bucket, key, values) {
628
+ const filter = this.filter(bucket, encode(key));
629
+ const doc = this.buildDoc(values);
630
+ transaction.push(async () => {
631
+ await this.collection(bucket).updateOne(filter, { $unset: doc }, { upsert: true });
632
+ });
633
+ }
634
+ // --- helpers ---------------------------------------------------------------
635
+ collection(bucket) {
636
+ const name = this.useSingle ? SINGLE_COLLECTION : bucket;
637
+ return this.db.collection(this.prefix + this.sanitizeCollectionName(name));
638
+ }
639
+ filter(bucket, key) {
640
+ return this.useSingle ? { _bucketname: bucket, key } : { key };
641
+ }
642
+ /** Build a `{ <encoded member>: true }` doc from one or many values. */
643
+ buildDoc(values) {
644
+ const doc = {};
645
+ for (const value of toArray4(values)) {
646
+ doc[`${encode(value)}`] = true;
647
+ }
648
+ return doc;
649
+ }
650
+ /** Decode a stored document's field names back into set members. */
651
+ members(doc) {
652
+ return Object.keys(doc).filter((field) => field !== "key" && field !== "_id").map(decode);
653
+ }
654
+ sanitizeCollectionName(name) {
655
+ if (this.useRawCollectionNames) {
656
+ return name;
657
+ }
658
+ return decodeURIComponent(name).replace(/[/\s]/g, "_");
659
+ }
660
+ };
661
+
662
+ // src/index.ts
663
+ var VERSION = "1.0.0-alpha.0";
664
+
665
+ export { Acl, HttpError, MemoryBackend, MongoDBBackend, RedisBackend, VERSION, aclErrorHandler, aclMiddleware, Acl as default };
666
+ //# sourceMappingURL=index.js.map
667
+ //# sourceMappingURL=index.js.map