omegga 1.5.4 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/bin/omegga +1 -1
  3. package/dist/cli/terminal.js +46 -0
  4. package/dist/cli/terminal.js.map +1 -1
  5. package/dist/webserver/backend/database.js +309 -11
  6. package/dist/webserver/backend/database.js.map +1 -1
  7. package/dist/webserver/backend/events.js +1 -1
  8. package/dist/webserver/backend/events.js.map +1 -1
  9. package/dist/webserver/backend/index.js +120 -2
  10. package/dist/webserver/backend/index.js.map +1 -1
  11. package/dist/webserver/backend/permissions.js +133 -0
  12. package/dist/webserver/backend/permissions.js.map +1 -0
  13. package/dist/webserver/backend/roleHierarchy.js +108 -0
  14. package/dist/webserver/backend/roleHierarchy.js.map +1 -0
  15. package/dist/webserver/backend/router/chat.js +9 -7
  16. package/dist/webserver/backend/router/chat.js.map +1 -1
  17. package/dist/webserver/backend/router/index.js +3 -1
  18. package/dist/webserver/backend/router/index.js.map +1 -1
  19. package/dist/webserver/backend/router/mfa.js +162 -0
  20. package/dist/webserver/backend/router/mfa.js.map +1 -0
  21. package/dist/webserver/backend/router/player.js +16 -6
  22. package/dist/webserver/backend/router/player.js.map +1 -1
  23. package/dist/webserver/backend/router/plugin.js +14 -10
  24. package/dist/webserver/backend/router/plugin.js.map +1 -1
  25. package/dist/webserver/backend/router/role.js +201 -6
  26. package/dist/webserver/backend/router/role.js.map +1 -1
  27. package/dist/webserver/backend/router/server.js +85 -71
  28. package/dist/webserver/backend/router/server.js.map +1 -1
  29. package/dist/webserver/backend/router/session.js +11 -4
  30. package/dist/webserver/backend/router/session.js.map +1 -1
  31. package/dist/webserver/backend/router/user.js +230 -15
  32. package/dist/webserver/backend/router/user.js.map +1 -1
  33. package/dist/webserver/backend/router/world.js +10 -9
  34. package/dist/webserver/backend/router/world.js.map +1 -1
  35. package/dist/webserver/backend/scopes.js +332 -0
  36. package/dist/webserver/backend/scopes.js.map +1 -0
  37. package/dist/webserver/backend/totp.js +32 -0
  38. package/dist/webserver/backend/totp.js.map +1 -0
  39. package/dist/webserver/backend/trpc.js +47 -4
  40. package/dist/webserver/backend/trpc.js.map +1 -1
  41. package/doc/permissions.md +264 -0
  42. package/package.json +7 -1
  43. package/public/app.html +1012 -527
  44. package/public/assets/IconShieldCheck-Bjlm8mX2.js +2 -0
  45. package/public/assets/IconShieldCheck-Bjlm8mX2.js.map +1 -0
  46. package/public/assets/IconWorld-Bx3cKMms.js +2 -0
  47. package/public/assets/IconWorld-Bx3cKMms.js.map +1 -0
  48. package/public/assets/IconWorldPlus-BHSmzWsZ.js +2 -0
  49. package/public/assets/IconWorldPlus-BHSmzWsZ.js.map +1 -0
  50. package/public/assets/app-J90GEVdo.js +7 -0
  51. package/public/assets/app-J90GEVdo.js.map +1 -0
  52. package/public/assets/auth-nn5rDnJ9.js +2 -0
  53. package/public/assets/auth-nn5rDnJ9.js.map +1 -0
  54. package/public/assets/index-B2DCW1zi.js +2 -0
  55. package/public/assets/index-B2DCW1zi.js.map +1 -0
  56. package/public/assets/index-BDkarAkX.js +3 -0
  57. package/public/assets/index-BDkarAkX.js.map +1 -0
  58. package/public/assets/{index-2cgXLhrt.js → index-DEL-PD_8.js} +2 -2
  59. package/public/assets/{index-2cgXLhrt.js.map → index-DEL-PD_8.js.map} +1 -1
  60. package/public/assets/index-wYDkzaAO.js +12 -0
  61. package/public/assets/index-wYDkzaAO.js.map +1 -0
  62. package/public/assets/shared/@components-BWkdUJhC.js +2 -0
  63. package/public/assets/shared/@components-BWkdUJhC.js.map +1 -0
  64. package/public/assets/shared/@hooks-BuRTblPY.js +2 -0
  65. package/public/assets/shared/{@hooks-Ch_hJYh0.js.map → @hooks-BuRTblPY.js.map} +1 -1
  66. package/public/assets/shared/@tabler_icons-react-CdTxTJVq.js +3 -0
  67. package/public/assets/shared/{@tabler_icons-react-iQk11eD_.js.map → @tabler_icons-react-CdTxTJVq.js.map} +1 -1
  68. package/public/assets/shared/{wouter-CUC1caxu.js → wouter-CNISJ5i6.js} +2 -2
  69. package/public/assets/shared/wouter-CNISJ5i6.js.map +1 -0
  70. package/public/assets/useHasScope-DZPCXNQE.js +2 -0
  71. package/public/assets/useHasScope-DZPCXNQE.js.map +1 -0
  72. package/public/auth.html +153 -40
  73. package/public/assets/IconArrowRight-BuVxhqKX.js +0 -2
  74. package/public/assets/IconArrowRight-BuVxhqKX.js.map +0 -1
  75. package/public/assets/IconWorld-B6GK5qpx.js +0 -2
  76. package/public/assets/IconWorld-B6GK5qpx.js.map +0 -1
  77. package/public/assets/IconWorldPlus-Dkos72Ws.js +0 -2
  78. package/public/assets/IconWorldPlus-Dkos72Ws.js.map +0 -1
  79. package/public/assets/app-CLZ-5FIi.js +0 -6
  80. package/public/assets/app-CLZ-5FIi.js.map +0 -1
  81. package/public/assets/auth-6N37o421.js +0 -2
  82. package/public/assets/auth-6N37o421.js.map +0 -1
  83. package/public/assets/index-DLZ8-XzO.js +0 -4
  84. package/public/assets/index-DLZ8-XzO.js.map +0 -1
  85. package/public/assets/index-DY750sgZ.js +0 -2
  86. package/public/assets/index-DY750sgZ.js.map +0 -1
  87. package/public/assets/shared/@components-DPHARroA.js +0 -2
  88. package/public/assets/shared/@components-DPHARroA.js.map +0 -1
  89. package/public/assets/shared/@hooks-Ch_hJYh0.js +0 -2
  90. package/public/assets/shared/@tabler_icons-react-iQk11eD_.js +0 -3
  91. package/public/assets/shared/wouter-CUC1caxu.js.map +0 -1
@@ -11,7 +11,10 @@ const http = require("http");
11
11
  const https = require("https");
12
12
  const NedbStore = require("nedb-promises-session-store");
13
13
  const path = require("path");
14
+ const bcrypt = require("bcryptjs");
15
+ const server$1 = require("@simplewebauthn/server");
14
16
  const database = require("./database.js");
17
+ const totp = require("./totp.js");
15
18
  const metrics = require("./metrics.js");
16
19
  const index = require("./router/index.js");
17
20
  const server = require("./router/server.js");
@@ -24,6 +27,7 @@ const error = (...args) => logger.default.error(...args);
24
27
  class Webserver {
25
28
  // create a webserver
26
29
  constructor(options, omegga) {
30
+ this.lastReportedStatus = null;
27
31
  this.port = Number(
28
32
  options.port || process.env.OMEGGA_PORT || softconfig.default.DEFAULT_PORT
29
33
  );
@@ -129,14 +133,125 @@ class Webserver {
129
133
  }
130
134
  if (user) {
131
135
  req.session.userId = user._id;
132
- req.session.save();
133
- res.status(200).json({});
136
+ if (user.totpEnabled) {
137
+ req.session.mfaPending = true;
138
+ req.session.save();
139
+ res.status(200).json({ mfaRequired: true });
140
+ } else {
141
+ req.session.save();
142
+ res.status(200).json({});
143
+ }
134
144
  } else {
135
145
  res.status(404).json({ message: "no user found" });
136
146
  }
137
147
  });
