@axium/server 0.20.6 → 0.21.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.
Files changed (58) hide show
  1. package/build/client/_app/immutable/chunks/BT6JVi7A.js +3 -0
  2. package/build/client/_app/immutable/chunks/BT6JVi7A.js.br +0 -0
  3. package/build/client/_app/immutable/chunks/BT6JVi7A.js.gz +0 -0
  4. package/build/client/_app/immutable/entry/{app.DN-14WEp.js → app.ZFo0Iz7Y.js} +2 -2
  5. package/build/client/_app/immutable/entry/app.ZFo0Iz7Y.js.br +0 -0
  6. package/build/client/_app/immutable/entry/app.ZFo0Iz7Y.js.gz +0 -0
  7. package/build/client/_app/immutable/entry/start.BeJCp-7l.js +1 -0
  8. package/build/client/_app/immutable/entry/start.BeJCp-7l.js.br +2 -0
  9. package/build/client/_app/immutable/entry/start.BeJCp-7l.js.gz +0 -0
  10. package/build/client/_app/immutable/nodes/{1.CYGWa5Ht.js → 1.jmd85CY0.js} +1 -1
  11. package/build/client/_app/immutable/nodes/1.jmd85CY0.js.br +0 -0
  12. package/build/client/_app/immutable/nodes/1.jmd85CY0.js.gz +0 -0
  13. package/build/client/_app/version.json +1 -1
  14. package/build/client/_app/version.json.br +0 -0
  15. package/build/client/_app/version.json.gz +0 -0
  16. package/build/server/chunks/{1-ChHp-lcj.js → 1-IUsQDUky.js} +2 -2
  17. package/build/server/chunks/{1-ChHp-lcj.js.map → 1-IUsQDUky.js.map} +1 -1
  18. package/build/server/chunks/{3-qz4gvEJj.js → 3-BfZcxicd.js} +2 -2
  19. package/build/server/chunks/{3-qz4gvEJj.js.map → 3-BfZcxicd.js.map} +1 -1
  20. package/build/server/chunks/{_page.svelte-CNZCbT6U.js → _page.svelte-DnC0rUFy.js} +2 -2
  21. package/build/server/chunks/{_page.svelte-CNZCbT6U.js.map → _page.svelte-DnC0rUFy.js.map} +1 -1
  22. package/build/server/chunks/{hooks.server-D31Irsqw.js → hooks.server-DHvv3JCJ.js} +162 -29
  23. package/build/server/chunks/hooks.server-DHvv3JCJ.js.map +1 -0
  24. package/build/server/chunks/{string-CafUlmcI.js → string-SLyGnEy-.js} +6 -1
  25. package/build/server/chunks/{string-CafUlmcI.js.map → string-SLyGnEy-.js.map} +1 -1
  26. package/build/server/index.js +2 -2
  27. package/build/server/index.js.map +1 -1
  28. package/build/server/manifest.js +3 -3
  29. package/build/server/manifest.js.map +1 -1
  30. package/dist/api/metadata.js +15 -4
  31. package/dist/api/register.js +2 -0
  32. package/dist/api/session.js +2 -0
  33. package/dist/api/users.js +6 -1
  34. package/dist/audit.d.ts +84 -0
  35. package/dist/audit.js +125 -0
  36. package/dist/auth.d.ts +1 -0
  37. package/dist/auth.js +10 -2
  38. package/dist/cli.js +95 -31
  39. package/dist/config.d.ts +14 -8
  40. package/dist/config.js +20 -10
  41. package/dist/database.d.ts +12 -0
  42. package/dist/database.js +28 -2
  43. package/dist/io.d.ts +4 -0
  44. package/dist/io.js +9 -0
  45. package/dist/requests.d.ts +1 -2
  46. package/dist/requests.js +1 -1
  47. package/package.json +1 -1
  48. package/build/client/_app/immutable/chunks/LvObEX8u.js +0 -3
  49. package/build/client/_app/immutable/chunks/LvObEX8u.js.br +0 -0
  50. package/build/client/_app/immutable/chunks/LvObEX8u.js.gz +0 -0
  51. package/build/client/_app/immutable/entry/app.DN-14WEp.js.br +0 -0
  52. package/build/client/_app/immutable/entry/app.DN-14WEp.js.gz +0 -0
  53. package/build/client/_app/immutable/entry/start.AjI7jPvA.js +0 -1
  54. package/build/client/_app/immutable/entry/start.AjI7jPvA.js.br +0 -2
  55. package/build/client/_app/immutable/entry/start.AjI7jPvA.js.gz +0 -0
  56. package/build/client/_app/immutable/nodes/1.CYGWa5Ht.js.br +0 -0
  57. package/build/client/_app/immutable/nodes/1.CYGWa5Ht.js.gz +0 -0
  58. package/build/server/chunks/hooks.server-D31Irsqw.js.map +0 -1
