insite-users-server 2.3.5 → 2.5.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Eugene Nesvetaev
3
+ Copyright (c) 2026 Eugene Nesvetaev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,13 +1,382 @@
1
- # inSite users server side
1
+ # insite-users-server
2
+
3
+ [![npm](https://img.shields.io/npm/v/insite-users-server)](https://www.npmjs.com/package/insite-users-server)
4
+
5
+ Server-side core for users, organizations, roles, sessions, and avatars in the inSite stack. In-memory domain models backed by MongoDB with change streams for real-time sync, role-based abilities, and hierarchical org structure.
6
+
7
+ Part of [inSite](../../README.md) — uses [insite-db](../db/README.md) and [insite-common](../common/README.md). WebSocket integration via [insite-users-server-ws](../users-server-ws/README.md).
2
8
 
3
9
  ## Installation
4
10
 
5
11
  ```sh
6
- $ npm install insite-users-server
12
+ npm install insite-users-server
7
13
  ```
8
14
 
9
- Or
15
+ Or:
10
16
 
11
17
  ```sh
12
- $ bun add insite-users-server
18
+ bun add insite-users-server
19
+ ```
20
+
21
+ ## Overview
22
+
23
+ - **Users** — in-memory `Map<string, User>` with `byEmail`, `bySessionId`, `sorted`; login/logout, password hashing (Argon2), root user auto-creation
24
+ - **Orgs** — hierarchical organizations with owners (users or orgs), `slaveOrgs`, `null` org for users without org
25
+ - **Roles** — role hierarchy with `involves`, merged abilities, `root` role
26
+ - **Sessions** — TTL (7 days), `isOnline`, prolong, `uid()` for session IDs
27
+ - **Avatars** — WebP storage in `users.avatars` collection, base64 save
28
+ - **Abilities** — schema-driven permissions via `AbilitiesMap`, merge from roles, params (number/items)
29
+
30
+ ## Setup
31
+
32
+ ```ts
33
+ import { Users } from "insite-users-server";
34
+ import type { Collections } from "insite-db";
35
+ import type { AbilitiesSchema } from "insite-common";
36
+
37
+ const users = await Users.init(collections, {
38
+ abilities: yourAbilitiesSchema,
39
+ // optional: indexes, schema, initialRoot, roles, orgs, sessions, avatars, collection
40
+ });
41
+
42
+ // or with constructor:
43
+ const usersInstance = new Users(collections, options);
44
+ await usersInstance.whenReady();
45
+ ```
46
+
47
+ **Options<AS>:**
48
+
49
+ | Option | Type | Description |
50
+ |--------|------|-------------|
51
+ | `abilities` | `AS` | Abilities schema from [insite-common](../common/README.md) |
52
+ | `indexes` | `CollectionIndexes` | Additional MongoDB indexes for users |
53
+ | `schema` | `CollectionSchema` | Custom schema extensions for users |
54
+ | `initialRoot` | `Partial<UserDoc>` | Props for auto-created root user |
55
+ | `roles` | `RolesOptions` | Roles collection config |
56
+ | `orgs` | `OrgsOptions` | Orgs collection config |
57
+ | `sessions` | `SessionsOptions` | Sessions collection config |
58
+ | `avatars` | `AvatarsOptions` | Avatars collection config |
59
+ | `collection` | `CollectionOptions` | Base collection options |
60
+
61
+ **Events:** `user-create`, `user-is-online`, `user-permissions-change`, `session-delete`
62
+
63
+ ## Public API
64
+
65
+ ### Exports
66
+
67
+ | Export | Type | Description |
68
+ |--------|------|-------------|
69
+ | `Users` | class | Main entry, extends `Map<string, User>` |
70
+ | `User` | class | User entity |
71
+ | `UserDoc`, `Options`, `AvatarDoc` | types | User-related types |
72
+ | `Org`, `OrgDoc`, `Orgs` | types | Org-related |
73
+ | `Role`, `RoleDoc`, `Roles` | types | Role-related |
74
+ | `Session`, `SessionDoc`, `Sessions` | types | Session-related |
75
+ | `AbilitiesMap`, `AbilityBoolean`, `AbilityParam`, `AbilityParamItems`, `AbilityParamNumber`, `AbilityWithParams`, `GenericAbilities` | types | Abilities |
76
+ | `PermissionError`, `SubordinationError`, `UnauthorizedError` | classes | User errors |
77
+ | `RolesError`, `AbilityError` | classes | Role/ability errors |
78
+
79
+ ---
80
+
81
+ ### Users
82
+
83
+ ```ts
84
+ class Users<AS extends AbilitiesSchema> extends Map<string, User<AS>>
85
+ ```
86
+
87
+ | Method | Description |
88
+ |--------|-------------|
89
+ | `constructor(collections, options)` | Creates instance, starts async init |
90
+ | `static init(collections, options)` | Factory, returns `Promise<Users>` |
91
+ | `whenReady()` | Returns `Promise<Users>` for init completion |
92
+ | `load(userDoc)` | Loads user from doc into map |
93
+ | `create(newUser)` | Creates user, returns `_id` |
94
+ | `updateUser(_id, updates, byUser?)` | Updates user; `byUser` restricts role changes by subordination |
95
+ | `changePassword(_id, newPassword)` | Hashes and updates password, invalidates sessions |
96
+ | `login(email, password, sessionProps)` | Verifies credentials, creates session |
97
+ | `logout(session)` | Deletes session |
98
+ | `deleteUser(_id)` | Deletes user from collection |
99
+ | `sortIds(ids)` | Sorts ids by user/org display order |
100
+ | `replace(fromId)` | Runs replace handlers (e.g. merge user) |
101
+
102
+ | Property | Type | Description |
103
+ |----------|------|-------------|
104
+ | `collection` | `WatchedCollection<UserDoc>` | Users MongoDB collection |
105
+ | `roles` | `Roles<AS>` | Roles manager |
106
+ | `sessions` | `Sessions<AS>` | Sessions manager |
107
+ | `orgs` | `Orgs<AS>` | Orgs manager |
108
+ | `avatars` | `Avatars` | Avatars manager |
109
+ | `abilities` | `AbilitiesMap<AS>` | Abilities schema map |
110
+ | `byEmail` | `Map<string, User<AS>>` | Lookup by email |
111
+ | `bySessionId` | `Map<string, User<AS>>` | Lookup by session id |
112
+ | `sorted` | `User<AS>[]` | Users sorted by last name |
113
+ | `replaceMap` | `Map<string, string>` | Map fromId → toId for replace |
114
+ | `replaceHandlers` | `Set<(fromId, toId) => void \| Promise<void>>` | Handlers for `replace()` |
115
+
116
+ ---
117
+
118
+ ### User
119
+
120
+ ```ts
121
+ class User<AS extends AbilitiesSchema>
122
+ ```
123
+
124
+ | Property | Type | Description |
125
+ |----------|------|-------------|
126
+ | `_id` | `string` | User id |
127
+ | `email` | `string` | Email |
128
+ | `name` | `{ first, middle, last }` | Name parts |
129
+ | `initials` | `string` | First letters of first/last name |
130
+ | `displayLabel` | `string` | Display name (first + last or email) |
131
+ | `org` | `Org<AS>` | Organization |
132
+ | `job` | `string` | Job title |
133
+ | `avatar` | `string \| null` | Avatar timestamp id |
134
+ | `avatarUrl` | `string \| null` | Full avatar URL |
135
+ | `roles` | `Set<Role<AS>>` | All roles (own + inherited) |
136
+ | `ownRoles` | `Role<AS>[]` | Direct roles |
137
+ | `ownRoleIds` | `string[]` | Direct role ids |
138
+ | `slaveRoles` | `Set<Role<AS>>` | Inherited roles |
139
+ | `slaveRoleIds` | `string[]` | Inherited role ids |
140
+ | `abilities` | `Abilities<AS>` | Merged abilities from roles |
141
+ | `slaveOrgs` | `Set<Org<AS>>` | Orgs user owns |
142
+ | `slaveUsers` | `Set<User<AS>>` | Users in subordination |
143
+ | `slaves` | `Set<Org<AS> \| User<AS>>` | Union of slaveOrgs and slaveUsers |
144
+ | `slaveIds` | `string[]` | Ids of slaves |
145
+ | `permissiveIds` | `string[]` | Ids user can act on (org, self, slaves) |
146
+ | `sessions` | `Set<Session<AS>>` | Active sessions |
147
+ | `isOnline` | `boolean` | Has at least one online session |
148
+ | `isRoot` | `boolean` | Has root role |
149
+
150
+ ---
151
+
152
+ ### Orgs
153
+
154
+ ```ts
155
+ class Orgs<AS extends AbilitiesSchema> extends Map<string, Org<AS>>
156
+ ```
157
+
158
+ | Method | Description |
159
+ |--------|-------------|
160
+ | `create(newOrg)` | Creates org, returns `_id` |
161
+ | `updateOrg(_id, updates, byUser?)` | Updates org; `byUser` restricts owner changes |
162
+ | `deleteOrg(org)` | Removes org from owners, deletes org |
163
+ | `load(orgDoc)` | Loads org from doc |
164
+ | `replace(fromId)` | Runs replace handlers |
165
+
166
+ | Property | Type | Description |
167
+ |----------|------|-------------|
168
+ | `collection` | `WatchedCollection<OrgDoc>` | Orgs collection |
169
+ | `null` | `Org<AS>` | Virtual org for users without org |
170
+ | `sorted` | `Org<AS>[]` | Orgs in hierarchy order |
171
+ | `replaceMap` | `Map<string, string>` | Replace mapping |
172
+ | `replaceHandlers` | `Set<(fromId, toId) => void \| Promise<void>>` | Replace handlers |
173
+
174
+ ---
175
+
176
+ ### Org
177
+
178
+ ```ts
179
+ class Org<AS extends AbilitiesSchema>
180
+ ```
181
+
182
+ | Property | Type | Description |
183
+ |----------|------|-------------|
184
+ | `_id` | `string` | Org id |
185
+ | `title` | `string` | Org title |
186
+ | `initials` | `string` | First letter of title |
187
+ | `displayLabel` | `string` | Same as title |
188
+ | `ownerOrgs` | `Set<Org<AS>>` | Parent orgs |
189
+ | `ownerUsers` | `Set<User<AS>>` | Owner users |
190
+ | `users` | `Set<User<AS>>` | Users in this org |
191
+ | `slaveOrgs` | `Set<Org<AS>>` | Child orgs |
192
+ | `ownerIds` | `string[]` | Owner ids (users or orgs) |
193
+
194
+ | Method | Description |
195
+ |--------|-------------|
196
+ | `delete()` | Deletes org via Orgs |
197
+
198
+ ---
199
+
200
+ ### Roles
201
+
202
+ ```ts
203
+ class Roles<AS extends AbilitiesSchema> extends Map<string, Role<AS>>
13
204
  ```
205
+
206
+ | Method | Description |
207
+ |--------|-------------|
208
+ | `create(newRole)` | Creates role, returns `_id` |
209
+ | `updateRole(roleId, updates)` | Updates role (excludes abilities) |
210
+ | `deleteRole(role)` | Removes from involves, deletes role |
211
+ | `setAbility(roleId, abilityLongId, set)` | Enables/disables ability |
212
+ | `setAbilityParam(roleId, abilityLongId, paramId, value)` | Sets ability param |
213
+ | `cleanUpIds(ids)` | Removes redundant role ids (e.g. if A involves B, removes A when B requested) |
214
+ | `load(roleDoc)` | Loads role from doc |
215
+
216
+ | Property | Type | Description |
217
+ |----------|------|-------------|
218
+ | `collection` | `WatchedCollection<RoleDoc>` | Roles collection |
219
+ | `root` | `Role<AS>` | Root role |
220
+ | `sorted` | `Role<AS>[]` | Roles in hierarchy order |
221
+
222
+ ---
223
+
224
+ ### Role
225
+
226
+ ```ts
227
+ class Role<AS extends AbilitiesSchema>
228
+ ```
229
+
230
+ | Property | Type | Description |
231
+ |----------|------|-------------|
232
+ | `_id` | `string` | Role id |
233
+ | `ownInvolveIds` | `string[]` | Direct role ids this involves |
234
+ | `ownAbilities` | `Abilities<AS>` | Direct abilities |
235
+ | `title` | `string` | Role title |
236
+ | `displayTitle` | `string` | Title or id |
237
+ | `ownInvolves` | `Role<AS>[]` | Direct involved roles |
238
+ | `involves` | `Set<Role<AS>>` | All involved roles (transitive) |
239
+ | `abilities` | `Abilities<AS>` | Merged abilities |
240
+ | `inheritedAbilities` | `Abilities<AS>` | From involved roles |
241
+
242
+ ---
243
+
244
+ ### Sessions
245
+
246
+ ```ts
247
+ class Sessions<AS extends AbilitiesSchema> extends Map<string, Session<AS>>
248
+ ```
249
+
250
+ | Method | Description |
251
+ |--------|-------------|
252
+ | `uid()` | Generates session id (ObjectId~base64url) |
253
+ | `create(user, props)` | Creates session for user |
254
+ | `destroySession(_id)` | Deletes session |
255
+ | `load(sessionDoc)` | Loads session from doc |
256
+
257
+ | Property | Type | Description |
258
+ |----------|------|-------------|
259
+ | `collection` | `WatchedCollection<SessionDoc>` | Sessions collection (TTL 7 days) |
260
+
261
+ ---
262
+
263
+ ### Session
264
+
265
+ ```ts
266
+ class Session<AS extends AbilitiesSchema>
267
+ ```
268
+
269
+ | Property | Type | Description |
270
+ |----------|------|-------------|
271
+ | `_id` | `string` | Session id |
272
+ | `user` | `User<AS>` | User |
273
+ | `isOnline` | `boolean` | Online flag |
274
+ | `userAgent` | `string` | User agent |
275
+ | `remoteAddress` | `string` | IP |
276
+ | `createdAt` | `number` | Timestamp |
277
+ | `prolongedAt` | `number` | Last prolong timestamp |
278
+ | `expiresAt` | `Date` | Expiry |
279
+
280
+ | Method | Description |
281
+ |--------|-------------|
282
+ | `prolong(updates)` | Extends expiry, updates fields |
283
+ | `online()` | Sets isOnline true |
284
+ | `offline()` | Sets isOnline false |
285
+ | `delete()` | Removes from map and collection |
286
+
287
+ ---
288
+
289
+ ### Avatars
290
+
291
+ ```ts
292
+ class Avatars
293
+ ```
294
+
295
+ | Method | Description |
296
+ |--------|-------------|
297
+ | `save(_id, type, data)` | Saves base64 data (expects `image/webp`), updates user.avatar |
298
+ | `deleteAvatar(_id)` | Deletes avatar, clears user.avatar |
299
+
300
+ | Property | Type | Description |
301
+ |----------|------|-------------|
302
+ | `collection` | `WatchedCollection<AvatarDoc>` | Avatars collection |
303
+ | `TYPES_ACCEPTED` | `string[]` | `["image/webp"]` |
304
+ | `MAX_SIZE` | `number` | 512 KB |
305
+
306
+ ---
307
+
308
+ ### AbilitiesMap
309
+
310
+ ```ts
311
+ class AbilitiesMap<AS extends AbilitiesSchema> extends Map<string, AbilitySchema>
312
+ ```
313
+
314
+ | Method | Description |
315
+ |--------|-------------|
316
+ | `get(longId, parent?)` | Gets ability schema by long id (e.g. `"users.edit"`) |
317
+ | `getParamSchema(longId, paramId)` | Gets param schema |
318
+ | `getDefaultParam(longId, paramId, max?)` | Default value for param |
319
+ | `getDefaultAbility(schema, max?)` | Default ability object |
320
+ | `getDefaultAbilities(schemas?, max?)` | Full default abilities |
321
+ | `getSchemeFor(abilities)` | Returns schema filtered to given abilities |
322
+ | `getParam(abilities, longId, paramId)` | Gets param value or default |
323
+ | `setParam(abilities, longId, paramId, value)` | Sets param, returns abilities |
324
+ | `isParamFits(longId, paramId, value, referenceValue?)` | Validates param |
325
+ | `adjustParam(paramSchema, value)` | Clamps/validates param value |
326
+ | `adjust(abilities, parent?)` | Normalizes abilities to schema |
327
+ | `merge(target, source, parent?)` | Merges source into target |
328
+ | `hasAbility(abilities, longId)` | Checks if ability is set |
329
+ | `setAbility(target, longId)` | Enables ability |
330
+ | `unsetAbility(target, longId)` | Disables ability |
331
+
332
+ ---
333
+
334
+ ### Errors
335
+
336
+ | Class | Constructor | `name` |
337
+ |-------|-------------|--------|
338
+ | `UnauthorizedError` | `(payload?)` | `"UnauthorizedError"` |
339
+ | `SubordinationError` | `(payload?)` or `(type, payload?)` or `(type, ids, payload?)` — type: `"_id" \| "permissiveIds" \| "slaveIds"` | `"SubordinationError"` |
340
+ | `PermissionError` | `(payload?)` | `"PermissionError"` |
341
+ | `RolesError` | `(roleId, payload?)` or `(roleIds[], payload?)` | `"RolesError"` |
342
+ | `AbilityError` | `(payload?)` or `(longId, payload?)` | `"AbilityError"` |
343
+
344
+ All extend `Err` from `@nesvet/n` with `code` and `payload`.
345
+
346
+ ---
347
+
348
+ ## Types Reference
349
+
350
+ | Type | Description |
351
+ |------|-------------|
352
+ | `UserDoc` | `{ _id, email, password, roles, name, org, job, avatar?, meta, createdAt }` |
353
+ | `NewUser` | UserDoc minus _id, createdAt; name partial |
354
+ | `Options<AS>` | Users init options |
355
+ | `OrgDoc` | `{ _id, title, note, owners, meta, createdAt }` |
356
+ | `NewOrg` | OrgDoc minus _id, createdAt |
357
+ | `OrgsOptions` | Orgs config |
358
+ | `SessionDoc` | `{ _id, user, userAgent, remoteAddress, isOnline, meta, createdAt, prolongedAt, expiresAt }` |
359
+ | `SessionsOptions` | Sessions config |
360
+ | `RoleDoc` | `{ _id, involves, abilities, title, description, meta, createdAt }` |
361
+ | `NewRole` | RoleDoc minus abilities, createdAt |
362
+ | `RolesOptions` | Roles config |
363
+ | `AvatarDoc` | `{ _id, type, size, ts, data, meta }` |
364
+ | `AvatarsOptions` | Avatars config |
365
+ | `AbilityParam` | `AbilityParamItems \| AbilityParamNumber` |
366
+ | `AbilityParamItems` | `string[]` |
367
+ | `AbilityParamNumber` | `number` |
368
+ | `AbilityWithParams` | `Record<string, AbilityParam>` |
369
+ | `AbilityObject` | `Record<string, Ability \| AbilityParam>` |
370
+ | `AbilityBoolean` | `true` |
371
+ | `Ability` | `AbilityBoolean \| AbilityObject` |
372
+ | `GenericAbilities` | `Record<string, Ability>` |
373
+
374
+ ---
375
+
376
+ ## Related
377
+
378
+ - [insite-db](../db/README.md), [insite-common](../common/README.md), [insite-users-server-ws](../users-server-ws/README.md)
379
+
380
+ ## License
381
+
382
+ MIT
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- function O(r){if(r instanceof Set){let e=[];for(let t of r)e.push(t._id);return e}return r.map(e=>e._id)}function ce(r,...e){let t=0;for(let i of e)r.includes(i)||(r.push(i),t++);return t}function M(r,e,t){if(r&&e){if(e.includes(".")){let i=e.split("."),s=r;for(let o=0;o<i.length-1;o++){let n=i[o],l=s[n];s=l&&typeof l=="object"?l:s[n]={}}return s[i.at(-1)]=t}return r[e]=t}return null}function de(r,...e){for(let t of e)for(let i in t)Object.hasOwn(t,i)&&M(r,i,t[i]);return r}function S(){}var E=new WeakMap,x=class r extends Promise{constructor(e,t){let i,s;if(super((o,n)=>{i=o,s=n}),E.set(this,{resolve:i,reject:s}),t?.signal)if(t.signal.aborted)this.reject(new DOMException("Aborted","AbortError"));else{let o=()=>this.reject(new DOMException("Aborted","AbortError"));t.signal.addEventListener("abort",o),this.finally(()=>t.signal?.removeEventListener("abort",o))}if(typeof t?.timeout=="number"&&Number.isFinite(t.timeout)){let o=setTimeout(()=>this.reject(new DOMException("Timeout exceeded","TimeoutError")),t.timeout);this.finally(()=>clearTimeout(o))}e?.(this.resolve,this.reject)}isPending=!0;isFulfilled=!1;isRejected=!1;state="pending";result;resolve=e=>{if(this.isPending){this.isPending=!1,this.isFulfilled=!0,this.state="fulfilled",this.result=e;let t=E.get(this);t&&(t.resolve.call(this,e),E.delete(this))}};reject=e=>{if(this.isPending){this.isPending=!1,this.isRejected=!0,this.state="rejected",this.result=e;let t=E.get(this);t&&(t.reject.call(this,e),E.delete(this))}};static resolved(e){return new r(t=>t(e))}static rejected(e){return new r((t,i)=>i(e))}};function K(r,e=0,t={}){let{leading:i=!1,trailing:s=!0}=t,{maxWait:o=1/0}=t,n,l,a,d,g,A,p,w,ae=async()=>{let b=Date.now();b>=a?(n=void 0,a=void 0,clearTimeout(l),l=void 0,typeof p=="function"&&await p(),y.resolve(w),A=i):(clearTimeout(n),n=setTimeout(ae,a-b))},F=Number.isFinite(o)?()=>{p===v&&(p=!1),v(),l=setTimeout(F,o)}:null;async function v(){try{return w=await r.apply(d,g),w}catch(b){throw y.reject(b),b}}i&&typeof i=="boolean"&&(i=v),A=i,s&&typeof s=="boolean"&&(s=v),p=s;let y=Object.assign(function(...b){return g=b,d=this,a=Date.now()+e,p=s,y.promise.isPending||(y.promise=new x((Oe,Pe)=>{y.resolve=Oe,y.reject=Pe,typeof A=="function"&&(A(),A=!1,p===v&&(p=!1)),n||(n=setTimeout(ae,e),F&&(l=setTimeout(F,o)))})),y.promise},{callback:r,promise:x.resolved(void 0),resolve:S,reject:S,run:v,clear:async b=>{clearTimeout(n),n=void 0,clearTimeout(l),l=void 0,y.resolve(b?await v():w)}});return y}K.noop=Object.assign(()=>{},{callback:S,promise:x.resolved(void 0),resolve:S,reject:S,run:S,clear:S});function pe(r,e){if(r&&e){if(e.includes(".")){let t=e.split("."),i=r;for(let o=0;o<t.length-1;o++){let n=t[o],l=i[n];if(l&&typeof l=="object")i=l;else return!1}let s=t.at(-1);return s in i&&delete i[s]}return e in r&&delete r[e]}return!1}function u(r,e){if(r){for(let t of e)delete r[t];return r}return null}function U(r){for(let e in r)delete r[e];return r}var C=class extends Array{constructor(){super(),Object.freeze(this)}push(){return 0}unshift(){return 0}splice(){return[]}};var _=class extends Set{constructor(){super()}add(){return this}};var f=class extends Error{constructor(e,t=null,i){typeof e=="object"?(i=e,e=i.message,delete i.message,t=null):t&&typeof t=="object"&&(i=t,t=null),super(e),this.tag=t,i&&Object.assign(this,i)}name="Err-or";tag};function J(r,e,t=!1){let i=[];if(t)for(let s of e){let o=r.get(s);o!==void 0&&i.push(o)}else for(let s of e)i.push(r.get(s));return i}function N(r,e){if(e){if(e.includes(".")){let t=r;for(let i of e.split("."))if(t=t?.[i],t===void 0)break;return t}return r[e]}return r}function ue(r,e){for(let t of e)if(r.has(t))return!0;return!1}function fe(r,e,t,i,s=!0){return Array.isArray(r)&&(r=r.length),typeof i!="string"&&(s=i??!0,i=t),(s?`${r}\xA0`:"")+fe.raw(r,e,t,i)}fe.raw=(r,e,t,i=t)=>{r=Math.abs(r);let s=r%10,o=r%100;return s===1&&o!==11?e:s>=2&&s<=4&&!(o>=12&&o<=15)?t:i};function q(...r){let e=[];if(r.length){let[t,...i]=r;e:for(let s of t){for(let o of i)if(o instanceof Set?!o.has(s):!o.includes(s))continue e;e.push(s)}}return e}function he(r,e){let t={};for(let i of e)i in r&&(t[i]=r[i]);return t}function me(r,e){return r.delete(e),r.add(e),r}function h(r,e){let t,i=0;for(let s of e)for(;(t=r.indexOf(s))>-1;)r.splice(t,1),i++;return i}function ye(r,e){let t=r.indexOf(e);return t>-1?(r.splice(t,1),!0):!1}function be(r,e){for(let t of[...r].sort(e))r.delete(t),r.add(t);return r}function ge(...r){return[...new Set(r.flat())]}function k(r,e){return r.filter(t=>!e.includes(t))}var H=class extends f{constructor(e,t){typeof e=="object"&&(t=e,e=void 0),super("User lacks ability","lacksability",{...e&&{ability:e},...t})}name="AbilityError"};function V(r){return typeof r=="object"}function Z(r){return typeof r=="number"}function X(r){return Array.isArray(r)}var Ae=new Map,W=class extends Map{constructor(e){super();for(let t of e)delete t.top,this.#e.push(t);(function t(i,s,o){for(let n of i){if(Ae.set(n,s),n.level=o,!s||n.top){if(n.longId=n._id,this.has(n._id))throw new Error(`Top-level ability duplicate: ${n._id}`);this.set(n._id,n)}else n.longId=s?`${s.longId}.${n._id}`:n._id;n.abilities&&t.call(this,n.abilities,n,o+1)}}).call(this,e,null,0)}#e=[];get(e,t){let i=e.split("."),s=t??super.get(i.shift());if(s&&i.length){for(let o of i)if(s=s?.abilities?.find(({_id:n,top:l})=>!l&&n===o),!s)break}return s}getSchema=this.get;getParamSchema(e,t){return this.get(e)?.params?.find(({_id:i})=>i===t)}getDefaultParam(e,t,i){(t===void 0||typeof t=="boolean")&&(i=t);let s=typeof e=="string"?this.getParamSchema(e,t):e;if(s)switch(s.type){case"number":return i?s.max??1/0:s.min??0;case"items":return i?s.items.map(({_id:o})=>o):[]}}getDefaultAbility(e,t=!e){if(typeof e=="string"&&(e=this.get(e)),e){if(e.abilities||e.params){let i={};if(e.params)for(let s of e.params)i[s._id]=this.getDefaultParam(s,t);if(t&&e.abilities)for(let s of e.abilities)s.top||(i[s._id]=this.getDefaultAbility(s,t));return i}return!0}}getDefaultAbilities(e,t=!e){let i=(e?(Array.isArray(e)?e:[e]).map(o=>typeof o=="string"?this.get(o):o).filter(Boolean):[...this.values()]).sort((o,n)=>o.level-n.level),s=new Map;for(let o of i)(function n(l){let a=Ae.get(l);a&&(n(a),s.has(a)||s.set(a,!1))})(o),t?(function n(l){if(s.has(l)||s.set(l,!0),l.abilities)for(let a of l.abilities)a.top&&n(a)})(o):s.has(o)||s.set(o,!1);return de({},Object.fromEntries([...s].map(([o,n])=>[o.longId,this.getDefaultAbility(o,n)])))}getSchemeFor(e){return e=this.adjust(structuredClone(e)),(function t(i,s){for(let o=0;o<i.length;o++){let n=i[o],l=n.top?e[n._id]:s[n._id];if(l){if(n.params)for(let a of n.params){let d=l[a._id];switch(a.type){case"number":a.max=d;break;case"items":a.items=a.items.filter(({_id:g})=>d.includes(g))}}n.abilities&&(t(n.abilities,l),n.abilities.length||delete n.abilities)}else i.splice(o--,1)}return i})(structuredClone(this.#e),e)}getParam(e,t,i){if(this.getParamSchema(t,i)){let s=N(e,`${t}.${i}`);return this.isParamFits(t,i,s)?s:this.getDefaultParam(t,i)}}setParam(e,t,i,s){let o=this.getParamSchema(t,i);if(!o)throw new Error(`No such ability param: ${t}.${i}`);return M(e,`${t}.${i}`,this.adjustParam(o,s)),e}isParamFits(e,t,i,s){if(i!==void 0){let o=this.getParamSchema(e,t);if(o)switch(o.type){case"number":return Z(i)&&(o.min===void 0||i>=o.min)&&(s===void 0?o.max===void 0||i<=o.max:i<=s);case"items":return X(i)&&i.length===new Set(i).size&&(s===void 0?i.every(n=>o.items.find(({_id:l})=>l===n)):i.every(n=>s.includes(n)))}}return!1}adjustParam(e,t){switch(e.type){case"number":return Z(t)?Math.max(Math.min(t,e.max??1/0),e.min??0):e.min??0;default:return X(t)?q(e.items.map(i=>i._id),t):[]}}adjust(e,t){let i={};for(let[s,o]of Object.entries(e)){let n=this.get(s,t);if(n)if(n.params||n.abilities){let l={};if(n.params)if(V(o))for(let a of n.params)l[a._id]=this.adjustParam(a,o[a._id]);else for(let a of n.params)l[a._id]=this.getDefaultParam(a);n.abilities&&V(o)&&Object.assign(l,this.adjust(he(o,n.abilities.filter(a=>!a.top).map(a=>a._id)),n)),i[s]=l}else i[s]=!0}return Object.assign(U(e),i)}merge(e,t,i){i||(this.adjust(e),t=this.adjust(structuredClone(t)));for(let[s,o]of Object.entries(t)){let n=this.get(s,i);if(n&&n.isInheritable!==!1)if(s in e&&(n.params||n.abilities)){let l=e[s],a=o;if(n.params){for(let d of n.params)if(d.isInheritable!==!1)switch(d.type){case"number":l[d._id]=Math.max(l[d._id],a[d._id]);break;case"items":ce(l[d._id],...a[d._id])}}n.abilities&&this.merge(l,a,n)}else e[s]=o}return e}hasAbility(e,t){return!!N(e,t)}setAbility(e,t){let i=typeof t=="string"?this.get(t):t;if(!i)throw new Error(`No such ability: ${t}`);return this.merge(e,this.getDefaultAbilities(i)),e}unsetAbility(e,t){let i=typeof t=="string"?this.get(t):t;if(!i)throw new Error(`No such ability: ${t}`);return(function s(o){if(pe(e,o.longId),o.abilities)for(let n of o.abilities)s(n)})(i),e}};var Y=class extends f{constructor(e,t){super(`User lacks role${Array.isArray(e)?"s":""}`,"lacksroles",{roles:Array.isArray(e)?e:[e],...t})}name="RolesError"};var Q=new WeakMap,D=class{constructor(e,t){this.#e=e,this._id=t._id,e.set(this._id,this),this.update(t)}#e;_id;ownInvolveIds;ownAbilities;title="";displayTitle="";_o=0;ownInvolves=[];involves=new Set;abilities={};inheritedAbilities={};update({_id:e,involves:t,abilities:i,title:s,...o}){if(Object.assign(this,o),t&&(this.ownInvolveIds=t),i&&(this.ownAbilities=i),s!==void 0&&(this.title=s,this.displayTitle=s||this._id),t||i){let n=[this.ownInvolveIds.join(","),JSON.stringify(this.ownAbilities)].join(`
2
- `);n!==Q.get(this)&&(Q.set(this,n),this.#e.users.isInited&&this.#e.update())}}delete(){this.#e.delete(this._id),Q.delete(this),this.#e.update()}};var c={_id:/^[\dA-Fa-f]{24}$/,email:/^[^\s@]{1,63}@[^\s@]{1,63}\.[^\s@]{1,63}$/,argon2:/^\$argon2(?:i|d|id)\$v=\d+\$m=\d+,t=\d+,p=\d+(?:\$[\d+/A-Za-z]+){2}$/,sessionId:/^(?:[\dA-Fa-f]{16,32}\$[\dA-Fa-f]{16,32}|[0-9a-f]{24}~[A-Za-z0-9_-]{43})$/,role:/^[\d_a-z-]{1,64}$/,ip:/^(?:\d{1,3}\.){3}\d{1,3}$/};var I={required:["_id","involves","abilities","title","description","meta","createdAt"],properties:{_id:{bsonType:"string",pattern:c.role.source,not:{enum:["root"]}},involves:{bsonType:"array",uniqueItems:!0,items:{bsonType:"string",pattern:c.role.source,not:{enum:["root"]}},additionalItems:!1},abilities:{bsonType:"object"},title:{bsonType:"string",maxLength:256},description:{bsonType:"string",maxLength:10240},meta:{bsonType:"object"},createdAt:{bsonType:"number"}},additionalProperties:!1};var _e=[[{involves:1}]];function Se(){throw new Error("Direct usage of roles collection.deleteOne or .deleteMany is forbidden. Use roles.deleteRole instead.")}var G=class extends Map{constructor(e,t={}){super();let{root:i,...s}=t;this.users=e,this.collections=e.collections,this.abilities=e.abilities,this.root=new D(this,{...i,_id:"root",involves:[],abilities:this.abilities.getDefaultAbilities(),title:"Root",description:"Root",meta:{},createdAt:Date.now()}),this.initOptions=s}users;collections;collection;abilities;root;initOptions;async init(){if(!this.users.isInited){let{schema:e,indexes:t,collection:i}=this.initOptions;e&&(e.required&&h(e.required,I.required),e.properties&&u(e,Object.keys(I.properties)));let s={...I,...e,required:[...I.required,...e?.required??[]],properties:{...I.properties,...e?.properties}};this.collection=Object.assign(await this.collections.ensure("roles",{...i,schema:s}),{deleteOne:Se,deleteMany:Se}),await this.collection.ensureIndexes([..._e,...t??[]]),await this.#e();for await(let o of this.collection.find())this.abilities.adjust(o.abilities)&&await this.collection.updateOne({_id:o._id},{$set:{abilities:o.abilities}}),this.load(o);this.update(),this.collection.onChange(this.#t),delete this.initOptions}}async#e(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}#t=e=>{switch(e.operationType){case"insert":{new D(this,e.fullDocument);break}case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};load(e){new D(this,e)}resolve(e,t){t=[e,...t];for(let i of e.ownInvolveIds){let s=this.get(i);if(s){for(let o of t)me(o.involves,s),this.abilities.merge(o.inheritedAbilities,s.abilities);s!==e&&this.resolve(s,t)}}}sorted=[];#i(e,t){return t.involves.size-e.involves.size||(e.title&&t.title?e.title>t.title?1:e.title<t.title?-1:0:e.title?-1:t.title||e._id>t._id?1:e._id<t._id?-1:0)}update(){let e=[...this.values()];this.root.ownInvolveIds=[];for(let s of e)s!==this.root&&!e.some(o=>o.ownInvolveIds.includes(s._id))&&this.root.ownInvolveIds.push(s._id),s.involves.clear(),s.abilities=structuredClone(s.ownAbilities),s.inheritedAbilities={};this.root.involves.add(this.root),this.resolve(this.root,[]);for(let s of e)s.ownInvolves=J(this,s.ownInvolveIds,!0).sort(this.#i).reverse();let t=[this.root];for(let s=0;s<t.length;s++){let o=t[s];for(let n of o.ownInvolves){let l=t.indexOf(n);~l&&(t.splice(l,1),s--),t.splice(s+1,0,n)}}let i=0;for(let s of t)this.abilities.merge(s.abilities,s.inheritedAbilities),s._o=i++;this.sorted=t,this.users.isInited&&this.users.update(!0)}async create({_id:e,involves:t,title:i,description:s,...o}){return await this.collection.insertOne({_id:e,involves:t||[],abilities:{},title:i??"",description:s??"",meta:{},...u(o,["abilities"]),createdAt:Date.now()}),e}async updateRole(e,t){let i=this.get(e);if(!i)return!1;if(t.involves){h(t.involves,[e]);for(let s of t.involves)this.get(s)?.involves.has(i)&&h(t.involves,[s]);t.involves=this.cleanUpIds(t.involves)}return await this.collection.updateOne({_id:e},{$set:t}),!0}deleteRole(e){let t=typeof e=="string"?e:e._id;return this.collection.bulkWrite([{updateMany:{filter:{involves:t},update:{$pullAll:{involves:[t]}}}},{deleteOne:{filter:{_id:t}}}])}cleanUpIds(e){let t=q(this.sorted,J(this,e,!0)),i=[];for(let s of t)t.every(o=>o===s||!o.involves.has(s))&&i.push(s._id);return i}async setAbility(e,t,i){let s=this.get(e);return s?(await this.collection.updateOne({_id:e},{$set:{abilities:i?this.abilities.setAbility(s.ownAbilities,t):this.abilities.unsetAbility(s.ownAbilities,t)}}),!0):!1}async setAbilityParam(e,t,i,s){let o=this.get(e);return o?(await this.collection.updateOne({_id:e},{$set:{abilities:this.abilities.setParam(o.ownAbilities,t,i,s)}}),!0):!1}};var ee=class extends f{constructor(e){super("Unauthorized","unauthorized",e)}name="UnauthorizedError"},te=class extends f{constructor(e,t,i){switch(arguments.length){case 1:i=e,e=void 0;break;case 2:i=t,t=void 0}super("Insubordination attempt","subordination",{...e&&{type:e},...t&&{ids:Array.isArray(t)?t:[t]},...i})}name="SubordinationError"},ie=class extends f{constructor(e){super("Insufficient permissions","permissions",e)}name="PermissionError"};import{hash as ve,verify as ke}from"@node-rs/argon2";import Me from"eventemitter3";import{newObjectIdString as Ue}from"insite-db";import{newObjectIdString as De}from"insite-db";var se=new WeakMap,m=class{constructor(e,t){this.orgs=e,this._id=t._id,e.set(this._id,this),this.update(t)}_id;title="";initials="";displayLabel="";_o=0;orgs;isOrg=!0;ownerOrgs=new Set;ownerUsers=new Set;users=new Set;slaveOrgs=new Set;ownerIds=[];update({_id:e,title:t,owners:i,...s}){if(Object.assign(this,s),t!==void 0&&(this.title=t,this.initials=t?.[0]?.toUpperCase()??"",this.displayLabel=t),i){this.ownerIds=i;let o=this.ownerIds.join(",");o!==se.get(this)&&(se.set(this,o),this.orgs.users.isInited&&this.orgs.update())}}async delete(){this.orgs.delete(this._id),se.delete(this),await this.orgs.replace(this._id),this.orgs.update()}};var j={required:["_id","title","note","owners","meta","createdAt"],properties:{_id:{bsonType:"string",pattern:c._id.source},title:{bsonType:"string",maxLength:512},note:{bsonType:"string",maxLength:131072},owners:{bsonType:"array",uniqueItems:!0,items:{bsonType:"string",pattern:c._id.source},additionalItems:!1},meta:{bsonType:"object"},createdAt:{bsonType:"number"}},additionalProperties:!1};var Ie=[[{owners:1}]];function xe(){throw new Error("Direct usage of orgs collection.deleteOne or .deleteMany is forbidden. Use orgs.deleteOrg instead.")}var L=class extends Map{constructor(e,t={}){super(),this.users=e,this.collections=e.collections,this.preinitOptions=t}users;collections;preinitOptions;collection;null;#e=!1;async preinit(){if(!this.#e){this.#e=!0;let{schema:e,indexes:t,null:i,collection:s}=this.preinitOptions;e&&(e.required&&h(e.required,j.required),e.properties&&u(e,Object.keys(j.properties)));let o={...j,...e,required:[...j.required,...e?.required??[]],properties:{...j.properties,...e?.properties}};this.collection=Object.assign(await this.collections.ensure("orgs",{...s,schema:o}),{deleteOne:xe,deleteMany:xe}),await this.collection.ensureIndexes([...Ie,...t??[]]),this.null=Object.assign(new m(this,{...i,_id:"",title:"",note:"",owners:[],meta:{},createdAt:Date.now()}),{_id:null,ownerIds:new C,ownerOrgs:new _,ownerUsers:new _,slaveOrgs:new _}),await this.#t();for await(let n of this.collection.find())this.load(n);delete this.preinitOptions}}async#t(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}init(){this.users.isInited||(this.update(),this.collection.onChange(this.#i))}#i=e=>{switch(e.operationType){case"insert":{new m(this,e.fullDocument);break}case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};load(e){new m(this,e)}#o(e,t){t=[e,...t];for(let i of e.ownerOrgs){this.#o(i,t);for(let s of t)i.slaveOrgs.add(s)}}sorted=[];update(){let e=[...this.values()],t=[];for(let i of e){i.ownerOrgs.clear(),i.ownerUsers.clear(),i.slaveOrgs.clear();for(let s of i.ownerIds){let o=this.get(s);if(o)i.ownerOrgs.add(o);else{let n=this.users.get(s);n&&i.ownerUsers.add(n)}}!i.ownerOrgs.size&&i._id&&t.push(i)}for(let i of e)e.some(s=>s.ownerOrgs.has(i))||this.#o(i,[]);t.sort(this.#s);for(let i of e)be(i.slaveOrgs,this.#s);for(let i=0;i<t.length;i++){let s=t[i];for(let o of[...s.slaveOrgs].reverse())if(o.ownerOrgs.has(s)){let n=t.indexOf(o);~n&&(t.splice(n,1),i--),t.splice(i+1,0,o)}}for(let i=0,{length:s}=t;i<s;)t[i]._o=i++;this.sorted=t,this.users.isInited&&this.users.update()}#s(e,t){return t.slaveOrgs.size-e.slaveOrgs.size||(e.title>t.title?1:e.title<t.title?-1:0)}async create({title:e,note:t,...i}){let s=De();return await this.collection.insertOne({_id:s,title:e??"",note:t??"",meta:{},...u(i,["_id"]),createdAt:Date.now()}),s}async updateOrg(e,t,i){let s=this.get(e);return s?(t.owners&&(t.owners=this.users.sortIds(ge(i?k(s.ownerIds,i.slaveIds):[],k(t.owners,[e,...O(s.slaveOrgs)])))),await this.collection.updateOne({_id:e},{$set:t}),!0):!1}deleteOrg(e){let t=typeof e=="string"?e:e._id;return this.collection.bulkWrite([{updateMany:{filter:{owners:t},update:{$pullAll:{owners:[t]}}}},{deleteOne:{filter:{_id:t}}}])}replaceMap=new Map;replaceHandlers=new Set;async replace(e){let t=this.replaceMap.get(e)??null;t&&this.replaceMap.delete(e);for(let i of this.replaceHandlers)await i(e,t)}};import{randomBytes as je}from"node:crypto";import{newObjectIdString as Re}from"insite-db";var R={required:["_id","user","remoteAddress","isOnline","meta","createdAt","prolongedAt","expiresAt"],properties:{_id:{bsonType:"string",pattern:c.sessionId.source},user:{bsonType:"string",pattern:c._id.source},userAgent:{bsonType:"string",maxLength:4096},remoteAddress:{bsonType:"string",pattern:c.ip.source},isOnline:{bsonType:"bool"},meta:{bsonType:"object"},createdAt:{bsonType:"number"},prolongedAt:{bsonType:"number"},expiresAt:{bsonType:"date"}}};var $=class{constructor(e,t){let i=e.users.get(t.user);i?(this.#e=e,this._id=t._id,this.#e.set(this._id,this),this.user=i,this.user.sessions.add(this),this.#e.users.bySessionId.set(this._id,this.user),this.update(t)):e.collection.deleteOne({_id:t._id})}_id;user;isOnline=!1;userAgent;remoteAddress;createdAt;prolongedAt;expiresAt;#e;update({_id:e,user:t,isOnline:i,...s}){i!==void 0&&this.isOnline!==i&&(this.isOnline=i,this.#e.users.isInited&&this.user.isOnline!==this.isOnline&&this.user.updateIsOnlineDebounced()),Object.assign(this,s)}prolong({_id:e,user:t,...i}){let s=Date.now();return i.prolongedAt=s,i.expiresAt=new Date(s+this.#e.collection.expireAfterSeconds*1e3),this.update(i),this.#e.collection.updateOne({_id:this._id},{$set:i})}async online(){if(!this.isOnline){let e={isOnline:!0};this.update(e),await this.#e.collection.updateOne({_id:this._id},{$set:e})}}async offline(){if(this.isOnline){let e={isOnline:!1};this.update(e),await this.#e.collection.updateOne({_id:this._id},{$set:e})}}delete(){this.#e.delete(this._id),this.user.sessions.delete(this),this.#e.users.bySessionId.delete(this._id),this.#e.users.emit("session-delete",this),this.isOnline&&this.user.updateIsOnline()}};var oe=3600*24*7,$e=[[{user:1}],[{expiresAt:1},{expireAfterSeconds:oe}]],B=class extends Map{constructor(e,t={}){super(),this.users=e,this.collections=e.collections,this.initOptions=t}users;collections;initOptions;collection;async init(){if(!this.users.isInited){let{schema:e,indexes:t,collection:i}=this.initOptions;e&&(e.required&&h(e.required,R.required),e.properties&&u(e,Object.keys(R.properties)));let s={...R,...e,required:[...R.required,...e?.required??[]],properties:{...R.properties,...e?.properties}};this.collection=Object.assign(await this.collections.ensure("sessions",{...i,schema:s}),{expireAfterSeconds:oe}),await this.collection.ensureIndexes([...$e,...t??[]]),await this.#e();for await(let o of this.collection.find())this.load(o);this.collection.onChange(this.#t),delete this.initOptions}}async#e(){await this.collection.bulkWrite([{updateMany:{filter:{meta:{$exists:!1}},update:{$set:{meta:{}}}}},{updateMany:{filter:{isOnline:!0},update:{$set:{isOnline:!1}}}}])}#t=e=>{switch(e.operationType){case"insert":this.has(e.documentKey._id)||new $(this,e.fullDocument),this.users.get(e.fullDocument.user)?.trimSessions();break;case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};uid(){return`${Re()}~${je(32).toString("base64url")}`}load(e){new $(this,e)}async create(e,t){let i=Date.now(),s={userAgent:"",remoteAddress:"127.0.0.1",isOnline:!0,...t,_id:this.uid(),user:e._id,meta:{},createdAt:i,prolongedAt:i,expiresAt:new Date(i+oe*1e3)},o=new $(this,s);return await this.collection.insertOne(s),o}async destroySession(e){let{deletedCount:t}=await this.collection.deleteOne({_id:e});return!!t}};import{Binary as Te}from"insite-db";var we={required:["_id","type","size","ts","data","meta"],properties:{_id:{bsonType:"string",pattern:c._id.source},type:{bsonType:"string",maxLength:256},size:{bsonType:"number"},ts:{bsonType:"string",maxLength:10},data:{bsonType:"binData"},meta:{bsonType:"object"}},additionalProperties:!1};var z=class{constructor(e,t={}){this.#e=e,this.collections=e.collections,this.initOptions=t}TYPES_ACCEPTED=["image/webp"];MAX_SIZE=1024*512;#e;collections;collection;initOptions;async init(){if(!this.#e.isInited){let{collection:e}=this.initOptions;this.collection=await this.collections.ensure("users.avatars",{...e,schema:we}),await this.#t()}}async#t(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}async save(e,t,i){let s=Te.createFromBase64(i.slice(i.indexOf(","))),o=Date.now().toString(36);await Promise.all([this.collection.replaceOne({_id:e},{type:t,size:s.length(),ts:o,data:s,meta:{}},{upsert:!0}),this.#e.collection.updateOne({_id:e},{$set:{avatar:o}})])}async deleteAvatar(e){let[{deletedCount:t}]=await Promise.all([this.collection.deleteOne({_id:e}),this.#e.collection.updateOne({_id:e},{$set:{avatar:null}})]);return!!t}};var T={required:["_id","email","password","roles","name","org","job","meta","createdAt"],properties:{_id:{bsonType:"string",pattern:c._id.source},email:{bsonType:"string",pattern:c.email.source},password:{bsonType:"string",pattern:c.argon2.source},roles:{bsonType:"array",uniqueItems:!0,items:{bsonType:"string",pattern:c.role.source},additionalItems:!1},name:{bsonType:"object",required:["first","middle","last"],properties:{first:{bsonType:"string",maxLength:512},middle:{bsonType:"string",maxLength:512},last:{bsonType:"string",maxLength:512}},additionalProperties:!1},org:{oneOf:[{bsonType:"string",pattern:c._id.source},{bsonType:"null"}]},job:{bsonType:"string",maxLength:512},avatar:{oneOf:[{bsonType:"string",maxLength:10},{bsonType:"null"}]},meta:{bsonType:"object"},createdAt:{bsonType:"number"}},additionalProperties:!1};var re=new WeakMap,ne=new WeakMap,Ee=process.env.INSITE_STATIC_URL??"",P=class{constructor(e,t){this.#e=e,this._id=t._id,e.set(this._id,this),this.update(t),e.isInited&&e.emit("user-create",this)}_id;email;name={first:"",middle:"",last:""};initials="";displayLabel="";org;job="";avatar=null;avatarUrl=null;#e;isUser=!0;isRoot;roles=new Set;ownRoles=[];ownRoleIds=[];slaveRoles=new Set;slaveRoleIds=[];abilities={};slaveOrgs=new Set;slaveUsers=new Set;slaves=new Set;slaveIds=[];permissiveIds=[];sessions=new Set;isOnline=!1;update({_id:e,name:t,email:i,roles:s,org:o,avatar:n,...l}){Object.assign(this,l);let a;i&&this.email!==i&&(this.#e.byEmail.delete(this.email),this.email=i,this.#e.byEmail.set(i,this),a=!0);let d,g;t&&(this.name.first!==t.first||this.name.middle!==t.middle||this.name.last!==t.last)&&(g=this.name.last!==t.last,this.name={first:t.first??"",middle:t.middle??"",last:t.last??""},this.initials=((t.first?.[0]??"")+(t.last?.[0]??"")||this.email[0]).toUpperCase(),d=!0),(a||d)&&(this.displayLabel=`${this.name.first} ${this.name.last}`.trim()||this.name.middle||this.email);let A;if(s){this.ownRoleIds=s;let w=this.ownRoleIds.join(",");w!==re.get(this)&&(re.set(this,w),this.#e.isInited&&(this.updateRoles(),A=!0))}let p;o!==void 0&&this.org?._id!==o&&(this.org?.users.delete(this),this.org=o&&this.#e.orgs.get(o)||this.#e.orgs.null,this.org.users.add(this),p=!0),n!==void 0&&this.avatar!==n&&(this.avatar=n,this.avatarUrl=n&&`${Ee}/avatars/${this._id}-${n}.webp`),this.#e.isInited&&(A||p?(this.updatePermissions(),g&&(this.#e.isSortRequired=!0),this.#e.update()):g&&this.#e.sort())}updateRoles(){this.ownRoles=[],this.roles.clear(),this.slaveRoles.clear(),U(this.abilities);for(let e of this.ownRoleIds){let t=this.#e.roles.get(e);if(t){this.ownRoles.push(t),this.roles.add(t);for(let i of t.involves)this.roles.add(i),this.slaveRoles.add(i);this.slaveRoleIds=O(this.slaveRoles),this.#e.abilities.merge(this.abilities,t.abilities)}}this.roles.has(this.#e.roles.root)&&(this.isRoot=!0),this.trimSessions()}updatePermissions(){this.slaveOrgs.clear(),this.slaveUsers.clear(),this.slaves.clear();let e=new Set;for(let s of this.#e.orgs.sorted)if((s.ownerUsers.has(this)||this.isRoot)&&!this.slaveOrgs.has(s)){this.slaveOrgs.add(s),this.slaves.add(s),e.add(s._id);for(let o of s.slaveOrgs)this.slaveOrgs.add(o),this.slaves.add(o),e.add(o._id)}for(let s of new Set([this.org,...this.slaveOrgs,this.#e.orgs.null]))for(let o of s.users)o!==this&&(ue(this.slaveRoles,o.ownRoles)||!o.ownRoles.length)&&(this.slaveUsers.add(o),this.slaves.add(o),e.add(o._id));this.slaveIds=[...e];let t=[];if(this.org._id){t.push(this.org._id);for(let s of this.org.slaveOrgs)this.slaveOrgs.has(s)||t.push(s._id)}this.permissiveIds=[...t,this._id,...e];let i=[this.permissiveIds.join(","),this.ownRoleIds.join(","),JSON.stringify(this.abilities),this.slaveRoleIds.join(",")].join(`
3
- `);i!==ne.get(this)&&(ne.set(this,i),this.#e.isInited&&this.#e.emit("user-permissions-change",this))}trimSessions(){let e=this.abilities.login?.sessionsLimit??0;if(this.sessions.size>e)return this.#e.sessions.collection.deleteMany({_id:{$in:O([...this.sessions].sort((t,i)=>t.expiresAt.valueOf()-i.expiresAt.valueOf()).slice(0,this.sessions.size-e))}})}updateIsOnline(){let e=!1;for(let t of this.sessions)if(t.isOnline){e=!0;break}this.isOnline!==e&&(this.isOnline=e,this.#e.emit("user-is-online",this))}updateIsOnlineDebounced=K(()=>this.updateIsOnline(),500);async delete(){this.org.users.delete(this),this.#e.delete(this._id),this.#e.byEmail.delete(this.email),ye(this.#e.sorted,this),re.delete(this),ne.delete(this),await this.#e.replace(this._id),this.#e.update()}};var Ce=[[{email:1},{unique:!0}],[{org:1}]],le=class r extends Map{constructor(e,t){super();let i=new Me;this.#e=i,this._events=i._events,this._eventsCount=i._eventsCount,this.emit=i.emit,this.on=i.on,this.once=i.once,this.addListener=i.addListener,this.off=i.off,this.removeListener=i.removeListener,this.collections=e,this.abilities=new W(t.abilities),this.initOptions=t,this.init()}#e;_events;_eventsCount;emit;on(e,t,i){return this.#e.on(e,t,i),this}once;addListener;off;removeListener;initOptions;collections;abilities;collection;roles;sessions;orgs;avatars;byEmail=new Map;bySessionId=new Map;sorted=[];isSortRequired=!0;#t=!1;async preinit(){if(!this.#t){this.#t=!0;let{indexes:e,schema:t,initialRoot:i,collection:s}=this.initOptions;t&&(t.required&&h(t.required,T.required),t.properties&&u(t,Object.keys(T.properties)));let o={...T,...t,required:[...T.required,...t?.required??[]],properties:{...T.properties,...t?.properties}};this.collection=await this.collections.ensure("users",{...s,schema:o}),await this.collection.ensureIndexes([...Ce,...e??[]]),await this.collection.countDocuments({roles:"root"})||await this.create({email:process.env.INSITE_ROOT_EMAIL??"insite@root.email",password:process.env.INSITE_ROOT_PASSWORD??"inSiteRootPassword",name:{first:"Root"},job:"Root",...i,roles:["root"],org:null,meta:{}}),await this.#i();for await(let n of this.collection.find())this.load(n)}}async#i(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}isInited=!1;async init(){if(!this.isInited){let{roles:e,orgs:t,sessions:i,avatars:s,collection:{quiet:o}={}}=this.initOptions;this.roles=new G(this,{...e,collection:{quiet:o,...e?.collection}}),this.orgs=new L(this,{...t,collection:{quiet:o,...t?.collection}}),this.sessions=new B(this,{...i,collection:{quiet:o,...i?.collection}}),this.avatars=new z(this,{...s,collection:{quiet:o,...s?.collection}}),await this.roles.init(),await this.orgs.preinit(),await this.preinit(),this.orgs.init(),await this.avatars.init(),this.update(!0),this.collection.onChange(this.#o),await this.sessions.init(),delete this.initOptions,this.isInited=!0,this.#s.resolve(this)}}#o=e=>{switch(e.operationType){case"insert":new P(this,e.fullDocument);break;case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};load(e){new P(this,e)}sort(){this.isSortRequired=!1,this.sorted=[...this.values()].sort((e,t)=>e.name.last>t.name.last?1:e.name.last<t.name.last?-1:0)}update(e){if(e)for(let t of this.values())t.updateRoles();for(let t of this.values())t.updatePermissions();this.isSortRequired&&this.sort()}#s=new x;whenReady(){return this.#s}async create({email:e,password:t,roles:i,name:s,org:o,job:n,...l}){if(this.byEmail.has(e))throw new Error("User exists");let a=Ue();return await this.collection.insertOne({_id:a,email:e,password:await ve(t),roles:this.isInited?this.roles.cleanUpIds(i):i,name:{first:s.first??"",middle:s.middle??"",last:s.last??""},org:o??null,job:n??"",meta:{},...u(l,["_id"]),createdAt:Date.now()}),a}async updateUser(e,t,i){let s=this.get(e);return s?(t.roles&&(t.roles=s.isRoot?["root"]:this.roles.cleanUpIds(i?k(s.ownRoleIds,i.slaveRoleIds).concat(t.roles):t.roles)),await this.collection.updateOne({_id:e},{$set:t}),!0):!1}async changePassword(e,t){if(!this.has(e))throw new Error("usernotfound");if(!t)throw new Error("emptypasswordisnotallowed");await Promise.all([this.collection.updateOne({_id:e},{$set:{password:await ve(t)}}),this.sessions.collection.deleteMany({user:e})])}async login(e,t,i){let s=this.byEmail.get(e);if(!s)throw new Error("usernotfound");if(!await ke(s.password,t))throw new Error("incorrectpassword");if(!s.abilities.login)throw new Error("userisnotabletologin");return this.sessions.create(s,i)}logout(e){return e.delete(),this.sessions.collection.deleteOne({_id:e._id})}#r(e,t){return e instanceof P&&t instanceof P?e.name.last&&t.name.last?e.name.last>t.name.last?1:e.name.last<t.name.last?-1:0:!e.name.last&&!t.name.last?e.displayLabel>t.displayLabel?1:e.displayLabel<t.displayLabel?-1:0:e.name.last?-1:1:e instanceof m&&t instanceof m?e.title>t.title?1:e.title<t.title?-1:0:e instanceof m?-1:1}sortIds(e){return O(e.map(t=>this.get(t)||this.orgs.get(t)).filter(Boolean).sort(this.#r))}replaceMap=new Map;replaceHandlers=new Set;async replace(e){let t=this.replaceMap.get(e)??null;t&&this.replaceMap.delete(e);for(let i of this.replaceHandlers)await i(e,t)}async deleteUser(e){let{deletedCount:t}=await this.collection.deleteOne({_id:e});return!!t}static init(e,t){return new r(e,t).whenReady()}};export{H as AbilityError,ie as PermissionError,Y as RolesError,te as SubordinationError,ee as UnauthorizedError,le as Users};
1
+ import{Err as se}from"@nesvet/n";var P=class extends se{constructor(e,t){typeof e=="object"&&(t=e,e=void 0),super("User lacks ability","lacksability",{...e&&{ability:e},...t})}name="AbilityError"};import{add as oe,assignPath as re,deletePath as ne,empty as ae,getPath as B,intersection as le,pick as ce,setPath as de}from"@nesvet/n";function x(l){return typeof l=="object"}function D(l){return typeof l=="number"}function I(l){return Array.isArray(l)}var F=new Map,S=class extends Map{constructor(e){super();for(let t of e)delete t.top,this.#e.push(t);(function t(i,s,o){for(let r of i){if(F.set(r,s),r.level=o,!s||r.top){if(r.longId=r._id,this.has(r._id))throw new Error(`Top-level ability duplicate: ${r._id}`);this.set(r._id,r)}else r.longId=s?`${s.longId}.${r._id}`:r._id;r.abilities&&t.call(this,r.abilities,r,o+1)}}).call(this,e,null,0)}#e=[];get(e,t){let i=e.split("."),s=t??super.get(i.shift());if(s&&i.length){for(let o of i)if(s=s?.abilities?.find(({_id:r,top:a})=>!a&&r===o),!s)break}return s}getSchema=this.get;getParamSchema(e,t){return this.get(e)?.params?.find(({_id:i})=>i===t)}getDefaultParam(e,t,i){(t===void 0||typeof t=="boolean")&&(i=t);let s=typeof e=="string"?this.getParamSchema(e,t):e;if(s)switch(s.type){case"number":return i?s.max??1/0:s.min??0;case"items":return i?s.items.map(({_id:o})=>o):[]}}getDefaultAbility(e,t=!e){if(typeof e=="string"&&(e=this.get(e)),e){if(e.abilities||e.params){let i={};if(e.params)for(let s of e.params)i[s._id]=this.getDefaultParam(s,t);if(t&&e.abilities)for(let s of e.abilities)s.top||(i[s._id]=this.getDefaultAbility(s,t));return i}return!0}}getDefaultAbilities(e,t=!e){let i=(e?(Array.isArray(e)?e:[e]).map(o=>typeof o=="string"?this.get(o):o).filter(Boolean):[...this.values()]).sort((o,r)=>o.level-r.level),s=new Map;for(let o of i)(function r(a){let n=F.get(a);n&&(r(n),s.has(n)||s.set(n,!1))})(o),t?(function r(a){if(s.has(a)||s.set(a,!0),a.abilities)for(let n of a.abilities)n.top&&r(n)})(o):s.has(o)||s.set(o,!1);return re({},Object.fromEntries([...s].map(([o,r])=>[o.longId,this.getDefaultAbility(o,r)])))}getSchemeFor(e){return e=this.adjust(structuredClone(e)),(function t(i,s){for(let o=0;o<i.length;o++){let r=i[o],a=r.top?e[r._id]:s[r._id];if(a){if(r.params)for(let n of r.params){let d=a[n._id];switch(n.type){case"number":n.max=d;break;case"items":n.items=n.items.filter(({_id:A})=>d.includes(A))}}r.abilities&&(t(r.abilities,a),r.abilities.length||delete r.abilities)}else i.splice(o--,1)}return i})(structuredClone(this.#e),e)}getParam(e,t,i){if(this.getParamSchema(t,i)){let s=B(e,`${t}.${i}`);return this.isParamFits(t,i,s)?s:this.getDefaultParam(t,i)}}setParam(e,t,i,s){let o=this.getParamSchema(t,i);if(!o)throw new Error(`No such ability param: ${t}.${i}`);return de(e,`${t}.${i}`,this.adjustParam(o,s)),e}isParamFits(e,t,i,s){if(i!==void 0){let o=this.getParamSchema(e,t);if(o)switch(o.type){case"number":return D(i)&&(o.min===void 0||i>=o.min)&&(s===void 0?o.max===void 0||i<=o.max:i<=s);case"items":return I(i)&&i.length===new Set(i).size&&(s===void 0?i.every(r=>o.items.find(({_id:a})=>a===r)):i.every(r=>s.includes(r)))}}return!1}adjustParam(e,t){return e.type==="number"?D(t)?Math.max(Math.min(t,e.max??1/0),e.min??0):e.min??0:I(t)?le(e.items.map(i=>i._id),t):[]}adjust(e,t){let i={};for(let[s,o]of Object.entries(e)){let r=this.get(s,t);if(r)if(r.params||r.abilities){let a={};if(r.params)if(x(o))for(let n of r.params)a[n._id]=this.adjustParam(n,o[n._id]);else for(let n of r.params)a[n._id]=this.getDefaultParam(n);r.abilities&&x(o)&&Object.assign(a,this.adjust(ce(o,r.abilities.filter(n=>!n.top).map(n=>n._id)),r)),i[s]=a}else i[s]=!0}return Object.assign(ae(e),i)}merge(e,t,i){i||(this.adjust(e),t=this.adjust(structuredClone(t)));for(let[s,o]of Object.entries(t)){let r=this.get(s,i);if(r&&r.isInheritable!==!1)if(s in e&&(r.params||r.abilities)){let a=e[s],n=o;if(r.params){for(let d of r.params)if(d.isInheritable!==!1)switch(d.type){case"number":a[d._id]=Math.max(a[d._id],n[d._id]);break;case"items":oe(a[d._id],...n[d._id])}}r.abilities&&this.merge(a,n,r)}else e[s]=o}return e}hasAbility(e,t){return!!B(e,t)}setAbility(e,t){let i=typeof t=="string"?this.get(t):t;if(!i)throw new Error(`No such ability: ${t}`);return this.merge(e,this.getDefaultAbilities(i)),e}unsetAbility(e,t){let i=typeof t=="string"?this.get(t):t;if(!i)throw new Error(`No such ability: ${t}`);return(function s(o){if(ne(e,o.longId),o.abilities)for(let r of o.abilities)s(r)})(i),e}};import{Err as pe}from"@nesvet/n";var R=class extends pe{constructor(e,t){super(`User lacks role${Array.isArray(e)?"s":""}`,"lacksroles",{roles:Array.isArray(e)?e:[e],...t})}name="RolesError"};import{deleteProps as H,getAll as Z,intersection as he,reAdd as me,removeAll as T}from"@nesvet/n";var U=new WeakMap,m=class{constructor(e,t){this.#e=e,this._id=t._id,e.set(this._id,this),this.update(t)}#e;_id;ownInvolveIds;ownAbilities;title="";displayTitle="";_o=0;ownInvolves=[];involves=new Set;abilities={};inheritedAbilities={};update({_id:e,involves:t,abilities:i,title:s,...o}){if(Object.assign(this,o),t&&(this.ownInvolveIds=t),i&&(this.ownAbilities=i),s!==void 0&&(this.title=s,this.displayTitle=s||this._id),t||i){let r=[this.ownInvolveIds.join(","),JSON.stringify(this.ownAbilities)].join(`
2
+ `);r!==U.get(this)&&(U.set(this,r),this.#e.users.isInited&&this.#e.update())}}delete(){this.#e.delete(this._id),U.delete(this),this.#e.update()}};var c={_id:/^[\dA-Fa-f]{24}$/,email:/^[^\s@]{1,63}@[^\s@]{1,63}\.[^\s@]{1,63}$/,argon2:/^\$argon2(?:i|d|id)\$v=\d+\$m=\d+,t=\d+,p=\d+(?:\$[\d+/A-Za-z]+){2}$/,sessionId:/^(?:[\dA-Fa-f]{16,32}\$[\dA-Fa-f]{16,32}|[0-9a-f]{24}~[A-Za-z0-9_-]{43})$/,role:/^[\d_a-z-]{1,64}$/,ip:/^(?:\d{1,3}\.){3}\d{1,3}$/};var u={required:["_id","involves","abilities","title","description","meta","createdAt"],properties:{_id:{bsonType:"string",pattern:c.role.source,not:{enum:["root"]}},involves:{bsonType:"array",uniqueItems:!0,items:{bsonType:"string",pattern:c.role.source,not:{enum:["root"]}},additionalItems:!1},abilities:{bsonType:"object"},title:{bsonType:"string",maxLength:256},description:{bsonType:"string",maxLength:10240},meta:{bsonType:"object"},createdAt:{bsonType:"number"}},additionalProperties:!1};var ue=[[{involves:1}]];function J(){throw new Error("Direct usage of roles collection.deleteOne or .deleteMany is forbidden. Use roles.deleteRole instead.")}var w=class extends Map{constructor(e,t={}){super();let{root:i,...s}=t;this.users=e,this.collections=e.collections,this.abilities=e.abilities,this.root=new m(this,{...i,_id:"root",involves:[],abilities:this.abilities.getDefaultAbilities(),title:"Root",description:"Root",meta:{},createdAt:Date.now()}),this.initOptions=s}users;collections;collection;abilities;root;initOptions;async init(){if(!this.users.isInited){let{schema:e,indexes:t,collection:i}=this.initOptions;e&&(e.required&&T(e.required,u.required),e.properties&&H(e,Object.keys(u.properties)));let s={...u,...e,required:[...u.required,...e?.required??[]],properties:{...u.properties,...e?.properties}};this.collection=Object.assign(await this.collections.ensure("roles",{...i,schema:s}),{deleteOne:J,deleteMany:J}),await this.collection.ensureIndexes([...ue,...t??[]]),await this.#e();for await(let o of this.collection.find())this.abilities.adjust(o.abilities)&&await this.collection.updateOne({_id:o._id},{$set:{abilities:o.abilities}}),this.load(o);this.update(),this.collection.onChange(this.#t),delete this.initOptions}}async#e(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}#t=e=>{switch(e.operationType){case"insert":{new m(this,e.fullDocument);break}case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};load(e){new m(this,e)}resolve(e,t){t=[e,...t];for(let i of e.ownInvolveIds){let s=this.get(i);if(s){for(let o of t)me(o.involves,s),this.abilities.merge(o.inheritedAbilities,s.abilities);s!==e&&this.resolve(s,t)}}}sorted=[];#i(e,t){return t.involves.size-e.involves.size||(e.title&&t.title?e.title>t.title?1:e.title<t.title?-1:0:e.title?-1:t.title||e._id>t._id?1:e._id<t._id?-1:0)}update(){let e=[...this.values()];this.root.ownInvolveIds=[];for(let s of e)s!==this.root&&!e.some(o=>o.ownInvolveIds.includes(s._id))&&this.root.ownInvolveIds.push(s._id),s.involves.clear(),s.abilities=structuredClone(s.ownAbilities),s.inheritedAbilities={};this.root.involves.add(this.root),this.resolve(this.root,[]);for(let s of e)s.ownInvolves=Z(this,s.ownInvolveIds,!0).sort(this.#i).reverse();let t=[this.root];for(let s=0;s<t.length;s++){let o=t[s];for(let r of o.ownInvolves){let a=t.indexOf(r);~a&&(t.splice(a,1),s--),t.splice(s+1,0,r)}}let i=0;for(let s of t)this.abilities.merge(s.abilities,s.inheritedAbilities),s._o=i++;this.sorted=t,this.users.isInited&&this.users.update(!0)}async create({_id:e,involves:t,title:i,description:s,...o}){return await this.collection.insertOne({_id:e,involves:t||[],abilities:{},title:i??"",description:s??"",meta:{},...H(o,["abilities"]),createdAt:Date.now()}),e}async updateRole(e,t){let i=this.get(e);if(!i)return!1;if(t.involves){T(t.involves,[e]);for(let s of t.involves)this.get(s)?.involves.has(i)&&T(t.involves,[s]);t.involves=this.cleanUpIds(t.involves)}return await this.collection.updateOne({_id:e},{$set:t}),!0}deleteRole(e){let t=typeof e=="string"?e:e._id;return this.collection.bulkWrite([{updateMany:{filter:{involves:t},update:{$pullAll:{involves:[t]}}}},{deleteOne:{filter:{_id:t}}}])}cleanUpIds(e){let t=he(this.sorted,Z(this,e,!0)),i=[];for(let s of t)t.every(o=>o===s||!o.involves.has(s))&&i.push(s._id);return i}async setAbility(e,t,i){let s=this.get(e);return s?(await this.collection.updateOne({_id:e},{$set:{abilities:i?this.abilities.setAbility(s.ownAbilities,t):this.abilities.unsetAbility(s.ownAbilities,t)}}),!0):!1}async setAbilityParam(e,t,i,s){let o=this.get(e);return o?(await this.collection.updateOne({_id:e},{$set:{abilities:this.abilities.setParam(o.ownAbilities,t,i,s)}}),!0):!1}};import{Err as E}from"@nesvet/n";var C=class extends E{constructor(e){super("Unauthorized","unauthorized",e)}name="UnauthorizedError"},j=class extends E{constructor(e,t,i){switch(arguments.length){case 1:i=e,e=void 0;break;case 2:i=t,t=void 0}super("Insubordination attempt","subordination",{...e&&{type:e},...t&&{ids:Array.isArray(t)?t:[t]},...i})}name="SubordinationError"},$=class extends E{constructor(e){super("Insufficient permissions","permissions",e)}name="PermissionError"};import{hash as te,verify as je}from"@node-rs/argon2";import $e from"eventemitter3";import{_ids as Ee,deleteProps as ie,removeAll as Me,StatefulPromise as ke,without as qe}from"@nesvet/n";import{newObjectIdString as Ge}from"insite-db";import{_ids as fe,deleteProps as X,EmptyArray as be,EmptySet as k,removeAll as ye,sort as ge,union as Ae,without as Y}from"@nesvet/n";import{newObjectIdString as Se}from"insite-db";var M=new WeakMap,p=class{constructor(e,t){this.orgs=e,this._id=t._id,e.set(this._id,this),this.update(t)}_id;title="";initials="";displayLabel="";_o=0;orgs;isOrg=!0;ownerOrgs=new Set;ownerUsers=new Set;users=new Set;slaveOrgs=new Set;ownerIds=[];update({_id:e,title:t,owners:i,...s}){if(Object.assign(this,s),t!==void 0&&(this.title=t,this.initials=t?.[0]?.toUpperCase()??"",this.displayLabel=t),i){this.ownerIds=i;let o=this.ownerIds.join(",");o!==M.get(this)&&(M.set(this,o),this.orgs.users.isInited&&this.orgs.update())}}async delete(){this.orgs.delete(this._id),M.delete(this),await this.orgs.replace(this._id),this.orgs.update()}};var f={required:["_id","title","note","owners","meta","createdAt"],properties:{_id:{bsonType:"string",pattern:c._id.source},title:{bsonType:"string",maxLength:512},note:{bsonType:"string",maxLength:131072},owners:{bsonType:"array",uniqueItems:!0,items:{bsonType:"string",pattern:c._id.source},additionalItems:!1},meta:{bsonType:"object"},createdAt:{bsonType:"number"}},additionalProperties:!1};var we=[[{owners:1}]];function Q(){throw new Error("Direct usage of orgs collection.deleteOne or .deleteMany is forbidden. Use orgs.deleteOrg instead.")}var v=class extends Map{constructor(e,t={}){super(),this.users=e,this.collections=e.collections,this.preinitOptions=t}users;collections;preinitOptions;collection;null;#e=!1;async preinit(){if(!this.#e){this.#e=!0;let{schema:e,indexes:t,null:i,collection:s}=this.preinitOptions;e&&(e.required&&ye(e.required,f.required),e.properties&&X(e,Object.keys(f.properties)));let o={...f,...e,required:[...f.required,...e?.required??[]],properties:{...f.properties,...e?.properties}};this.collection=Object.assign(await this.collections.ensure("orgs",{...s,schema:o}),{deleteOne:Q,deleteMany:Q}),await this.collection.ensureIndexes([...we,...t??[]]),this.null=Object.assign(new p(this,{...i,_id:"",title:"",note:"",owners:[],meta:{},createdAt:Date.now()}),{_id:null,ownerIds:new be,ownerOrgs:new k,ownerUsers:new k,slaveOrgs:new k}),await this.#t();for await(let r of this.collection.find())this.load(r);delete this.preinitOptions}}async#t(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}init(){this.users.isInited||(this.update(),this.collection.onChange(this.#i))}#i=e=>{switch(e.operationType){case"insert":{new p(this,e.fullDocument);break}case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};load(e){new p(this,e)}#o(e,t){t=[e,...t];for(let i of e.ownerOrgs){this.#o(i,t);for(let s of t)i.slaveOrgs.add(s)}}sorted=[];update(){let e=[...this.values()],t=[];for(let i of e){i.ownerOrgs.clear(),i.ownerUsers.clear(),i.slaveOrgs.clear();for(let s of i.ownerIds){let o=this.get(s);if(o)i.ownerOrgs.add(o);else{let r=this.users.get(s);r&&i.ownerUsers.add(r)}}!i.ownerOrgs.size&&i._id&&t.push(i)}for(let i of e)e.some(s=>s.ownerOrgs.has(i))||this.#o(i,[]);t.sort(this.#s);for(let i of e)ge(i.slaveOrgs,this.#s);for(let i=0;i<t.length;i++){let s=t[i];for(let o of[...s.slaveOrgs].reverse())if(o.ownerOrgs.has(s)){let r=t.indexOf(o);~r&&(t.splice(r,1),i--),t.splice(i+1,0,o)}}for(let i=0,{length:s}=t;i<s;)t[i]._o=i++;this.sorted=t,this.users.isInited&&this.users.update()}#s(e,t){return t.slaveOrgs.size-e.slaveOrgs.size||(e.title>t.title?1:e.title<t.title?-1:0)}async create({title:e,note:t,...i}){let s=Se();return await this.collection.insertOne({_id:s,title:e??"",note:t??"",meta:{},...X(i,["_id"]),createdAt:Date.now()}),s}async updateOrg(e,t,i){let s=this.get(e);return s?(t.owners&&(t.owners=this.users.sortIds(Ae(i?Y(s.ownerIds,i.slaveIds):[],Y(t.owners,[e,...fe(s.slaveOrgs)])))),await this.collection.updateOne({_id:e},{$set:t}),!0):!1}deleteOrg(e){let t=typeof e=="string"?e:e._id;return this.collection.bulkWrite([{updateMany:{filter:{owners:t},update:{$pullAll:{owners:[t]}}}},{deleteOne:{filter:{_id:t}}}])}replaceMap=new Map;replaceHandlers=new Set;async replace(e){let t=this.replaceMap.get(e)??null;t&&this.replaceMap.delete(e);for(let i of this.replaceHandlers)await i(e,t)}};import{randomBytes as ve}from"node:crypto";import{deleteProps as Oe,removeAll as _e}from"@nesvet/n";import{newObjectIdString as Pe}from"insite-db";var b={required:["_id","user","remoteAddress","isOnline","meta","createdAt","prolongedAt","expiresAt"],properties:{_id:{bsonType:"string",pattern:c.sessionId.source},user:{bsonType:"string",pattern:c._id.source},userAgent:{bsonType:"string",maxLength:4096},remoteAddress:{bsonType:"string",pattern:c.ip.source},isOnline:{bsonType:"bool"},meta:{bsonType:"object"},createdAt:{bsonType:"number"},prolongedAt:{bsonType:"number"},expiresAt:{bsonType:"date"}}};var y=class{constructor(e,t){let i=e.users.get(t.user);i?(this.#e=e,this._id=t._id,this.#e.set(this._id,this),this.user=i,this.user.sessions.add(this),this.#e.users.bySessionId.set(this._id,this.user),this.update(t)):e.collection.deleteOne({_id:t._id})}_id;user;isOnline=!1;userAgent;remoteAddress;createdAt;prolongedAt;expiresAt;#e;update({_id:e,user:t,isOnline:i,...s}){i!==void 0&&this.isOnline!==i&&(this.isOnline=i,this.#e.users.isInited&&this.user.isOnline!==this.isOnline&&this.user.updateIsOnlineDebounced()),Object.assign(this,s)}prolong({_id:e,user:t,...i}){let s=Date.now();return i.prolongedAt=s,i.expiresAt=new Date(s+this.#e.collection.expireAfterSeconds*1e3),this.update(i),this.#e.collection.updateOne({_id:this._id},{$set:i})}async online(){if(!this.isOnline){let e={isOnline:!0};this.update(e),await this.#e.collection.updateOne({_id:this._id},{$set:e})}}async offline(){if(this.isOnline){let e={isOnline:!1};this.update(e),await this.#e.collection.updateOne({_id:this._id},{$set:e})}}delete(){this.#e.delete(this._id),this.user.sessions.delete(this),this.#e.users.bySessionId.delete(this._id),this.#e.users.emit("session-delete",this),this.isOnline&&this.user.updateIsOnline()}};var q=3600*24*7,xe=[[{user:1}],[{expiresAt:1},{expireAfterSeconds:q}]],O=class extends Map{constructor(e,t={}){super(),this.users=e,this.collections=e.collections,this.initOptions=t}users;collections;initOptions;collection;async init(){if(!this.users.isInited){let{schema:e,indexes:t,collection:i}=this.initOptions;e&&(e.required&&_e(e.required,b.required),e.properties&&Oe(e,Object.keys(b.properties)));let s={...b,...e,required:[...b.required,...e?.required??[]],properties:{...b.properties,...e?.properties}};this.collection=Object.assign(await this.collections.ensure("sessions",{...i,schema:s}),{expireAfterSeconds:q}),await this.collection.ensureIndexes([...xe,...t??[]]),await this.#e();for await(let o of this.collection.find())this.load(o);this.collection.onChange(this.#t),delete this.initOptions}}async#e(){await this.collection.bulkWrite([{updateMany:{filter:{meta:{$exists:!1}},update:{$set:{meta:{}}}}},{updateMany:{filter:{isOnline:!0},update:{$set:{isOnline:!1}}}}])}#t=e=>{switch(e.operationType){case"insert":this.has(e.documentKey._id)||new y(this,e.fullDocument),this.users.get(e.fullDocument.user)?.trimSessions();break;case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};uid(){return`${Pe()}~${ve(32).toString("base64url")}`}load(e){new y(this,e)}async create(e,t){let i=Date.now(),s={userAgent:"",remoteAddress:"127.0.0.1",isOnline:!0,...t,_id:this.uid(),user:e._id,meta:{},createdAt:i,prolongedAt:i,expiresAt:new Date(i+q*1e3)},o=new y(this,s);return await this.collection.insertOne(s),o}async destroySession(e){let{deletedCount:t}=await this.collection.deleteOne({_id:e});return!!t}};import{Binary as De}from"insite-db";var V={required:["_id","type","size","ts","data","meta"],properties:{_id:{bsonType:"string",pattern:c._id.source},type:{bsonType:"string",maxLength:256},size:{bsonType:"number"},ts:{bsonType:"string",maxLength:10},data:{bsonType:"binData"},meta:{bsonType:"object"}},additionalProperties:!1};var _=class{constructor(e,t={}){this.#e=e,this.collections=e.collections,this.initOptions=t}TYPES_ACCEPTED=["image/webp"];MAX_SIZE=1024*512;#e;collections;collection;initOptions;async init(){if(!this.#e.isInited){let{collection:e}=this.initOptions;this.collection=await this.collections.ensure("users.avatars",{...e,schema:V}),await this.#t()}}async#t(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}async save(e,t,i){let s=De.createFromBase64(i.slice(i.indexOf(","))),o=Date.now().toString(36);await Promise.all([this.collection.replaceOne({_id:e},{type:t,size:s.length(),ts:o,data:s,meta:{}},{upsert:!0}),this.#e.collection.updateOne({_id:e},{$set:{avatar:o}})])}async deleteAvatar(e){let[{deletedCount:t}]=await Promise.all([this.collection.deleteOne({_id:e}),this.#e.collection.updateOne({_id:e},{$set:{avatar:null}})]);return!!t}};var g={required:["_id","email","password","roles","name","org","job","meta","createdAt"],properties:{_id:{bsonType:"string",pattern:c._id.source},email:{bsonType:"string",pattern:c.email.source},password:{bsonType:"string",pattern:c.argon2.source},roles:{bsonType:"array",uniqueItems:!0,items:{bsonType:"string",pattern:c.role.source},additionalItems:!1},name:{bsonType:"object",required:["first","middle","last"],properties:{first:{bsonType:"string",maxLength:512},middle:{bsonType:"string",maxLength:512},last:{bsonType:"string",maxLength:512}},additionalProperties:!1},org:{oneOf:[{bsonType:"string",pattern:c._id.source},{bsonType:"null"}]},job:{bsonType:"string",maxLength:512},avatar:{oneOf:[{bsonType:"string",maxLength:10},{bsonType:"null"}]},meta:{bsonType:"object"},createdAt:{bsonType:"number"}},additionalProperties:!1};import{_ids as ee,debounce as Ie,empty as Re,hasAny as Ue,removeOne as Te}from"@nesvet/n";var G=new WeakMap,N=new WeakMap,Ce=process.env.INSITE_STATIC_URL??"",h=class{constructor(e,t){this.#e=e,this._id=t._id,e.set(this._id,this),this.update(t),e.isInited&&e.emit("user-create",this)}_id;email;name={first:"",middle:"",last:""};initials="";displayLabel="";org;job="";avatar=null;avatarUrl=null;#e;isUser=!0;isRoot;roles=new Set;ownRoles=[];ownRoleIds=[];slaveRoles=new Set;slaveRoleIds=[];abilities={};slaveOrgs=new Set;slaveUsers=new Set;slaves=new Set;slaveIds=[];permissiveIds=[];sessions=new Set;isOnline=!1;update({_id:e,name:t,email:i,roles:s,org:o,avatar:r,...a}){Object.assign(this,a);let n;i&&this.email!==i&&(this.#e.byEmail.delete(this.email),this.email=i,this.#e.byEmail.set(i,this),n=!0);let d,A;t&&(this.name.first!==t.first||this.name.middle!==t.middle||this.name.last!==t.last)&&(A=this.name.last!==t.last,this.name={first:t.first??"",middle:t.middle??"",last:t.last??""},this.initials=((t.first?.[0]??"")+(t.last?.[0]??"")||this.email[0]).toUpperCase(),d=!0),(n||d)&&(this.displayLabel=`${this.name.first} ${this.name.last}`.trim()||this.name.middle||this.email);let W;if(s){this.ownRoleIds=s;let K=this.ownRoleIds.join(",");K!==G.get(this)&&(G.set(this,K),this.#e.isInited&&(this.updateRoles(),W=!0))}let z;o!==void 0&&this.org?._id!==o&&(this.org?.users.delete(this),this.org=o&&this.#e.orgs.get(o)||this.#e.orgs.null,this.org.users.add(this),z=!0),r!==void 0&&this.avatar!==r&&(this.avatar=r,this.avatarUrl=r&&`${Ce}/avatars/${this._id}-${r}.webp`),this.#e.isInited&&(W||z?(this.updatePermissions(),A&&(this.#e.isSortRequired=!0),this.#e.update()):A&&this.#e.sort())}updateRoles(){this.ownRoles=[],this.roles.clear(),this.slaveRoles.clear(),Re(this.abilities);for(let e of this.ownRoleIds){let t=this.#e.roles.get(e);if(t){this.ownRoles.push(t),this.roles.add(t);for(let i of t.involves)this.roles.add(i),this.slaveRoles.add(i);this.slaveRoleIds=ee(this.slaveRoles),this.#e.abilities.merge(this.abilities,t.abilities)}}this.roles.has(this.#e.roles.root)&&(this.isRoot=!0),this.trimSessions()}updatePermissions(){this.slaveOrgs.clear(),this.slaveUsers.clear(),this.slaves.clear();let e=new Set;for(let s of this.#e.orgs.sorted)if((s.ownerUsers.has(this)||this.isRoot)&&!this.slaveOrgs.has(s)){this.slaveOrgs.add(s),this.slaves.add(s),e.add(s._id);for(let o of s.slaveOrgs)this.slaveOrgs.add(o),this.slaves.add(o),e.add(o._id)}for(let s of new Set([this.org,...this.slaveOrgs,this.#e.orgs.null]))for(let o of s.users)o!==this&&(Ue(this.slaveRoles,o.ownRoles)||!o.ownRoles.length)&&(this.slaveUsers.add(o),this.slaves.add(o),e.add(o._id));this.slaveIds=[...e];let t=[];if(this.org._id){t.push(this.org._id);for(let s of this.org.slaveOrgs)this.slaveOrgs.has(s)||t.push(s._id)}this.permissiveIds=[...t,this._id,...e];let i=[this.permissiveIds.join(","),this.ownRoleIds.join(","),JSON.stringify(this.abilities),this.slaveRoleIds.join(",")].join(`
3
+ `);i!==N.get(this)&&(N.set(this,i),this.#e.isInited&&this.#e.emit("user-permissions-change",this))}trimSessions(){let e=this.abilities.login?.sessionsLimit??0;if(this.sessions.size>e)return this.#e.sessions.collection.deleteMany({_id:{$in:ee([...this.sessions].sort((t,i)=>t.expiresAt.valueOf()-i.expiresAt.valueOf()).slice(0,this.sessions.size-e))}})}updateIsOnline(){let e=!1;for(let t of this.sessions)if(t.isOnline){e=!0;break}this.isOnline!==e&&(this.isOnline=e,this.#e.emit("user-is-online",this))}updateIsOnlineDebounced=Ie(()=>this.updateIsOnline(),500);async delete(){this.org.users.delete(this),this.#e.delete(this._id),this.#e.byEmail.delete(this.email),Te(this.#e.sorted,this),G.delete(this),N.delete(this),await this.#e.replace(this._id),this.#e.update()}};var Ne=[[{email:1},{unique:!0}],[{org:1}]],L=class l extends Map{constructor(e,t){super();let i=new $e;this.#e=i,this._events=i._events,this._eventsCount=i._eventsCount,this.emit=i.emit,this.on=i.on,this.once=i.once,this.addListener=i.addListener,this.off=i.off,this.removeListener=i.removeListener,this.collections=e,this.abilities=new S(t.abilities),this.initOptions=t,this.init()}#e;_events;_eventsCount;emit;on(e,t,i){return this.#e.on(e,t,i),this}once;addListener;off;removeListener;initOptions;collections;abilities;collection;roles;sessions;orgs;avatars;byEmail=new Map;bySessionId=new Map;sorted=[];isSortRequired=!0;#t=!1;async preinit(){if(!this.#t){this.#t=!0;let{indexes:e,schema:t,initialRoot:i,collection:s}=this.initOptions;t&&(t.required&&Me(t.required,g.required),t.properties&&ie(t,Object.keys(g.properties)));let o={...g,...t,required:[...g.required,...t?.required??[]],properties:{...g.properties,...t?.properties}};this.collection=await this.collections.ensure("users",{...s,schema:o}),await this.collection.ensureIndexes([...Ne,...e??[]]),await this.collection.countDocuments({roles:"root"})||await this.create({email:process.env.INSITE_ROOT_EMAIL??"insite@root.email",password:process.env.INSITE_ROOT_PASSWORD??"inSiteRootPassword",name:{first:"Root"},job:"Root",...i,roles:["root"],org:null,meta:{}}),await this.#i();for await(let r of this.collection.find())this.load(r)}}async#i(){await this.collection.updateMany({meta:{$exists:!1}},{$set:{meta:{}}})}isInited=!1;async init(){if(!this.isInited){let{roles:e,orgs:t,sessions:i,avatars:s,collection:{quiet:o}={}}=this.initOptions;this.roles=new w(this,{...e,collection:{quiet:o,...e?.collection}}),this.orgs=new v(this,{...t,collection:{quiet:o,...t?.collection}}),this.sessions=new O(this,{...i,collection:{quiet:o,...i?.collection}}),this.avatars=new _(this,{...s,collection:{quiet:o,...s?.collection}}),await this.roles.init(),await this.orgs.preinit(),await this.preinit(),this.orgs.init(),await this.avatars.init(),this.update(!0),this.collection.onChange(this.#o),await this.sessions.init(),delete this.initOptions,this.isInited=!0,this.#s.resolve(this)}}#o=e=>{switch(e.operationType){case"insert":new h(this,e.fullDocument);break;case"replace":this.get(e.documentKey._id)?.update(e.fullDocument);break;case"update":this.get(e.documentKey._id)?.update(e.updateDescription.updatedFields);break;case"delete":this.get(e.documentKey._id)?.delete()}};load(e){new h(this,e)}sort(){this.isSortRequired=!1,this.sorted=[...this.values()].sort((e,t)=>e.name.last>t.name.last?1:e.name.last<t.name.last?-1:0)}update(e){if(e)for(let t of this.values())t.updateRoles();for(let t of this.values())t.updatePermissions();this.isSortRequired&&this.sort()}#s=new ke;whenReady(){return this.#s}async create({email:e,password:t,roles:i,name:s,org:o,job:r,...a}){if(this.byEmail.has(e))throw new Error("User exists");let n=Ge();return await this.collection.insertOne({_id:n,email:e,password:await te(t),roles:this.isInited?this.roles.cleanUpIds(i):i,name:{first:s.first??"",middle:s.middle??"",last:s.last??""},org:o??null,job:r??"",meta:{},...ie(a,["_id"]),createdAt:Date.now()}),n}async updateUser(e,t,i){let s=this.get(e);return s?(t.roles&&(t.roles=s.isRoot?["root"]:this.roles.cleanUpIds(i?qe(s.ownRoleIds,i.slaveRoleIds).concat(t.roles):t.roles)),await this.collection.updateOne({_id:e},{$set:t}),!0):!1}async changePassword(e,t){if(!this.has(e))throw new Error("usernotfound");if(!t)throw new Error("emptypasswordisnotallowed");await Promise.all([this.collection.updateOne({_id:e},{$set:{password:await te(t)}}),this.sessions.collection.deleteMany({user:e})])}async login(e,t,i){let s=this.byEmail.get(e);if(!s)throw new Error("usernotfound");if(!await je(s.password,t))throw new Error("incorrectpassword");if(!s.abilities.login)throw new Error("userisnotabletologin");return this.sessions.create(s,i)}logout(e){return e.delete(),this.sessions.collection.deleteOne({_id:e._id})}#r(e,t){return e instanceof h&&t instanceof h?e.name.last&&t.name.last?e.name.last>t.name.last?1:e.name.last<t.name.last?-1:0:!e.name.last&&!t.name.last?e.displayLabel>t.displayLabel?1:e.displayLabel<t.displayLabel?-1:0:e.name.last?-1:1:e instanceof p&&t instanceof p?e.title>t.title?1:e.title<t.title?-1:0:e instanceof p?-1:1}sortIds(e){return Ee(e.map(t=>this.get(t)||this.orgs.get(t)).filter(Boolean).sort(this.#r))}replaceMap=new Map;replaceHandlers=new Set;async replace(e){let t=this.replaceMap.get(e)??null;t&&this.replaceMap.delete(e);for(let i of this.replaceHandlers)await i(e,t)}async deleteUser(e){let{deletedCount:t}=await this.collection.deleteOne({_id:e});return!!t}static init(e,t){return new l(e,t).whenReady()}};export{P as AbilityError,$ as PermissionError,R as RolesError,j as SubordinationError,C as UnauthorizedError,L as Users};
4
4
  //# sourceMappingURL=index.js.map