148
+ const mfaAttempts = /* @__PURE__ */ new Map();
149
+ openApi.post("/auth/mfa/totp", async (req, res) => {
150
+ if (!req.session.mfaPending || !req.session.userId) {
151
+ return res.status(400).json({ message: "no pending MFA challenge" });
152
+ }
153
+ const sid = req.sessionID;
154
+ const attempt = mfaAttempts.get(sid) ?? { count: 0, lockedUntil: 0 };
155
+ if (Date.now() < attempt.lockedUntil) {
156
+ return res.status(429).json({ message: "too many attempts, try again later" });
157
+ }
158
+ const { code } = req.body ?? {};
159
+ if (typeof code !== "string") {
160
+ return res.status(422).json({ message: "code required" });
161
+ }
162
+ const user = await this.database.findUserById(req.session.userId);
163
+ if (!user || !user.totpSecret) {
164
+ return res.status(400).json({ message: "MFA not configured" });
165
+ }
166
+ let verified = false;
167
+ if (totp.verifyToken(code, user.totpSecret)) {
168
+ verified = true;
169
+ }
170
+ if (!verified && user.recoveryCodes?.length) {
171
+ for (const hashedCode of user.recoveryCodes) {
172
+ if (await bcrypt.compare(code, hashedCode)) {
173
+ await this.database.removeRecoveryCode(user.username, hashedCode);
174
+ verified = true;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ if (verified) {
180
+ mfaAttempts.delete(sid);
181
+ delete req.session.mfaPending;
182
+ req.session.save();
183
+ return res.status(200).json({});
184
+ }
185
+ attempt.count++;
186
+ if (attempt.count >= 5) {
187
+ attempt.lockedUntil = Date.now() + 6e4;
188
+ attempt.count = 0;
189
+ }
190
+ mfaAttempts.set(sid, attempt);
191
+ res.status(401).json({ message: "invalid code" });
192
+ });
193
+ const getRpID = (req) => {
194
+ const origin = req.headers.origin;
195
+ if (origin) {
196
+ try {
197
+ return new URL(origin).hostname;
198
+ } catch {
199
+ }
200
+ }
201
+ return req.hostname;
202
+ };
203
+ openApi.get("/auth/webauthn/options", async (req, res) => {
204
+ const rpID = getRpID(req);
205
+ const options = await server$1.generateAuthenticationOptions({ rpID });
206
+ req.session.mfaChallenge = options.challenge;
207
+ req.session.save();
208
+ res.json(options);
209
+ });
210
+ openApi.post("/auth/webauthn/verify", async (req, res) => {
211
+ const challenge = req.session.mfaChallenge;
212
+ if (!challenge) {
213
+ return res.status(400).json({ message: "no pending challenge" });
214
+ }
215
+ delete req.session.mfaChallenge;
216
+ req.session.save();
217
+ try {
218
+ const { credential } = req.body;
219
+ const user = await this.database.findUserByPasskeyId(credential?.id);
220
+ if (!user || user.isBanned) {
221
+ return res.status(401).json({ message: "unknown credential" });
222
+ }
223
+ const passkey = user.passkeys.find((p) => p.id === credential.id);
224
+ const verification = await server$1.verifyAuthenticationResponse({
225
+ response: credential,
226
+ expectedChallenge: challenge,
227
+ expectedOrigin: req.headers.origin ?? `${req.protocol}://${req.get("host")}`,
228
+ expectedRPID: getRpID(req),
229
+ credential: {
230
+ id: passkey.id,
231
+ publicKey: Buffer.from(passkey.publicKey, "base64url"),
232
+ counter: passkey.counter,
233
+ transports: passkey.transports
234
+ }
235
+ });
236
+ if (!verification.verified) {
237
+ return res.status(401).json({ message: "verification failed" });
238
+ }
239
+ await this.database.updatePasskeyCounter(
240
+ user.username,
241
+ passkey.id,
242
+ verification.authenticationInfo.newCounter
243
+ );
244
+ req.session.userId = user._id;
245
+ delete req.session.mfaPending;
246
+ req.session.save();
247
+ res.status(200).json({});
248
+ } catch (e) {
249
+ res.status(400).json({ message: "verification error" });
250
+ }
251
+ });
138
252
  api.all("*", (req, _res, next) => {
139
253
  (async () => {
254
+ if (req.session.mfaPending) return next(new Error("unauthorized"));
140
255
  const user = await this.database.findUserById(req.session.userId);
141
256
  if (!user || user.isBanned) return next(new Error("unauthorized"));
142
257
  next();
@@ -158,6 +273,9 @@ class Webserver {
158
273
  );
159
274
  metrics.default(this);
160
275
  this.app.use(async (req, res) => {
276
+ if (req.session.mfaPending) {
277
+ return res.sendFile(path.join(PUBLIC_PATH, "auth.html"));
278
+ }
161
279
  const user = await this.database.findUserById(req.session.userId);
162
280
  const isAuth = user && !user.isBanned;
163
281
  if (isAuth) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../src/webserver/backend/index.ts"],"sourcesContent":["import Logger from '@/logger';\nimport soft from '@/softconfig';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { IServerStatus } from '@omegga/types';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport bodyParser from 'body-parser';\nimport express from 'express';\nimport expressSession from 'express-session';\nimport hasbin from 'hasbin';\nimport http from 'http';\nimport https from 'https';\nimport NedbStore from 'nedb-promises-session-store';\nimport path from 'path';\nimport Database from './database';\nimport setupMetrics from './metrics';\nimport { appRouter } from './router';\nimport { setWebserver } from './router/server';\nimport { createContext, setContextDeps } from './trpc';\nimport * as util from './util';\n\n// path to vite-built data\nconst PUBLIC_PATH = path.join(__dirname, '../../../public');\nconst ASSETS_PATH = path.join(__dirname, '../../../public/assets');\n\nconst log = (...args: any[]) => Logger.log(...args);\nconst error = (...args: any[]) => Logger.error(...args);\n\n// the webserver serves an authenticated web UI\nexport default class Webserver {\n database: Database;\n port: number;\n omegga: Omegga;\n\n app: express.Express;\n server: http.Server | https.Server;\n\n options: IServerConfig;\n https: boolean;\n started: boolean;\n created: Promise<boolean>;\n\n serverStatusInterval: ReturnType<typeof setInterval>;\n lastReportedStatus: IServerStatus;\n\n dataPath: string;\n\n // create a webserver\n constructor(options: IServerConfig, omegga: Omegga) {\n this.port = Number(\n options.port || process.env.OMEGGA_PORT || soft.DEFAULT_PORT,\n );\n this.options = options;\n this.omegga = omegga;\n this.dataPath = path.join(omegga.path, soft.DATA_PATH);\n\n // the database provides omegga with metrics, chat logs, and more\n // to help administrators keep track of their users and server\n this.database = new Database(options, omegga);\n\n // https status of the server\n this.https = false;\n // started status of the server\n this.started = false;\n this.created = this.createServer();\n }\n\n // create the webserver\n async createServer() {\n const hasOpenSSL = hasbin.sync('openssl');\n\n // let the database do migrations\n await this.database.doMigrations();\n\n // create express app\n this.app = express();\n\n // create a server with https if applicable\n const pickProtocol = async () => {\n if (this.options.https) {\n if (hasOpenSSL) {\n const certFile = await util.getSSLKeys(this.dataPath);\n if (certFile) {\n // handy notification of generated ssl certs\n if (certFile.new) log('>>'.green, 'Generated new SSL certificate');\n\n this.https = true;\n return https.createServer(\n {\n key: certFile.keys.serviceKey,\n cert: certFile.keys.certificate,\n },\n this.app,\n );\n } else {\n error(\n '!>'.red,\n 'Error generating SSL certificate - falling back to http',\n );\n }\n }\n\n // warn the user\n log(\n ':>'.yellow,\n 'Running web server with http - install',\n 'openssl'.yellow.underline,\n 'for https (more secure)',\n );\n }\n\n // fallback to http\n return new http.Server(this.app);\n };\n\n // create http(s) server based on openssl availability\n this.server = await pickProtocol();\n\n this.app.set('trust proxy', 1);\n const session = expressSession({\n secret: util.getSessionSecret(this.dataPath),\n resave: false,\n saveUninitialized: false,\n cookie: {\n path: '/',\n secure: this.https,\n maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year\n },\n // use a nedb database for session store\n // For some reason vite is not importing the default export properly without this...\n store: (NedbStore['default'] as typeof NedbStore)({\n filename: path.join(this.dataPath, soft.SESSION_STORE),\n connect: expressSession,\n }),\n });\n this.app.use(session);\n\n // setup routes and webserver\n await this.initWebUI();\n return true;\n }\n\n // setup the web ui routes\n async initWebUI() {\n await this.database.getInstanceId();\n\n // Initialize tRPC context deps\n setContextDeps({\n database: this.database,\n omegga: this.omegga,\n });\n setWebserver(this);\n\n // provide assets in the /public path\n this.app.use('/assets', express.static(ASSETS_PATH));\n this.app.use(bodyParser.json());\n\n // --- Auth routes (outside tRPC) ---\n const openApi = express.Router();\n const api = express.Router();\n\n // check if this is the first user in the database\n openApi.get('/first', async (req, res) =>\n res.json(await this.database.isFirstUser()),\n );\n\n // login / create admin user route\n openApi.post('/auth', async (req, res) => {\n if (\n typeof req.body !== 'object' ||\n typeof req.body.username !== 'string' ||\n typeof req.body.password !== 'string'\n ) {\n return res.status(422).json({ message: 'invalid body' });\n }\n const { username, password } = req.body;\n\n if (!username.match(/^\\w{0,32}$/)) {\n return res.status(422).json({ message: 'invalid body' });\n }\n\n const isFirst = await this.database.isFirstUser();\n let user;\n if (isFirst) {\n user = await this.database.createAdminUser(\n username,\n username === '' ? '' : password,\n );\n } else {\n user = await this.database.authUser(username, password);\n }\n\n if (user) {\n req.session.userId = user._id;\n req.session.save();\n res.status(200).json({});\n } else {\n res.status(404).json({ message: 'no user found' });\n }\n });\n\n // authentication middleware for protected api routes\n api.all('*', (req, _res, next) => {\n (async () => {\n const user = await this.database.findUserById(req.session.userId);\n if (!user || user.isBanned) return next(new Error('unauthorized'));\n next();\n })();\n });\n\n // kill a session\n api.get('/logout', (req, res) => {\n req.session.destroy(e => {\n res.status(e ? 500 : 200).json({});\n });\n });\n\n this.app.use('/api/v1', openApi);\n this.app.use('/api/v1', api);\n\n // --- tRPC ---\n this.app.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext,\n }),\n );\n\n // setup metrics and tracking\n setupMetrics(this);\n\n // every request goes through the index file (frontend handles 404s)\n this.app.use(async (req, res) => {\n const user = await this.database.findUserById(req.session.userId);\n const isAuth = user && !user.isBanned;\n if (isAuth) {\n res.sendFile(path.join(PUBLIC_PATH, 'app.html'));\n } else {\n res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n });\n }\n\n // start the webserver\n async start() {\n if (this.started) return;\n await this.created;\n return await new Promise<void>(resolve => {\n this.server.listen(this.port, () => {\n log(\n `${'>>'.green} Web UI available at`,\n `http${this.https ? 's' : ''}://127.0.0.1:${this.port}`.green,\n );\n this.started = true;\n this.database.addChatLog('server', {}, 'Server started');\n resolve();\n });\n });\n }\n\n // stop the webserver\n stop() {\n this.database.addChatLog('server', {}, 'Server stopped');\n this.server.close();\n this.started = false;\n clearInterval(this.serverStatusInterval);\n }\n}\n"],"names":["Logger","soft","Database","util.getSSLKeys","util.getSessionSecret","setContextDeps","setWebserver","createExpressMiddleware","appRouter","createContext","setupMetrics"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../src/webserver/backend/index.ts"],"sourcesContent":["import Logger from '@/logger';\nimport soft from '@/softconfig';\nimport { IServerConfig } from '@config/types';\nimport type Omegga from '@omegga/server';\nimport { IServerStatus } from '@omegga/types';\nimport { createExpressMiddleware } from '@trpc/server/adapters/express';\nimport bodyParser from 'body-parser';\nimport express from 'express';\nimport expressSession from 'express-session';\nimport hasbin from 'hasbin';\nimport http from 'http';\nimport https from 'https';\nimport NedbStore from 'nedb-promises-session-store';\nimport path from 'path';\nimport bcrypt from 'bcryptjs';\nimport {\n generateAuthenticationOptions,\n verifyAuthenticationResponse,\n} from '@simplewebauthn/server';\nimport Database from './database';\nimport { verifyToken as verifyTOTP } from './totp';\nimport setupMetrics from './metrics';\nimport { appRouter } from './router';\nimport { setWebserver } from './router/server';\nimport { createContext, setContextDeps } from './trpc';\nimport * as util from './util';\n\n// path to vite-built data\nconst PUBLIC_PATH = path.join(__dirname, '../../../public');\nconst ASSETS_PATH = path.join(__dirname, '../../../public/assets');\n\nconst log = (...args: any[]) => Logger.log(...args);\nconst error = (...args: any[]) => Logger.error(...args);\n\n// the webserver serves an authenticated web UI\nexport default class Webserver {\n database: Database;\n port: number;\n omegga: Omegga;\n\n app: express.Express;\n server: http.Server | https.Server;\n\n options: IServerConfig;\n https: boolean;\n started: boolean;\n created: Promise<boolean>;\n\n serverStatusInterval: ReturnType<typeof setInterval>;\n lastReportedStatus: IServerStatus | null = null;\n\n dataPath: string;\n\n // create a webserver\n constructor(options: IServerConfig, omegga: Omegga) {\n this.port = Number(\n options.port || process.env.OMEGGA_PORT || soft.DEFAULT_PORT,\n );\n this.options = options;\n this.omegga = omegga;\n this.dataPath = path.join(omegga.path, soft.DATA_PATH);\n\n // the database provides omegga with metrics, chat logs, and more\n // to help administrators keep track of their users and server\n this.database = new Database(options, omegga);\n\n // https status of the server\n this.https = false;\n // started status of the server\n this.started = false;\n this.created = this.createServer();\n }\n\n // create the webserver\n async createServer() {\n const hasOpenSSL = hasbin.sync('openssl');\n\n // let the database do migrations\n await this.database.doMigrations();\n\n // create express app\n this.app = express();\n\n // create a server with https if applicable\n const pickProtocol = async () => {\n if (this.options.https) {\n if (hasOpenSSL) {\n const certFile = await util.getSSLKeys(this.dataPath);\n if (certFile) {\n // handy notification of generated ssl certs\n if (certFile.new) log('>>'.green, 'Generated new SSL certificate');\n\n this.https = true;\n return https.createServer(\n {\n key: certFile.keys.serviceKey,\n cert: certFile.keys.certificate,\n },\n this.app,\n );\n } else {\n error(\n '!>'.red,\n 'Error generating SSL certificate - falling back to http',\n );\n }\n }\n\n // warn the user\n log(\n ':>'.yellow,\n 'Running web server with http - install',\n 'openssl'.yellow.underline,\n 'for https (more secure)',\n );\n }\n\n // fallback to http\n return new http.Server(this.app);\n };\n\n // create http(s) server based on openssl availability\n this.server = await pickProtocol();\n\n this.app.set('trust proxy', 1);\n const session = expressSession({\n secret: util.getSessionSecret(this.dataPath),\n resave: false,\n saveUninitialized: false,\n cookie: {\n path: '/',\n secure: this.https,\n maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year\n },\n // use a nedb database for session store\n // For some reason vite is not importing the default export properly without this...\n store: (NedbStore['default'] as typeof NedbStore)({\n filename: path.join(this.dataPath, soft.SESSION_STORE),\n connect: expressSession,\n }),\n });\n this.app.use(session);\n\n // setup routes and webserver\n await this.initWebUI();\n return true;\n }\n\n // setup the web ui routes\n async initWebUI() {\n await this.database.getInstanceId();\n\n // Initialize tRPC context deps\n setContextDeps({\n database: this.database,\n omegga: this.omegga,\n });\n setWebserver(this);\n\n // provide assets in the /public path\n this.app.use('/assets', express.static(ASSETS_PATH));\n this.app.use(bodyParser.json());\n\n // --- Auth routes (outside tRPC) ---\n const openApi = express.Router();\n const api = express.Router();\n\n // check if this is the first user in the database\n openApi.get('/first', async (req, res) =>\n res.json(await this.database.isFirstUser()),\n );\n\n // login / create admin user route\n openApi.post('/auth', async (req, res) => {\n if (\n typeof req.body !== 'object' ||\n typeof req.body.username !== 'string' ||\n typeof req.body.password !== 'string'\n ) {\n return res.status(422).json({ message: 'invalid body' });\n }\n const { username, password } = req.body;\n\n if (!username.match(/^\\w{0,32}$/)) {\n return res.status(422).json({ message: 'invalid body' });\n }\n\n const isFirst = await this.database.isFirstUser();\n let user;\n if (isFirst) {\n user = await this.database.createAdminUser(\n username,\n username === '' ? '' : password,\n );\n } else {\n user = await this.database.authUser(username, password);\n }\n\n if (user) {\n req.session.userId = user._id;\n if (user.totpEnabled) {\n req.session.mfaPending = true;\n req.session.save();\n res.status(200).json({ mfaRequired: true });\n } else {\n req.session.save();\n res.status(200).json({});\n }\n } else {\n res.status(404).json({ message: 'no user found' });\n }\n });\n\n // rate limiting for MFA verification (per session)\n const mfaAttempts = new Map<\n string,\n { count: number; lockedUntil: number }\n >();\n\n // TOTP verification during MFA challenge\n openApi.post('/auth/mfa/totp', async (req, res) => {\n if (!req.session.mfaPending || !req.session.userId) {\n return res.status(400).json({ message: 'no pending MFA challenge' });\n }\n\n // rate limit: 5 attempts, then 60s lockout\n const sid = req.sessionID;\n const attempt = mfaAttempts.get(sid) ?? { count: 0, lockedUntil: 0 };\n if (Date.now() < attempt.lockedUntil) {\n return res\n .status(429)\n .json({ message: 'too many attempts, try again later' });\n }\n\n const { code } = req.body ?? {};\n if (typeof code !== 'string') {\n return res.status(422).json({ message: 'code required' });\n }\n const user = await this.database.findUserById(req.session.userId);\n if (!user || !user.totpSecret) {\n return res.status(400).json({ message: 'MFA not configured' });\n }\n\n let verified = false;\n\n if (verifyTOTP(code, user.totpSecret)) {\n verified = true;\n }\n\n // try recovery codes\n if (!verified && user.recoveryCodes?.length) {\n for (const hashedCode of user.recoveryCodes) {\n if (await bcrypt.compare(code, hashedCode)) {\n await this.database.removeRecoveryCode(user.username, hashedCode);\n verified = true;\n break;\n }\n }\n }\n\n if (verified) {\n mfaAttempts.delete(sid);\n delete req.session.mfaPending;\n req.session.save();\n return res.status(200).json({});\n }\n\n attempt.count++;\n if (attempt.count >= 5) {\n attempt.lockedUntil = Date.now() + 60_000;\n attempt.count = 0;\n }\n mfaAttempts.set(sid, attempt);\n res.status(401).json({ message: 'invalid code' });\n });\n\n const getRpID = (req: express.Request) => {\n const origin = req.headers.origin;\n if (origin) {\n try {\n return new URL(origin as string).hostname;\n } catch {}\n }\n return req.hostname;\n };\n\n // WebAuthn authentication options (passwordless login)\n openApi.get('/auth/webauthn/options', async (req, res) => {\n const rpID = getRpID(req);\n const options = await generateAuthenticationOptions({ rpID });\n req.session.mfaChallenge = options.challenge;\n req.session.save();\n res.json(options);\n });\n\n // WebAuthn authentication verification\n openApi.post('/auth/webauthn/verify', async (req, res) => {\n const challenge = req.session.mfaChallenge;\n if (!challenge) {\n return res.status(400).json({ message: 'no pending challenge' });\n }\n // clear challenge immediately to prevent replay\n delete req.session.mfaChallenge;\n req.session.save();\n try {\n const { credential } = req.body;\n const user = await this.database.findUserByPasskeyId(credential?.id);\n if (!user || user.isBanned) {\n return res.status(401).json({ message: 'unknown credential' });\n }\n const passkey = user.passkeys!.find(p => p.id === credential.id)!;\n const verification = await verifyAuthenticationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin:\n req.headers.origin ?? `${req.protocol}://${req.get('host')}`,\n expectedRPID: getRpID(req),\n credential: {\n id: passkey.id,\n publicKey: Buffer.from(passkey.publicKey, 'base64url'),\n counter: passkey.counter,\n transports: passkey.transports as any,\n },\n });\n if (!verification.verified) {\n return res.status(401).json({ message: 'verification failed' });\n }\n await this.database.updatePasskeyCounter(\n user.username,\n passkey.id,\n verification.authenticationInfo.newCounter,\n );\n req.session.userId = (user as any)._id;\n delete req.session.mfaPending;\n req.session.save();\n res.status(200).json({});\n } catch (e) {\n res.status(400).json({ message: 'verification error' });\n }\n });\n\n // authentication middleware for protected api routes\n api.all('*', (req, _res, next) => {\n (async () => {\n if (req.session.mfaPending) return next(new Error('unauthorized'));\n const user = await this.database.findUserById(req.session.userId);\n if (!user || user.isBanned) return next(new Error('unauthorized'));\n next();\n })();\n });\n\n // kill a session\n api.get('/logout', (req, res) => {\n req.session.destroy(e => {\n res.status(e ? 500 : 200).json({});\n });\n });\n\n this.app.use('/api/v1', openApi);\n this.app.use('/api/v1', api);\n\n // --- tRPC ---\n this.app.use(\n '/trpc',\n createExpressMiddleware({\n router: appRouter,\n createContext,\n }),\n );\n\n // setup metrics and tracking\n setupMetrics(this);\n\n // every request goes through the index file (frontend handles 404s)\n this.app.use(async (req, res) => {\n if (req.session.mfaPending) {\n return res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n const user = await this.database.findUserById(req.session.userId);\n const isAuth = user && !user.isBanned;\n if (isAuth) {\n res.sendFile(path.join(PUBLIC_PATH, 'app.html'));\n } else {\n res.sendFile(path.join(PUBLIC_PATH, 'auth.html'));\n }\n });\n }\n\n // start the webserver\n async start() {\n if (this.started) return;\n await this.created;\n return await new Promise<void>(resolve => {\n this.server.listen(this.port, () => {\n log(\n `${'>>'.green} Web UI available at`,\n `http${this.https ? 's' : ''}://127.0.0.1:${this.port}`.green,\n );\n this.started = true;\n this.database.addChatLog('server', {}, 'Server started');\n resolve();\n });\n });\n }\n\n // stop the webserver\n stop() {\n this.database.addChatLog('server', {}, 'Server stopped');\n this.server.close();\n this.started = false;\n clearInterval(this.serverStatusInterval);\n }\n}\n"],"names":["Logger","soft","Database","util.getSSLKeys","util.getSessionSecret","setContextDeps","setWebserver","verifyTOTP","generateAuthenticationOptions","verifyAuthenticationResponse","createExpressMiddleware","appRouter","createContext","setupMetrics"],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const scopes = require("./scopes.js");
4
+ var RootLevel = /* @__PURE__ */ ((RootLevel2) => {
5
+ RootLevel2["All"] = "all";
6
+ RootLevel2["Read"] = "read";
7
+ RootLevel2["Off"] = "off";
8
+ return RootLevel2;
9
+ })(RootLevel || {});
10
+ const EMPTY_PERMISSIONS = {
11
+ root: "off",
12
+ domains: {},
13
+ scopes: {}
14
+ };
15
+ const DOT_ENCODE = /\./g;
16
+ const DOT_DECODE = /:/g;
17
+ function encodeKeys(obj) {
18
+ const out = {};
19
+ for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_ENCODE, ":")] = v;
20
+ return out;
21
+ }
22
+ function decodeKeys(obj) {
23
+ const out = {};
24
+ for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_DECODE, ".")] = v;
25
+ return out;
26
+ }
27
+ function encodePermissions(p) {
28
+ return { root: p.root, domains: p.domains, scopes: encodeKeys(p.scopes) };
29
+ }
30
+ function decodePermissions(p) {
31
+ if (!p) return EMPTY_PERMISSIONS;
32
+ return {
33
+ root: p.root ?? "off",
34
+ domains: p.domains ?? {},
35
+ scopes: decodeKeys(p.scopes ?? {})
36
+ };
37
+ }
38
+ const ALL_SCOPES = Object.keys(scopes.SCOPES);
39
+ const ROOT_RANK = {
40
+ [
41
+ "off"
42
+ /* Off */
43
+ ]: 0,
44
+ [
45
+ "read"
46
+ /* Read */
47
+ ]: 1,
48
+ [
49
+ "all"
50
+ /* All */
51
+ ]: 2
52
+ };
53
+ const DOMAIN_RANK = {
54
+ [
55
+ "none"
56
+ /* None */
57
+ ]: 0,
58
+ [
59
+ "read"
60
+ /* Read */
61
+ ]: 1,
62
+ [
63
+ "all"
64
+ /* All */
65
+ ]: 2
66
+ };
67
+ const ROOT_BY_RANK = [
68
+ "off",
69
+ "read",
70
+ "all"
71
+ /* All */
72
+ ];
73
+ function mergePermissionSets(...sets) {
74
+ let rootRank = 0;
75
+ const domains = {};
76
+ const scopes2 = {};
77
+ for (const s of sets) {
78
+ rootRank = Math.max(rootRank, ROOT_RANK[s.root] ?? 0);
79
+ for (const [d, level] of Object.entries(s.domains)) {
80
+ const cur = domains[d];
81
+ const curRank = cur ? DOMAIN_RANK[cur] : 0;
82
+ const newRank = DOMAIN_RANK[level] ?? 0;
83
+ if (newRank > curRank) domains[d] = level;
84
+ }
85
+ for (const [sc, val] of Object.entries(s.scopes)) {
86
+ if (val) scopes2[sc] = true;
87
+ }
88
+ }
89
+ return {
90
+ root: ROOT_BY_RANK[rootRank],
91
+ domains,
92
+ scopes: scopes2
93
+ };
94
+ }
95
+ function resolveScope(userPerms, rolePermissions, scopeName) {
96
+ if (scopeName === scopes.ScopeName.SessionInfo) return true;
97
+ const scopeDef = scopes.SCOPES[scopeName];
98
+ if (userPerms.root === "all") return true;
99
+ if (userPerms.root === "read" && scopeDef.readOnly) return true;
100
+ const domain = userPerms.domains[scopeDef.domain];
101
+ if (domain === "all") return true;
102
+ if (domain === "read" && scopeDef.readOnly) return true;
103
+ if (userPerms.scopes[scopeName] === true) return true;
104
+ if (rolePermissions.length > 0) {
105
+ const merged = mergePermissionSets(...rolePermissions);
106
+ return resolveScope(merged, [], scopeName);
107
+ }
108
+ return false;
109
+ }
110
+ function resolveAllScopes(userPerms, rolePermissions) {
111
+ const result = {};
112
+ for (const scope of ALL_SCOPES) {
113
+ result[scope] = resolveScope(userPerms, rolePermissions, scope);
114
+ }
115
+ return result;
116
+ }
117
+ function userHasScope(user, scope, rolePermissions) {
118
+ if (user.isOwner) return true;
119
+ return resolveScope(
120
+ user.permissions ?? EMPTY_PERMISSIONS,
121
+ rolePermissions,
122
+ scope
123
+ );
124
+ }
125
+ exports.EMPTY_PERMISSIONS = EMPTY_PERMISSIONS;
126
+ exports.RootLevel = RootLevel;
127
+ exports.decodePermissions = decodePermissions;
128
+ exports.encodePermissions = encodePermissions;
129
+ exports.mergePermissionSets = mergePermissionSets;
130
+ exports.resolveAllScopes = resolveAllScopes;
131
+ exports.resolveScope = resolveScope;
132
+ exports.userHasScope = userHasScope;
133
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sources":["../../../src/webserver/backend/permissions.ts"],"sourcesContent":["import { SCOPES, ScopeName, type Domain, type Scope } from './scopes';\n\nexport enum RootLevel {\n All = 'all',\n Read = 'read',\n Off = 'off',\n}\n\nexport enum DomainLevel {\n All = 'all',\n Read = 'read',\n None = 'none',\n}\n\nexport interface PermissionSet {\n root: RootLevel;\n domains: Partial<Record<Domain, DomainLevel>>;\n scopes: Partial<Record<Scope, boolean>>;\n}\n\nexport const EMPTY_PERMISSIONS: PermissionSet = {\n root: RootLevel.Off,\n domains: {},\n scopes: {},\n};\n\n// NeDB rejects field names containing dots, so we encode/decode\n// scope keys (e.g. \"server.status\") for storage\nconst DOT_ENCODE = /\\./g;\nconst DOT_DECODE = /:/g;\n\nfunction encodeKeys<V>(obj: Record<string, V>): Record<string, V> {\n const out: Record<string, V> = {};\n for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_ENCODE, ':')] = v;\n return out;\n}\n\nfunction decodeKeys<V>(obj: Record<string, V>): Record<string, V> {\n const out: Record<string, V> = {};\n for (const [k, v] of Object.entries(obj)) out[k.replace(DOT_DECODE, '.')] = v;\n return out;\n}\n\nexport interface StoredPermissionSet {\n root: RootLevel;\n domains: Partial<Record<string, DomainLevel>>;\n scopes: Record<string, boolean>;\n}\n\nexport function encodePermissions(p: PermissionSet): StoredPermissionSet {\n return { root: p.root, domains: p.domains, scopes: encodeKeys(p.scopes) };\n}\n\nexport function decodePermissions(\n p: StoredPermissionSet | null | undefined,\n): PermissionSet {\n if (!p) return EMPTY_PERMISSIONS;\n return {\n root: p.root ?? RootLevel.Off,\n domains: p.domains ?? {},\n scopes: decodeKeys(p.scopes ?? {}),\n };\n}\n\nconst ALL_SCOPES = Object.keys(SCOPES) as Scope[];\n\nconst ROOT_RANK: Record<RootLevel, number> = {\n [RootLevel.Off]: 0,\n [RootLevel.Read]: 1,\n [RootLevel.All]: 2,\n};\n\nconst DOMAIN_RANK: Record<DomainLevel, number> = {\n [DomainLevel.None]: 0,\n [DomainLevel.Read]: 1,\n [DomainLevel.All]: 2,\n};\n\nconst ROOT_BY_RANK: RootLevel[] = [\n RootLevel.Off,\n RootLevel.Read,\n RootLevel.All,\n];\nconst DOMAIN_BY_RANK: DomainLevel[] = [\n DomainLevel.None,\n DomainLevel.Read,\n DomainLevel.All,\n];\n\nexport function mergePermissionSets(...sets: PermissionSet[]): PermissionSet {\n let rootRank = 0;\n const domains: Partial<Record<Domain, DomainLevel>> = {};\n const scopes: Partial<Record<Scope, boolean>> = {};\n\n for (const s of sets) {\n rootRank = Math.max(rootRank, ROOT_RANK[s.root] ?? 0);\n for (const [d, level] of Object.entries(s.domains) as [\n Domain,\n DomainLevel,\n ][]) {\n const cur = domains[d];\n const curRank = cur ? DOMAIN_RANK[cur] : 0;\n const newRank = DOMAIN_RANK[level] ?? 0;\n if (newRank > curRank) domains[d] = level;\n }\n for (const [sc, val] of Object.entries(s.scopes) as [Scope, boolean][]) {\n if (val) scopes[sc] = true;\n }\n }\n\n return {\n root: ROOT_BY_RANK[rootRank],\n domains,\n scopes,\n };\n}\n\nexport function resolveScope(\n userPerms: PermissionSet,\n rolePermissions: PermissionSet[],\n scopeName: Scope,\n): boolean {\n if (scopeName === ScopeName.SessionInfo) return true;\n\n const scopeDef = SCOPES[scopeName];\n\n // 1. Root\n if (userPerms.root === RootLevel.All) return true;\n if (userPerms.root === RootLevel.Read && scopeDef.readOnly) return true;\n\n // 2. Domain\n const domain = userPerms.domains[scopeDef.domain as Domain];\n if (domain === DomainLevel.All) return true;\n if (domain === DomainLevel.Read && scopeDef.readOnly) return true;\n\n // 3. Scope (additive only - explicit true grants, false/absent falls through)\n if (userPerms.scopes[scopeName] === true) return true;\n\n // 4. Fall back to merged role permissions\n if (rolePermissions.length > 0) {\n const merged = mergePermissionSets(...rolePermissions);\n return resolveScope(merged, [], scopeName);\n }\n\n return false;\n}\n\nexport function resolveAllScopes(\n userPerms: PermissionSet,\n rolePermissions: PermissionSet[],\n): Record<Scope, boolean> {\n const result = {} as Record<Scope, boolean>;\n for (const scope of ALL_SCOPES) {\n result[scope] = resolveScope(userPerms, rolePermissions, scope);\n }\n return result;\n}\n\nexport function userHasScope(\n user: { isOwner: boolean; permissions?: PermissionSet },\n scope: Scope,\n rolePermissions: PermissionSet[],\n): boolean {\n if (user.isOwner) return true;\n return resolveScope(\n user.permissions ?? EMPTY_PERMISSIONS,\n rolePermissions,\n scope,\n );\n}\n"],"names":["RootLevel","SCOPES","scopes","ScopeName"],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const permissions = require("./permissions.js");
4
+ const trpc = require("./trpc.js");
5
+ function getActorHighestOrder(actor, actorRoles, scope, allRolePermissions) {
6
+ let highest = -1;
7
+ for (const role of actorRoles) {
8
+ const perms = permissions.decodePermissions(role.permissions);
9
+ const resolved = permissions.resolveAllScopes(perms, []);
10
+ if (resolved[scope]) {
11
+ highest = Math.max(highest, role.order);
12
+ }
13
+ }
14
+ if (highest < 0) {
15
+ const userPerms = actor.permissions ?? permissions.EMPTY_PERMISSIONS;
16
+ if (permissions.resolveScope(userPerms, allRolePermissions ?? [], scope))
17
+ return Infinity;
18
+ }
19
+ return highest;
20
+ }
21
+ function canManageRole(actorOrder, targetRole) {
22
+ return actorOrder > targetRole.order;
23
+ }
24
+ function actorHasAllPermissions(actorEffective, granting) {
25
+ const actorScopes = permissions.resolveAllScopes(actorEffective, []);
26
+ const grantingScopes = permissions.resolveAllScopes(granting, []);
27
+ for (const [scope, granted] of Object.entries(grantingScopes)) {
28
+ if (granted && !actorScopes[scope]) return false;
29
+ }
30
+ return true;
31
+ }
32
+ function checkPermissionEscalation(actor, actorRolePermissions, proposed) {
33
+ if (actor.isOwner) return null;
34
+ const effective = getActorEffectivePermissions(actor, actorRolePermissions);
35
+ const myScopes = permissions.resolveAllScopes(effective, []);
36
+ const grantedScopes = permissions.resolveAllScopes(proposed, []);
37
+ for (const [scope, granted] of Object.entries(grantedScopes)) {
38
+ if (granted && !myScopes[scope])
39
+ return `cannot grant permission you do not have: ${scope}`;
40
+ }
41
+ return null;
42
+ }
43
+ function checkPermissionRevocation(current, proposed) {
44
+ const currentScopes = permissions.resolveAllScopes(current, []);
45
+ const proposedScopes = permissions.resolveAllScopes(proposed, []);
46
+ for (const [scope, granted] of Object.entries(currentScopes)) {
47
+ if (granted && !proposedScopes[scope])
48
+ return `cannot revoke permission: ${scope}`;
49
+ }
50
+ return null;
51
+ }
52
+ function getGrantablePermissions(actorRoles, minOrder) {
53
+ const qualifying = minOrder !== void 0 ? actorRoles.filter((r) => r.order > minOrder) : actorRoles;
54
+ if (qualifying.length === 0) return permissions.EMPTY_PERMISSIONS;
55
+ return permissions.mergePermissionSets(
56
+ ...qualifying.map((r) => permissions.decodePermissions(r.permissions))
57
+ );
58
+ }
59
+ function getActorEffectivePermissions(user, rolePermissions) {
60
+ return permissions.mergePermissionSets(
61
+ user.permissions ?? permissions.EMPTY_PERMISSIONS,
62
+ ...rolePermissions
63
+ );
64
+ }
65
+ function validateHierarchy(actor, actorRoles, targetRole, requiredScope, allRolePermissions) {
66
+ if (actor.isOwner) return null;
67
+ const order = getActorHighestOrder(
68
+ actor,
69
+ actorRoles,
70
+ requiredScope,
71
+ allRolePermissions
72
+ );
73
+ if (order < 0) return `missing permission: ${requiredScope}`;
74
+ if (!canManageRole(order, targetRole))
75
+ return `cannot manage role "${targetRole.name}" (order ${targetRole.order}) from order ${order}`;
76
+ return null;
77
+ }
78
+ async function checkUserHierarchy(actor, target) {
79
+ if (actor.isOwner) return null;
80
+ if (target.isOwner) return "cannot modify the owner";
81
+ const { database } = trpc.getContextDeps();
82
+ const targetRoles = await database.getUserAssignedRoles(target);
83
+ if (targetRoles.length === 0) return null;
84
+ const targetMaxOrder = Math.max(...targetRoles.map((r) => r.order));
85
+ const actorRoles = await database.getUserAssignedRoles(actor);
86
+ const actorMaxOrder = Math.max(0, ...actorRoles.map((r) => r.order));
87
+ if (actorMaxOrder <= targetMaxOrder)
88
+ return "cannot modify a user with equal or higher role";
89
+ return null;
90
+ }
91
+ async function checkRoleHierarchy(user, role, scope) {
92
+ if (user.isOwner) return null;
93
+ const { database } = trpc.getContextDeps();
94
+ const actorRoles = await database.getUserAssignedRoles(user);
95
+ const rolePerms = await database.getUserRolePermissions(user);
96
+ return validateHierarchy(user, actorRoles, role, scope, rolePerms);
97
+ }
98
+ exports.actorHasAllPermissions = actorHasAllPermissions;
99
+ exports.canManageRole = canManageRole;
100
+ exports.checkPermissionEscalation = checkPermissionEscalation;
101
+ exports.checkPermissionRevocation = checkPermissionRevocation;
102
+ exports.checkRoleHierarchy = checkRoleHierarchy;
103
+ exports.checkUserHierarchy = checkUserHierarchy;
104
+ exports.getActorEffectivePermissions = getActorEffectivePermissions;
105
+ exports.getActorHighestOrder = getActorHighestOrder;
106
+ exports.getGrantablePermissions = getGrantablePermissions;
107
+ exports.validateHierarchy = validateHierarchy;
108
+ //# sourceMappingURL=roleHierarchy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleHierarchy.js","sources":["../../../src/webserver/backend/roleHierarchy.ts"],"sourcesContent":["import {\n decodePermissions,\n EMPTY_PERMISSIONS,\n mergePermissionSets,\n resolveAllScopes,\n resolveScope,\n type PermissionSet,\n} from './permissions';\nimport { type Scope } from './scopes';\nimport { getContextDeps } from './trpc';\nimport type { IStoreRole, IStoreUser } from './types';\n\nexport function getActorHighestOrder(\n actor: IStoreUser,\n actorRoles: IStoreRole[],\n scope: Scope,\n allRolePermissions?: PermissionSet[],\n): number {\n let highest = -1;\n for (const role of actorRoles) {\n const perms = decodePermissions(role.permissions);\n const resolved = resolveAllScopes(perms, []);\n if (resolved[scope]) {\n highest = Math.max(highest, role.order);\n }\n }\n if (highest < 0) {\n const userPerms = actor.permissions ?? EMPTY_PERMISSIONS;\n if (resolveScope(userPerms, allRolePermissions ?? [], scope))\n return Infinity;\n }\n return highest;\n}\n\nexport function canManageRole(\n actorOrder: number,\n targetRole: IStoreRole,\n): boolean {\n return actorOrder > targetRole.order;\n}\n\nexport function actorHasAllPermissions(\n actorEffective: PermissionSet,\n granting: PermissionSet,\n): boolean {\n const actorScopes = resolveAllScopes(actorEffective, []);\n const grantingScopes = resolveAllScopes(granting, []);\n for (const [scope, granted] of Object.entries(grantingScopes)) {\n if (granted && !actorScopes[scope as Scope]) return false;\n }\n return true;\n}\n\nexport function checkPermissionEscalation(\n actor: IStoreUser,\n actorRolePermissions: PermissionSet[],\n proposed: PermissionSet,\n): string | null {\n if (actor.isOwner) return null;\n const effective = getActorEffectivePermissions(actor, actorRolePermissions);\n const myScopes = resolveAllScopes(effective, []);\n const grantedScopes = resolveAllScopes(proposed, []);\n for (const [scope, granted] of Object.entries(grantedScopes)) {\n if (granted && !myScopes[scope as Scope])\n return `cannot grant permission you do not have: ${scope}`;\n }\n return null;\n}\n\nexport function checkPermissionRevocation(\n current: PermissionSet,\n proposed: PermissionSet,\n): string | null {\n const currentScopes = resolveAllScopes(current, []);\n const proposedScopes = resolveAllScopes(proposed, []);\n for (const [scope, granted] of Object.entries(currentScopes)) {\n if (granted && !proposedScopes[scope as Scope])\n return `cannot revoke permission: ${scope}`;\n }\n return null;\n}\n\nexport function getGrantablePermissions(\n actorRoles: IStoreRole[],\n minOrder?: number,\n): PermissionSet {\n const qualifying =\n minOrder !== undefined\n ? actorRoles.filter(r => r.order > minOrder)\n : actorRoles;\n if (qualifying.length === 0) return EMPTY_PERMISSIONS;\n return mergePermissionSets(\n ...qualifying.map(r => decodePermissions(r.permissions)),\n );\n}\n\nexport function getActorEffectivePermissions(\n user: IStoreUser,\n rolePermissions: PermissionSet[],\n): PermissionSet {\n return mergePermissionSets(\n user.permissions ?? EMPTY_PERMISSIONS,\n ...rolePermissions,\n );\n}\n\nexport function validateHierarchy(\n actor: IStoreUser,\n actorRoles: IStoreRole[],\n targetRole: IStoreRole,\n requiredScope: Scope,\n allRolePermissions?: PermissionSet[],\n): string | null {\n if (actor.isOwner) return null;\n const order = getActorHighestOrder(\n actor,\n actorRoles,\n requiredScope,\n allRolePermissions,\n );\n if (order < 0) return `missing permission: ${requiredScope}`;\n if (!canManageRole(order, targetRole))\n return `cannot manage role \"${targetRole.name}\" (order ${targetRole.order}) from order ${order}`;\n return null;\n}\n\nexport async function checkUserHierarchy(\n actor: IStoreUser,\n target: IStoreUser,\n): Promise<string | null> {\n if (actor.isOwner) return null;\n if (target.isOwner) return 'cannot modify the owner';\n const { database } = getContextDeps();\n const targetRoles = await database.getUserAssignedRoles(target);\n if (targetRoles.length === 0) return null;\n const targetMaxOrder = Math.max(...targetRoles.map(r => r.order));\n const actorRoles = await database.getUserAssignedRoles(actor);\n const actorMaxOrder = Math.max(0, ...actorRoles.map(r => r.order));\n if (actorMaxOrder <= targetMaxOrder)\n return 'cannot modify a user with equal or higher role';\n return null;\n}\n\nexport async function checkRoleHierarchy(\n user: IStoreUser,\n role: IStoreRole & { _id: string },\n scope: Scope,\n): Promise<string | null> {\n if (user.isOwner) return null;\n const { database } = getContextDeps();\n const actorRoles = await database.getUserAssignedRoles(user);\n const rolePerms = await database.getUserRolePermissions(user);\n return validateHierarchy(user, actorRoles, role, scope, rolePerms);\n}\n"],"names":["decodePermissions","resolveAllScopes","EMPTY_PERMISSIONS","resolveScope","mergePermissionSets","getContextDeps"],"mappings":"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"}
@@ -3,12 +3,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const v4 = require("zod/v4");
4
4
  const EventEmitter = require("events");
5
5
  const trpc = require("../trpc.js");
6
+ const scopes = require("../scopes.js");
6
7
  const events = require("../events.js");
7
8
  const logger = require("../../../logger.js");
8
9
  const chat = require("../../../util/chat.js");
9
10
  const chatRouter = trpc.router({
10
11
  chat: trpc.router({
11
- send: trpc.protectedProcedure().input(v4.z.string()).mutation(async ({ input: message, ctx }) => {
12
+ send: trpc.protectedProcedure(scopes.ScopeName.ChatSend).input(v4.z.string()).mutation(async ({ input: message, ctx }) => {
12
13
  const { database, omegga } = trpc.getContextDeps();
13
14
  if (typeof message !== "string") return;
14
15
  if (message.length > 140) message = message.slice(0, 140);
@@ -26,11 +27,11 @@ const chatRouter = trpc.router({
26
27
  logger.default.log(`[${user.name.brightMagenta.underline}]: ${message}`);
27
28
  return "ok";
28
29
  }),
29
- recent: trpc.protectedProcedure().query(() => {
30
+ recent: trpc.protectedProcedure(scopes.ScopeName.ChatRecent).query(() => {
30
31
  const { database } = trpc.getContextDeps();
31
32
  return database.getChats({ sameServer: true });
32
33
  }),
33
- history: trpc.protectedProcedure().input(
34
+ history: trpc.protectedProcedure(scopes.ScopeName.ChatHistory).input(
34
35
  v4.z.object({
35
36
  after: v4.z.number().optional(),
36
37
  before: v4.z.number().optional()
@@ -39,14 +40,15 @@ const chatRouter = trpc.router({
39
40
  const { database } = trpc.getContextDeps();
40
41
  return database.getChats({ after: input.after, before: input.before });
41
42
  }),
42
- calendar: trpc.protectedProcedure().query(() => {
43
+ calendar: trpc.protectedProcedure(scopes.ScopeName.ChatCalendar).query(() => {
43
44
  const { database } = trpc.getContextDeps();
44
45
  return database.calendar.years;
45
46
  }),
46
- onMessage: trpc.protectedProcedure().subscription(
47
- async function* ({ signal }) {
47
+ onMessage: trpc.protectedProcedure(scopes.ScopeName.ChatRecent).subscription(
48
+ async function* ({ signal, ctx }) {
49
+ const combined = AbortSignal.any([signal, ctx.userAbort.signal]);
48
50
  for await (const [chatLog] of EventEmitter.on(events.serverEvents, "chat", {
49
- signal
51
+ signal: combined
50
52
  })) {
51
53
  yield chatLog;
52
54
  }
@@ -1 +1 @@
1
- {"version":3,"file":"chat.js","sources":["../../../../src/webserver/backend/router/chat.ts"],"sourcesContent":["import { z } from 'zod/v4';\nimport { on } from 'events';\nimport { router, protectedProcedure, getContextDeps } from '../trpc';\nimport { serverEvents } from '../events';\nimport Logger from '@/logger';\nimport { parseLinks, sanitize } from '@util/chat';\n\nexport const chatRouter = router({\n chat: router({\n send: protectedProcedure('chat.send')\n .input(z.string())\n .mutation(async ({ input: message, ctx }) => {\n const { database, omegga } = getContextDeps();\n\n if (typeof message !== 'string') return;\n if (message.length > 140) message = message.slice(0, 140);\n\n const user = {\n name: ctx.user.username || 'Admin',\n id: ctx.user.playerId,\n color: 'ff00ff',\n web: true,\n };\n\n const chatLog = await database.addChatLog('msg', user, message);\n serverEvents.emit('chat', chatLog);\n\n omegga.broadcast(\n `\"[<b><color=\\\\\"ff00ff\\\\\">${user.name}</></>]: ${parseLinks(sanitize(message))}\"`,\n );\n Logger.log(`[${user.name.brightMagenta.underline}]: ${message}`);\n\n return 'ok';\n }),\n\n recent: protectedProcedure('chat.recent').query(() => {\n const { database } = getContextDeps();\n return database.getChats({ sameServer: true });\n }),\n\n history: protectedProcedure('chat.history')\n .input(\n z.object({\n after: z.number().optional(),\n before: z.number().optional(),\n }),\n )\n .query(({ input }) => {\n const { database } = getContextDeps();\n return database.getChats({ after: input.after, before: input.before });\n }),\n\n calendar: protectedProcedure('chat.calendar').query(() => {\n const { database } = getContextDeps();\n return database.calendar.years;\n }),\n\n onMessage: protectedProcedure('chat.onMessage').subscription(\n async function* ({ signal }) {\n for await (const [chatLog] of on(serverEvents, 'chat', {\n signal,\n })) {\n yield chatLog;\n }\n },\n ),\n }),\n});\n"],"names":["router","protectedProcedure","z","getContextDeps","serverEvents","parseLinks","sanitize","Logger","on"],"mappings":";;;;;;;;AAOO,MAAM,aAAaA,KAAAA,OAAO;AAAA,EAC/B,MAAMA,KAAAA,OAAO;AAAA,IACX,MAAMC,KAAAA,mBAA8B,EACjC,MAAMC,GAAAA,EAAE,OAAA,CAAQ,EAChB,SAAS,OAAO,EAAE,OAAO,SAAS,UAAU;AAC3C,YAAM,EAAE,UAAU,OAAA,IAAWC,oBAAA;AAE7B,UAAI,OAAO,YAAY,SAAU;AACjC,UAAI,QAAQ,SAAS,eAAe,QAAQ,MAAM,GAAG,GAAG;AAExD,YAAM,OAAO;AAAA,QACX,MAAM,IAAI,KAAK,YAAY;AAAA,QAC3B,IAAI,IAAI,KAAK;AAAA,QACb,OAAO;AAAA,QACP,KAAK;AAAA,MAAA;AAGP,YAAM,UAAU,MAAM,SAAS,WAAW,OAAO,MAAM,OAAO;AAC9DC,0BAAa,KAAK,QAAQ,OAAO;AAEjC,aAAO;AAAA,QACL,4BAA4B,KAAK,IAAI,YAAYC,KAAAA,WAAWC,KAAAA,SAAS,OAAO,CAAC,CAAC;AAAA,MAAA;AAEhFC,aAAAA,QAAO,IAAI,IAAI,KAAK,KAAK,cAAc,SAAS,MAAM,OAAO,EAAE;AAE/D,aAAO;AAAA,IACT,CAAC;AAAA,IAEH,QAAQN,KAAAA,mBAAgC,EAAE,MAAM,MAAM;AACpD,YAAM,EAAE,SAAA,IAAaE,oBAAA;AACrB,aAAO,SAAS,SAAS,EAAE,YAAY,MAAM;AAAA,IAC/C,CAAC;AAAA,IAED,SAASF,KAAAA,mBAAiC,EACvC;AAAA,MACCC,GAAAA,EAAE,OAAO;AAAA,QACP,OAAOA,GAAAA,EAAE,OAAA,EAAS,SAAA;AAAA,QAClB,QAAQA,GAAAA,EAAE,OAAA,EAAS,SAAA;AAAA,MAAS,CAC7B;AAAA,IAAA,EAEF,MAAM,CAAC,EAAE,YAAY;AACpB,YAAM,EAAE,SAAA,IAAaC,oBAAA;AACrB,aAAO,SAAS,SAAS,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,QAAQ;AAAA,IACvE,CAAC;AAAA,IAEH,UAAUF,KAAAA,mBAAkC,EAAE,MAAM,MAAM;AACxD,YAAM,EAAE,SAAA,IAAaE,oBAAA;AACrB,aAAO,SAAS,SAAS;AAAA,IAC3B,CAAC;AAAA,IAED,WAAWF,KAAAA,mBAAmC,EAAE;AAAA,MAC9C,iBAAiB,EAAE,OAAA,GAAU;AAC3B,yBAAiB,CAAC,OAAO,KAAKO,aAAAA,GAAGJ,OAAAA,cAAc,QAAQ;AAAA,UACrD;AAAA,QAAA,CACD,GAAG;AACF,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IAAA;AAAA,EACF,CACD;AACH,CAAC;;"}
1
+ {"version":3,"file":"chat.js","sources":["../../../../src/webserver/backend/router/chat.ts"],"sourcesContent":["import { z } from 'zod/v4';\nimport { on } from 'events';\nimport { router, protectedProcedure, getContextDeps } from '../trpc';\nimport { ScopeName } from '../scopes';\nimport { serverEvents } from '../events';\nimport Logger from '@/logger';\nimport { parseLinks, sanitize } from '@util/chat';\n\nexport const chatRouter = router({\n chat: router({\n send: protectedProcedure(ScopeName.ChatSend)\n .input(z.string())\n .mutation(async ({ input: message, ctx }) => {\n const { database, omegga } = getContextDeps();\n\n if (typeof message !== 'string') return;\n if (message.length > 140) message = message.slice(0, 140);\n\n const user = {\n name: ctx.user.username || 'Admin',\n id: ctx.user.playerId,\n color: 'ff00ff',\n web: true,\n };\n\n const chatLog = await database.addChatLog('msg', user, message);\n serverEvents.emit('chat', chatLog);\n\n omegga.broadcast(\n `\"[<b><color=\\\\\"ff00ff\\\\\">${user.name}</></>]: ${parseLinks(sanitize(message))}\"`,\n );\n Logger.log(`[${user.name.brightMagenta.underline}]: ${message}`);\n\n return 'ok';\n }),\n\n recent: protectedProcedure(ScopeName.ChatRecent).query(() => {\n const { database } = getContextDeps();\n return database.getChats({ sameServer: true });\n }),\n\n history: protectedProcedure(ScopeName.ChatHistory)\n .input(\n z.object({\n after: z.number().optional(),\n before: z.number().optional(),\n }),\n )\n .query(({ input }) => {\n const { database } = getContextDeps();\n return database.getChats({ after: input.after, before: input.before });\n }),\n\n calendar: protectedProcedure(ScopeName.ChatCalendar).query(() => {\n const { database } = getContextDeps();\n return database.calendar.years;\n }),\n\n onMessage: protectedProcedure(ScopeName.ChatRecent).subscription(\n async function* ({ signal, ctx }) {\n const combined = AbortSignal.any([signal!, ctx.userAbort.signal]);\n for await (const [chatLog] of on(serverEvents, 'chat', {\n signal: combined,\n })) {\n yield chatLog;\n }\n },\n ),\n }),\n});\n"],"names":["router","protectedProcedure","ScopeName","z","getContextDeps","serverEvents","parseLinks","sanitize","Logger","on"],"mappings":";;;;;;;;;AAQO,MAAM,aAAaA,KAAAA,OAAO;AAAA,EAC/B,MAAMA,KAAAA,OAAO;AAAA,IACX,MAAMC,KAAAA,mBAAmBC,OAAAA,UAAU,QAAQ,EACxC,MAAMC,GAAAA,EAAE,OAAA,CAAQ,EAChB,SAAS,OAAO,EAAE,OAAO,SAAS,UAAU;AAC3C,YAAM,EAAE,UAAU,OAAA,IAAWC,oBAAA;AAE7B,UAAI,OAAO,YAAY,SAAU;AACjC,UAAI,QAAQ,SAAS,eAAe,QAAQ,MAAM,GAAG,GAAG;AAExD,YAAM,OAAO;AAAA,QACX,MAAM,IAAI,KAAK,YAAY;AAAA,QAC3B,IAAI,IAAI,KAAK;AAAA,QACb,OAAO;AAAA,QACP,KAAK;AAAA,MAAA;AAGP,YAAM,UAAU,MAAM,SAAS,WAAW,OAAO,MAAM,OAAO;AAC9DC,0BAAa,KAAK,QAAQ,OAAO;AAEjC,aAAO;AAAA,QACL,4BAA4B,KAAK,IAAI,YAAYC,KAAAA,WAAWC,KAAAA,SAAS,OAAO,CAAC,CAAC;AAAA,MAAA;AAEhFC,aAAAA,QAAO,IAAI,IAAI,KAAK,KAAK,cAAc,SAAS,MAAM,OAAO,EAAE;AAE/D,aAAO;AAAA,IACT,CAAC;AAAA,IAEH,QAAQP,KAAAA,mBAAmBC,OAAAA,UAAU,UAAU,EAAE,MAAM,MAAM;AAC3D,YAAM,EAAE,SAAA,IAAaE,oBAAA;AACrB,aAAO,SAAS,SAAS,EAAE,YAAY,MAAM;AAAA,IAC/C,CAAC;AAAA,IAED,SAASH,KAAAA,mBAAmBC,iBAAU,WAAW,EAC9C;AAAA,MACCC,GAAAA,EAAE,OAAO;AAAA,QACP,OAAOA,GAAAA,EAAE,OAAA,EAAS,SAAA;AAAA,QAClB,QAAQA,GAAAA,EAAE,OAAA,EAAS,SAAA;AAAA,MAAS,CAC7B;AAAA,IAAA,EAEF,MAAM,CAAC,EAAE,YAAY;AACpB,YAAM,EAAE,SAAA,IAAaC,oBAAA;AACrB,aAAO,SAAS,SAAS,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,QAAQ;AAAA,IACvE,CAAC;AAAA,IAEH,UAAUH,KAAAA,mBAAmBC,OAAAA,UAAU,YAAY,EAAE,MAAM,MAAM;AAC/D,YAAM,EAAE,SAAA,IAAaE,oBAAA;AACrB,aAAO,SAAS,SAAS;AAAA,IAC3B,CAAC;AAAA,IAED,WAAWH,KAAAA,mBAAmBC,iBAAU,UAAU,EAAE;AAAA,MAClD,iBAAiB,EAAE,QAAQ,OAAO;AAChC,cAAM,WAAW,YAAY,IAAI,CAAC,QAAS,IAAI,UAAU,MAAM,CAAC;AAChE,yBAAiB,CAAC,OAAO,KAAKO,aAAAA,GAAGJ,OAAAA,cAAc,QAAQ;AAAA,UACrD,QAAQ;AAAA,QAAA,CACT,GAAG;AACF,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IAAA;AAAA,EACF,CACD;AACH,CAAC;;"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const trpc = require("../trpc.js");
4
4
  const chat = require("./chat.js");
5
+ const mfa = require("./mfa.js");
5
6
  const player = require("./player.js");
6
7
  const plugin = require("./plugin.js");
7
8
  const role = require("./role.js");
@@ -17,7 +18,8 @@ const appRouter = trpc.mergeRouters(
17
18
  server.serverRouter,
18
19
  user.userRouter,
19
20
  world.worldRouter,
20
- role.roleRouter
21
+ role.roleRouter,
22
+ mfa.mfaRouter
21
23
  );
22
24
  exports.appRouter = appRouter;
23
25
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/webserver/backend/router/index.ts"],"sourcesContent":["import { mergeRouters } from '../trpc';\nimport { chatRouter } from './chat';\nimport { playerRouter } from './player';\nimport { pluginRouter } from './plugin';\nimport { roleRouter } from './role';\nimport { serverRouter } from './server';\nimport { sessionRouter } from './session';\nimport { userRouter } from './user';\nimport { worldRouter } from './world';\n\nexport const appRouter = mergeRouters(\n sessionRouter,\n chatRouter,\n playerRouter,\n pluginRouter,\n serverRouter,\n userRouter,\n worldRouter,\n roleRouter,\n);\n\nexport type AppRouter = typeof appRouter;\n"],"names":["mergeRouters","sessionRouter","chatRouter","playerRouter","pluginRouter","serverRouter","userRouter","worldRouter","roleRouter"],"mappings":";;;;;;;;;;;AAUO,MAAM,YAAYA,KAAAA;AAAAA,EACvBC,QAAAA;AAAAA,EACAC,KAAAA;AAAAA,EACAC,OAAAA;AAAAA,EACAC,OAAAA;AAAAA,EACAC,OAAAA;AAAAA,EACAC,KAAAA;AAAAA,EACAC,MAAAA;AAAAA,EACAC,KAAAA;AACF;;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/webserver/backend/router/index.ts"],"sourcesContent":["import { mergeRouters } from '../trpc';\nimport { chatRouter } from './chat';\nimport { mfaRouter } from './mfa';\nimport { playerRouter } from './player';\nimport { pluginRouter } from './plugin';\nimport { roleRouter } from './role';\nimport { serverRouter } from './server';\nimport { sessionRouter } from './session';\nimport { userRouter } from './user';\nimport { worldRouter } from './world';\n\nexport const appRouter = mergeRouters(\n sessionRouter,\n chatRouter,\n playerRouter,\n pluginRouter,\n serverRouter,\n userRouter,\n worldRouter,\n roleRouter,\n mfaRouter,\n);\n\nexport type AppRouter = typeof appRouter;\n"],"names":["mergeRouters","sessionRouter","chatRouter","playerRouter","pluginRouter","serverRouter","userRouter","worldRouter","roleRouter","mfaRouter"],"mappings":";;;;;;;;;;;;AAWO,MAAM,YAAYA,KAAAA;AAAAA,EACvBC,QAAAA;AAAAA,EACAC,KAAAA;AAAAA,EACAC,OAAAA;AAAAA,EACAC,OAAAA;AAAAA,EACAC,OAAAA;AAAAA,EACAC,KAAAA;AAAAA,EACAC,MAAAA;AAAAA,EACAC,KAAAA;AAAAA,EACAC,IAAAA;AACF;;"}