@@ -4,7 +4,7 @@ import 'node:http';
4
4
  import 'node:https';
5
5
  import { Logger, levelText, allLogLevels } from 'logzen';
6
6
  import { join, resolve, dirname } from 'node:path/posix';
7
- import './string-CafUlmcI.js';
7
+ import { c as capitalize } from './string-SLyGnEy-.js';
8
8
  import 'node:child_process';
9
9
  import { homedir } from 'node:os';
10
10
  import { styleText } from 'node:util';
@@ -187,7 +187,7 @@ const logger = new Logger({
187
187
  /**
188
188
  * @internal
189
189
  */
190
- const output = {
190
+ const output$1 = {
191
191
  constructor: { name: 'Console' },
192
192
  error(message) {
193
193
  console.error(message.startsWith('\x1b') ? message : styleText('red', message));
@@ -205,7 +205,7 @@ const output = {
205
205
  _debugOutput && console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
206
206
  },
207
207
  };
208
- logger.attach(output);
208
+ logger.attach(output$1);
209
209
  let _debugOutput = false;
210
210
  /**
211
211
  * Enable or disable debug output.
@@ -216,7 +216,7 @@ function _setDebugOutput(enabled) {
216
216
  function defaultOutput(tag, message = '') {
217
217
  switch (tag) {
218
218
  case 'debug':
219
- _debugOutput && output.debug(message);
219
+ _debugOutput && output$1.debug(message);
220
220
  break;
221
221
  case 'info':
222
222
  console.log(message);
@@ -254,6 +254,9 @@ function debug(message) {
254
254
  function warn(message) {
255
255
  _taggedOutput?.('warn', message);
256
256
  }
257
+ function error$1(message) {
258
+ _taggedOutput?.('error', message);
259
+ }
257
260
  /**
258
261
  * This is a factory for handling errors when performing operations.
259
262
  * The handler will allow the parent scope to continue if a relation already exists,
@@ -271,6 +274,15 @@ function someWarnings(...allowList) {
271
274
  throw error;
272
275
  };
273
276
  }
277
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
278
+ // Shortcut to convert to 2-digit. Mostly used to make the line shorter.
279
+ const _2 = (v) => v.toString().padStart(2, '0');
280
+ /**
281
+ * Get a human-readable string for a date that also fits into CLIs well (fixed-width)
282
+ */
283
+ function prettyDate(date) {
284
+ return `${date.getFullYear()} ${months[date.getMonth()]} ${_2(date.getDate())} ${_2(date.getHours())}:${_2(date.getMinutes())}:${_2(date.getSeconds())}.${_2(date.getMilliseconds())}`;
285
+ }
274
286
 
275
287
  function zAsyncFunction(schema) {
276
288
  return custom((fn) => schema.implementAsync(fn));
@@ -346,24 +358,29 @@ async function loadPlugin(specifier) {
346
358
  throw 'Invalid plugin name. Plugin names can not start with a hash or contain spaces.';
347
359
  }
348
360
  plugins.add(plugin);
349
- output.debug(`Loaded plugin: ${plugin.name} ${plugin.version}`);
361
+ output$1.debug(`Loaded plugin: ${plugin.name} ${plugin.version}`);
350
362
  }
351
363
  catch (e) {
352
- output.debug(`Failed to load plugin from ${specifier}: ${e ? (e instanceof Error ? e.message : e.toString()) : e}`);
364
+ output$1.debug(`Failed to load plugin from ${specifier}: ${e ? (e instanceof Error ? e.message : e.toString()) : e}`);
353
365
  }
354
366
  }
355
367
 
368
+ const audit_severity_levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
369
+ const z_audit_severity = literal([...audit_severity_levels, ...audit_severity_levels.map(capitalize)]);
356
370
  const ConfigSchema = looseObject({
357
371
  allow_new_users: boolean(),
358
- api: looseObject({
359
- disable_metadata: boolean(),
360
- cookie_auth: boolean(),
361
- })
362
- .partial(),
363
372
  apps: looseObject({
364
373
  disabled: array(string()),
365
374
  })
366
375
  .partial(),
376
+ audit: looseObject({
377
+ allow_raw: boolean(),
378
+ /** How many days to keep events in the audit log */
379
+ retention: number().min(0),
380
+ min_severity: z_audit_severity,
381
+ auto_suspend: z_audit_severity,
382
+ })
383
+ .partial(),
367
384
  auth: looseObject({
368
385
  origin: string(),
369
386
  /** In minutes */
@@ -375,6 +392,8 @@ const ConfigSchema = looseObject({
375
392
  verification_timeout: number(),
376
393
  /** Whether users can verify emails */
377
394
  email_verification: boolean(),
395
+ /** Whether only the `Authorization` header can be used to authenticate requests. */
396
+ header_only: boolean(),
378
397
  })
379
398
  .partial(),
380
399
  db: looseObject({
@@ -424,13 +443,15 @@ const configShortcuts = {
424
443
  const config = _unique('config', {
425
444
  ...configShortcuts,
426
445
  allow_new_users: true,
427
- api: {
428
- disable_metadata: false,
429
- cookie_auth: true,
430
- },
431
446
  apps: {
432
447
  disabled: [],
433
448
  },
449
+ audit: {
450
+ allow_raw: false,
451
+ retention: 30,
452
+ min_severity: 'error',
453
+ auto_suspend: 'critical',
454
+ },
434
455
  auth: {
435
456
  origin: 'https://test.localhost',
436
457
  passkey_probation: 60,
@@ -439,6 +460,7 @@ const config = _unique('config', {
439
460
  secure_cookies: true,
440
461
  verification_timeout: 60,
441
462
  email_verification: false,
463
+ header_only: false,
442
464
  },
443
465
  db: {
444
466
  database: process.env.PGDATABASE || 'axium',
@@ -478,9 +500,9 @@ const FileSchema = looseObject({
478
500
  */
479
501
  function setConfig(other) {
480
502
  deepAssign(config, other);
481
- logger.detach(output);
503
+ logger.detach(output$1);
482
504
  if (config.log.console)
483
- logger.attach(output, { output: config.log.level });
505
+ logger.attach(output$1, { output: config.log.level });
484
506
  _setDebugOutput(config.debug);
485
507
  _duplicateStateWarnings(config.show_duplicate_state);
486
508
  }
@@ -497,7 +519,7 @@ async function loadConfig(path, options = {}) {
497
519
  catch (e) {
498
520
  if (!options.optional)
499
521
  throw e;
500
- output.debug(`Skipping config at ${path} (${e.message})`);
522
+ output$1.debug(`Skipping config at ${path} (${e.message})`);
501
523
  return;
502
524
  }
503
525
  let file;
@@ -507,12 +529,12 @@ async function loadConfig(path, options = {}) {
507
529
  catch (e) {
508
530
  if (!options.loose)
509
531
  throw e;
510
- output.debug(`Loading invalid config from ${path} (${e.message})`);
532
+ output$1.debug(`Loading invalid config from ${path} (${e.message})`);
511
533
  file = json;
512
534
  }
513
535
  configFiles.set(path, file);
514
536
  setConfig(file);
515
- output.debug('Loaded config: ' + path);
537
+ output$1.debug('Loaded config: ' + path);
516
538
  for (const include of file.include ?? [])
517
539
  await loadConfig(join(dirname(path), include), { optional: true });
518
540
  for (const plugin of file.plugins ?? [])
@@ -544,7 +566,7 @@ function saveConfigTo(path, changed) {
544
566
  setConfig(changed);
545
567
  const config = configFiles.get(path) ?? {};
546
568
  Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
547
- output.debug(`Wrote config to ${path}`);
569
+ output$1.debug(`Wrote config to ${path}`);
548
570
  writeFileSync(path, JSON.stringify(config));
549
571
  }
550
572
  /**
@@ -562,7 +584,7 @@ if (process.env.AXIUM_CONFIG)
562
584
 
563
585
  const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH'];
564
586
 
565
- var version = "0.20.6";
587
+ var version = "0.21.0";
566
588
  var pkg = {
567
589
  version: version};
568
590
 
@@ -16437,6 +16459,98 @@ async function clean(opt) {
16437
16459
  }
16438
16460
  }
16439
16461
 
16462
+ var Severity;
16463
+ (function (Severity) {
16464
+ Severity[Severity["Emergency"] = 0] = "Emergency";
16465
+ Severity[Severity["Alert"] = 1] = "Alert";
16466
+ Severity[Severity["Critical"] = 2] = "Critical";
16467
+ Severity[Severity["Error"] = 3] = "Error";
16468
+ Severity[Severity["Warning"] = 4] = "Warning";
16469
+ Severity[Severity["Notice"] = 5] = "Notice";
16470
+ Severity[Severity["Info"] = 6] = "Info";
16471
+ Severity[Severity["Debug"] = 7] = "Debug";
16472
+ })(Severity || (Severity = {}));
16473
+ const severityFormat = {
16474
+ [Severity.Emergency]: ['bgRedBright', 'white', 'underline'],
16475
+ [Severity.Alert]: ['bgRedBright', 'white'],
16476
+ [Severity.Critical]: ['bold', 'redBright'],
16477
+ [Severity.Error]: 'redBright',
16478
+ [Severity.Warning]: 'yellowBright',
16479
+ [Severity.Notice]: 'cyanBright',
16480
+ [Severity.Info]: [],
16481
+ [Severity.Debug]: ['dim'],
16482
+ };
16483
+ function styleSeverity(sev, align = false) {
16484
+ const text = align ? Severity[sev].padEnd(9) : Severity[sev];
16485
+ return styleText(severityFormat[sev], text.toUpperCase());
16486
+ }
16487
+ function output(event) {
16488
+ if (event.severity > Severity[capitalize(config.audit.min_severity)])
16489
+ return;
16490
+ console.error('[audit]', styleText('dim', prettyDate(event.timestamp)), styleSeverity(event.severity), event.name);
16491
+ }
16492
+ const events = new Map();
16493
+ function addEvent(init) {
16494
+ if (events.has(init.name))
16495
+ throw error$1(`Can not register multiple events with the same name ("${init.name}")`);
16496
+ const config = {
16497
+ ...init,
16498
+ extra: init.extra ? object(init.extra) : undefined,
16499
+ };
16500
+ events.set(init.name, config);
16501
+ }
16502
+ async function audit(eventName, userId, extra) {
16503
+ const cfg = events.get(eventName);
16504
+ if (!cfg) {
16505
+ warn('Ignoring audit event with unknown event name: ' + eventName);
16506
+ return;
16507
+ }
16508
+ try {
16509
+ if (cfg.extra)
16510
+ extra = cfg.extra.parse(extra);
16511
+ }
16512
+ catch (e) {
16513
+ error$1('Audit event has invalid extra data: ' + eventName);
16514
+ return;
16515
+ }
16516
+ const event = await database
16517
+ .insertInto('audit_log')
16518
+ .values({ ...omit(cfg, 'extra'), extra, userId })
16519
+ .returningAll()
16520
+ .executeTakeFirstOrThrow();
16521
+ output(event);
16522
+ if (userId && !cfg.noAutoSuspend && event.severity < Severity[capitalize(config.audit.auto_suspend)]) {
16523
+ await database
16524
+ .updateTable('users')
16525
+ .set({ isSuspended: true })
16526
+ .where('id', '=', userId)
16527
+ .returningAll()
16528
+ .executeTakeFirstOrThrow()
16529
+ .then(user => console.error('[audit] Auto-suspended user:', user.id, `<${user.email}>`))
16530
+ .catch(() => null);
16531
+ }
16532
+ }
16533
+ // Register built-ins
16534
+ addEvent({ source: '@axium/server', name: 'user_created', severity: Severity.Info, tags: ['user'] });
16535
+ addEvent({ source: '@axium/server', name: 'user_deleted', severity: Severity.Info, tags: ['user'] });
16536
+ addEvent({ source: '@axium/server', name: 'new_session', severity: Severity.Info, tags: ['user'], extra: { id: string() } });
16537
+ addEvent({ source: '@axium/server', name: 'logout', severity: Severity.Info, tags: ['user'], extra: { sessions: array(string()) } });
16538
+ addEvent({
16539
+ source: '@axium/server',
16540
+ name: 'admin_change',
16541
+ severity: Severity.Notice,
16542
+ tags: ['cli'],
16543
+ extra: { user: string() },
16544
+ });
16545
+ addEvent({
16546
+ source: '@axium/server',
16547
+ name: 'acl_id_mismatch',
16548
+ severity: Severity.Critical,
16549
+ tags: ['acl', 'auth'],
16550
+ extra: { item: string() },
16551
+ noAutoSuspend: true,
16552
+ });
16553
+
16440
16554
  async function getUser(id) {
16441
16555
  return await database.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
16442
16556
  }
@@ -16452,6 +16566,7 @@ async function createSession(userId, elevated = false) {
16452
16566
  created: new Date(),
16453
16567
  };
16454
16568
  await database.insertInto('sessions').values(session).execute();
16569
+ await audit('new_session', userId, { id: session.id });
16455
16570
  return session;
16456
16571
  }
16457
16572
  async function getSessionAndUser(token) {
@@ -16505,6 +16620,8 @@ async function checkAuthForUser(event, userId, sensitive = false) {
16505
16620
  if (!token)
16506
16621
  throw error(401, 'Missing token');
16507
16622
  const session = await getSessionAndUser(token).catch(withError('Invalid or expired session', 401));
16623
+ if (session.user.isSuspended)
16624
+ error(403, 'User is suspended');
16508
16625
  if (session.userId !== userId) {
16509
16626
  if (!session.user?.isAdmin)
16510
16627
  error(403, 'User ID mismatch');
@@ -16552,7 +16669,7 @@ function getToken(event, sensitive = false) {
16552
16669
  const header_token = event.request.headers.get('Authorization')?.replace('Bearer ', '');
16553
16670
  if (header_token)
16554
16671
  return header_token;
16555
- if (config.debug || config.api.cookie_auth) {
16672
+ if (config.debug || !config.auth.header_only) {
16556
16673
  return event.cookies.get(sensitive ? 'elevated_token' : 'session_token');
16557
16674
  }
16558
16675
  }
@@ -16643,7 +16760,7 @@ function addRoute(opt) {
16643
16760
  if (route.api && !route.server)
16644
16761
  throw new Error(`API routes cannot have a client page: ${route.path}`);
16645
16762
  routes.set(route.path, route);
16646
- output.debug('Added route: ' + route.path);
16763
+ output$1.debug('Added route: ' + route.path);
16647
16764
  }
16648
16765
  /**
16649
16766
  * Resolve a request URL into a route.
@@ -16681,9 +16798,19 @@ function resolveRoute(event) {
16681
16798
 
16682
16799
  addRoute({
16683
16800
  path: '/api/metadata',
16684
- async GET() {
16685
- if (config.api.disable_metadata)
16686
- error(401, 'API metadata is disabled');
16801
+ async GET(event) {
16802
+ if (!config.debug) {
16803
+ const token = getToken(event);
16804
+ if (!token)
16805
+ error(401, 'Missing session token');
16806
+ const session = await getSessionAndUser(token);
16807
+ if (!session)
16808
+ error(401, 'Invalid session');
16809
+ if (!session.user.isAdmin)
16810
+ error(403, 'User is not an administrator');
16811
+ if (session.user.isSuspended)
16812
+ error(403, 'User is suspended');
16813
+ }
16687
16814
  return {
16688
16815
  version: pkg.version,
16689
16816
  routes: Object.fromEntries(routes
@@ -16793,6 +16920,7 @@ async function POST(event) {
16793
16920
  .values({ id: userId, name, email: email.toLowerCase() })
16794
16921
  .executeTakeFirstOrThrow()
16795
16922
  .catch(withError('Failed to create user'));
16923
+ await audit('user_created', userId);
16796
16924
  await createPasskey({
16797
16925
  transports: [],
16798
16926
  ...registrationInfo.credential,
@@ -16831,6 +16959,7 @@ addRoute({
16831
16959
  .returningAll()
16832
16960
  .executeTakeFirstOrThrow()
16833
16961
  .catch((e) => (e.message == 'no result' ? error(404, 'Session does not exist') : error(400, 'Invalid session')));
16962
+ await audit('logout', result.userId, { sessions: [result.id] });
16834
16963
  return omit(result, 'token');
16835
16964
  },
16836
16965
  });
@@ -16886,6 +17015,7 @@ addRoute({
16886
17015
  .returningAll()
16887
17016
  .executeTakeFirstOrThrow()
16888
17017
  .catch(withError('Failed to delete user'));
17018
+ await audit('user_deleted', userId);
16889
17019
  return result;
16890
17020
  },
16891
17021
  });
@@ -16908,7 +17038,9 @@ addRoute({
16908
17038
  async OPTIONS(event) {
16909
17039
  const userId = event.params.id;
16910
17040
  const { type } = await parseBody(event, UserAuthOptions);
16911
- await getUser(userId).catch(withError('User does not exist', 404));
17041
+ const user = await getUser(userId).catch(withError('User does not exist', 404));
17042
+ if (user.isSuspended)
17043
+ error(403, 'User is suspended');
16912
17044
  const passkeys = await getPasskeysByUserId(userId);
16913
17045
  if (!passkeys)
16914
17046
  error(409, 'No passkeys exists for this user');
@@ -17038,6 +17170,7 @@ addRoute({
17038
17170
  .returningAll()
17039
17171
  .execute()
17040
17172
  .catch(withError('Failed to delete one or more sessions'));
17173
+ await audit('logout', userId, { sessions: result.map(s => s.id) });
17041
17174
  return result.map(s => omit(s, 'token'));
17042
17175
  },
17043
17176
  });
@@ -17141,4 +17274,4 @@ async function handleSvelteKit({ event, resolve, }) {
17141
17274
  await init();
17142
17275
 
17143
17276
  export { handleSvelteKit as handle };
17144
- //# sourceMappingURL=hooks.server-D31Irsqw.js.map
17277
+ //# sourceMappingURL=hooks.server-DHvv3JCJ.js.map