@axium/server 0.9.0 → 0.11.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 (77) hide show
  1. package/assets/icons/brands.svg +1493 -0
  2. package/{web/api/index.ts → dist/api/index.d.ts} +0 -2
  3. package/dist/api/index.js +5 -0
  4. package/dist/api/metadata.d.ts +1 -0
  5. package/dist/api/metadata.js +28 -0
  6. package/dist/api/passkeys.d.ts +1 -0
  7. package/dist/api/passkeys.js +50 -0
  8. package/dist/api/register.d.ts +1 -0
  9. package/dist/api/register.js +70 -0
  10. package/dist/api/session.d.ts +1 -0
  11. package/dist/api/session.js +31 -0
  12. package/dist/api/users.d.ts +1 -0
  13. package/dist/api/users.js +244 -0
  14. package/dist/apps.d.ts +0 -5
  15. package/dist/apps.js +2 -9
  16. package/dist/auth.d.ts +14 -30
  17. package/dist/auth.js +12 -18
  18. package/dist/cli.js +289 -32
  19. package/dist/config.d.ts +21 -8
  20. package/dist/config.js +46 -17
  21. package/dist/database.d.ts +12 -12
  22. package/dist/database.js +83 -84
  23. package/dist/io.d.ts +19 -20
  24. package/dist/io.js +85 -56
  25. package/dist/linking.d.ts +10 -0
  26. package/dist/linking.js +76 -0
  27. package/dist/plugins.d.ts +28 -12
  28. package/dist/plugins.js +29 -25
  29. package/dist/requests.d.ts +14 -0
  30. package/dist/requests.js +67 -0
  31. package/dist/routes.d.ts +12 -13
  32. package/dist/routes.js +21 -22
  33. package/dist/serve.d.ts +7 -0
  34. package/dist/serve.js +11 -0
  35. package/dist/state.d.ts +4 -0
  36. package/dist/state.js +22 -0
  37. package/dist/sveltekit.d.ts +8 -0
  38. package/dist/sveltekit.js +94 -0
  39. package/package.json +17 -8
  40. package/{web/routes → routes}/account/+page.svelte +6 -5
  41. package/svelte.config.js +37 -0
  42. package/web/hooks.server.ts +8 -3
  43. package/web/lib/Dialog.svelte +0 -1
  44. package/web/lib/FormDialog.svelte +0 -1
  45. package/web/lib/Upload.svelte +58 -0
  46. package/web/lib/icons/Icon.svelte +2 -7
  47. package/web/lib/icons/index.ts +6 -3
  48. package/web/lib/icons/mime.json +2 -1
  49. package/web/template.html +18 -0
  50. package/web/tsconfig.json +2 -2
  51. package/web/api/metadata.ts +0 -35
  52. package/web/api/passkeys.ts +0 -56
  53. package/web/api/readme.md +0 -1
  54. package/web/api/register.ts +0 -83
  55. package/web/api/schemas.ts +0 -22
  56. package/web/api/session.ts +0 -33
  57. package/web/api/users.ts +0 -351
  58. package/web/api/utils.ts +0 -66
  59. package/web/app.html +0 -14
  60. package/web/auth.ts +0 -8
  61. package/web/index.server.ts +0 -1
  62. package/web/index.ts +0 -1
  63. package/web/lib/auth.ts +0 -12
  64. package/web/lib/index.ts +0 -5
  65. package/web/routes/+layout.svelte +0 -6
  66. package/web/routes/[...path]/+page.server.ts +0 -13
  67. package/web/routes/[appId]/[...page]/+page.server.ts +0 -14
  68. package/web/routes/api/[...path]/+server.ts +0 -49
  69. package/web/utils.ts +0 -26
  70. /package/{web/lib → assets}/icons/light.svg +0 -0
  71. /package/{web/lib → assets}/icons/regular.svg +0 -0
  72. /package/{web/lib → assets}/icons/solid.svg +0 -0
  73. /package/{web/lib → assets}/styles.css +0 -0
  74. /package/{web/routes → routes}/_axium/default/+page.svelte +0 -0
  75. /package/{web/routes → routes}/login/+page.svelte +0 -0
  76. /package/{web/routes → routes}/logout/+page.svelte +0 -0
  77. /package/{web/routes → routes}/register/+page.svelte +0 -0
package/dist/cli.js CHANGED
@@ -51,15 +51,35 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
51
51
  var e = new Error(message);
52
52
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
53
  });
54
+ import { formatDateRange } from '@axium/core/format';
54
55
  import { Argument, Option, program } from 'commander';
56
+ import { access } from 'node:fs/promises';
57
+ import { join } from 'node:path/posix';
58
+ import { createInterface } from 'node:readline/promises';
55
59
  import { styleText } from 'node:util';
56
60
  import { getByString, isJSON, setByString } from 'utilium';
61
+ import z from 'zod/v4';
57
62
  import $pkg from '../package.json' with { type: 'json' };
58
- import config from './config.js';
59
- import * as db from './database.js';
60
- import { _portActions, _portMethods, exit, handleError, output, restrictedPorts } from './io.js';
61
- import { loadDefaultPlugins, plugins, pluginText, resolvePlugin } from './plugins.js';
62
63
  import { apps } from './apps.js';
64
+ import config, { configFiles, saveConfigTo } from './config.js';
65
+ import * as db from './database.js';
66
+ import { _portActions, _portMethods, exit, handleError, output, restrictedPorts, setCommandTimeout, warn } from './io.js';
67
+ import { linkRoutes, listRouteLinks, unlinkRoutes } from './linking.js';
68
+ import { getSpecifier, plugins, pluginText, resolvePlugin } from './plugins.js';
69
+ import { serve } from './serve.js';
70
+ function readline() {
71
+ const rl = createInterface({
72
+ input: process.stdin,
73
+ output: process.stdout,
74
+ });
75
+ return Object.assign(rl, {
76
+ [Symbol.dispose]: rl.close.bind(rl),
77
+ });
78
+ }
79
+ function userText(user, bold = false) {
80
+ const text = `${user.name} <${user.email}> (${user.id})`;
81
+ return bold ? styleText('bold', text) : text;
82
+ }
63
83
  program
64
84
  .version($pkg.version)
65
85
  .name('axium')
@@ -72,15 +92,10 @@ program.on('option:debug', () => config.set({ debug: true }));
72
92
  program.on('option:config', () => void config.load(program.opts().config));
73
93
  program.hook('preAction', async function (_, action) {
74
94
  await config.loadDefaults();
75
- await loadDefaultPlugins();
76
95
  const opt = action.optsWithGlobals();
77
96
  opt.force && output.warn('--force: Protections disabled.');
78
97
  if (opt.debug === false)
79
98
  config.set({ debug: false });
80
- /* if (!config.auth.secret) {
81
- config.save({ auth: { secret: process.env.AUTH_SECRET || randomBytes(32).toString('base64') } }, true);
82
- output.debug('Auto-generated a new auth secret');
83
- } */
84
99
  });
85
100
  // Options shared by multiple (sub)commands
86
101
  const opts = {
@@ -92,13 +107,14 @@ const opts = {
92
107
  }),
93
108
  force: new Option('-f, --force', 'force the operation').default(false),
94
109
  global: new Option('-g, --global', 'apply the operation globally').default(false),
110
+ timeout: new Option('-t, --timeout <ms>', 'how long to wait for commands to complete.').default('1000').argParser(value => {
111
+ const timeout = parseInt(value);
112
+ if (!Number.isSafeInteger(timeout) || timeout < 0)
113
+ warn('Invalid timeout value, using default.');
114
+ setCommandTimeout(timeout);
115
+ }),
95
116
  };
96
- const axiumDB = program
97
- .command('db')
98
- .alias('database')
99
- .description('Manage the database')
100
- .option('-t, --timeout <ms>', 'how long to wait for commands to complete.', '1000')
101
- .addOption(opts.host);
117
+ const axiumDB = program.command('db').alias('database').description('Manage the database').addOption(opts.timeout).addOption(opts.host);
102
118
  axiumDB
103
119
  .command('init')
104
120
  .description('Initialize the database')
@@ -255,11 +271,7 @@ axiumConfig
255
271
  for (const path of config.files.keys())
256
272
  console.log(path);
257
273
  });
258
- const axiumPlugin = program
259
- .command('plugins')
260
- .description('Manage plugins')
261
- .addOption(opts.global)
262
- .option('--safe', 'do not perform actions that would execute code from plugins.');
274
+ const axiumPlugin = program.command('plugins').description('Manage plugins').addOption(opts.global);
263
275
  axiumPlugin
264
276
  .command('list')
265
277
  .alias('ls')
@@ -279,7 +291,7 @@ axiumPlugin
279
291
  }
280
292
  console.log(styleText('whiteBright', plugins.size + ' plugin(s) loaded:'));
281
293
  for (const plugin of plugins) {
282
- console.log(plugin.name, styleText('dim', `(${plugin.id})`), opt.versions ? plugin.version : '');
294
+ console.log(plugin.name, opt.versions ? plugin.version : '');
283
295
  }
284
296
  });
285
297
  axiumPlugin
@@ -292,6 +304,64 @@ axiumPlugin
292
304
  exit(`Can't find a plugin matching "${search}"`);
293
305
  console.log(pluginText(plugin));
294
306
  });
307
+ axiumPlugin
308
+ .command('remove')
309
+ .alias('rm')
310
+ .description('Remove a plugin')
311
+ .argument('<plugin>', 'the plugin to remove')
312
+ .action(async (search, opt) => {
313
+ const env_4 = { stack: [], error: void 0, hasError: false };
314
+ try {
315
+ const plugin = resolvePlugin(search);
316
+ if (!plugin)
317
+ exit(`Can't find a plugin matching "${search}"`);
318
+ const specifier = getSpecifier(plugin);
319
+ const _ = __addDisposableResource(env_4, db.connect(), true);
320
+ await plugin.hooks.remove?.(opt, db.database);
321
+ for (const [path, data] of configFiles) {
322
+ if (!data.plugins)
323
+ continue;
324
+ data.plugins = data.plugins.filter(p => p !== specifier);
325
+ saveConfigTo(path, data);
326
+ }
327
+ plugins.delete(plugin);
328
+ }
329
+ catch (e_4) {
330
+ env_4.error = e_4;
331
+ env_4.hasError = true;
332
+ }
333
+ finally {
334
+ const result_4 = __disposeResources(env_4);
335
+ if (result_4)
336
+ await result_4;
337
+ }
338
+ });
339
+ axiumPlugin
340
+ .command('init')
341
+ .alias('setup')
342
+ .alias('install')
343
+ .description('Initialize a plugin. This could include adding tables to the database or linking routes.')
344
+ .addOption(opts.timeout)
345
+ .argument('<plugin>', 'the plugin to initialize')
346
+ .action(async (search, opt) => {
347
+ const env_5 = { stack: [], error: void 0, hasError: false };
348
+ try {
349
+ const plugin = resolvePlugin(search);
350
+ if (!plugin)
351
+ exit(`Can't find a plugin matching "${search}"`);
352
+ const _ = __addDisposableResource(env_5, db.connect(), true);
353
+ await plugin.hooks.db_init?.({ force: false, ...opt, skip: true }, db.database);
354
+ }
355
+ catch (e_5) {
356
+ env_5.error = e_5;
357
+ env_5.hasError = true;
358
+ }
359
+ finally {
360
+ const result_5 = __disposeResources(env_5);
361
+ if (result_5)
362
+ await result_5;
363
+ }
364
+ });
295
365
  const axiumApps = program.command('apps').description('Manage Axium apps').addOption(opts.global);
296
366
  axiumApps
297
367
  .command('list')
@@ -313,28 +383,186 @@ axiumApps
313
383
  console.log(app.name, styleText('dim', `(${app.id})`));
314
384
  }
315
385
  });
386
+ const lookup = new Argument('<user>', 'the UUID or email of the user to operate on').argParser(async (lookup) => {
387
+ const value = await (lookup.includes('@') ? z.email() : z.uuid())
388
+ .parseAsync(lookup.toLowerCase())
389
+ .catch(() => exit('Invalid user ID or email.'));
390
+ db.connect();
391
+ const result = await db.database
392
+ .selectFrom('users')
393
+ .where(value.includes('@') ? 'email' : 'id', '=', value)
394
+ .selectAll()
395
+ .executeTakeFirst();
396
+ if (!result)
397
+ exit('No user with matching ID or email.');
398
+ return result;
399
+ });
400
+ /**
401
+ * Updates an array of strings by adding or removing items.
402
+ * Only returns whether the array was updated and diff text for what actually changed.
403
+ */
404
+ function diffUpdate(original, add, remove) {
405
+ const diffs = [];
406
+ // update the values
407
+ if (add) {
408
+ for (const role of add) {
409
+ if (original.includes(role))
410
+ continue;
411
+ original.push(role);
412
+ diffs.push(styleText('green', '+' + role));
413
+ }
414
+ }
415
+ if (remove)
416
+ original = original.filter(item => {
417
+ const allow = !remove.includes(item);
418
+ if (!allow)
419
+ diffs.push(styleText('red', '-' + item));
420
+ return allow;
421
+ });
422
+ return [!!diffs.length, original, diffs.join(', ')];
423
+ }
424
+ program
425
+ .command('user')
426
+ .description('Get or change information about a user')
427
+ .addArgument(lookup)
428
+ .option('-S, --sessions', 'show user sessions')
429
+ .option('-P, --passkeys', 'show user passkeys')
430
+ .option('--add-role <role...>', 'add roles to the user')
431
+ .option('--remove-role <role...>', 'remove roles from the user')
432
+ .option('--tag <tag...>', 'Add tags to the user')
433
+ .option('--untag <tag...>', 'Remove tags from the user')
434
+ .option('--delete', 'Delete the user')
435
+ .action(async (_user, opt) => {
436
+ const env_6 = { stack: [], error: void 0, hasError: false };
437
+ try {
438
+ let user = await _user;
439
+ const _ = __addDisposableResource(env_6, db.connect(), true);
440
+ const [updatedRoles, roles, rolesDiff] = diffUpdate(user.roles, opt.addRole, opt.removeRole);
441
+ const [updatedTags, tags, tagsDiff] = diffUpdate(user.tags, opt.tag, opt.untag);
442
+ if (updatedRoles || updatedTags) {
443
+ user = await db.database
444
+ .updateTable('users')
445
+ .set({ roles, tags })
446
+ .returningAll()
447
+ .executeTakeFirstOrThrow()
448
+ .then(u => {
449
+ if (updatedRoles && rolesDiff)
450
+ console.log(`> Updated roles: ${rolesDiff}`);
451
+ if (updatedTags && tagsDiff)
452
+ console.log(`> Updated tags: ${tagsDiff}`);
453
+ return u;
454
+ })
455
+ .catch(e => exit('Failed to update user roles: ' + e.message));
456
+ }
457
+ if (opt.delete) {
458
+ const env_7 = { stack: [], error: void 0, hasError: false };
459
+ try {
460
+ const rl = __addDisposableResource(env_7, readline(), false);
461
+ const confirmed = await rl
462
+ .question(`Are you sure you want to delete ${userText(user, true)}? (y/N) `)
463
+ .then(v => z.stringbool().parseAsync(v))
464
+ .catch(() => false);
465
+ if (!confirmed)
466
+ console.log(styleText('dim', '> Delete aborted.'));
467
+ else
468
+ await db.database
469
+ .deleteFrom('users')
470
+ .where('id', '=', user.id)
471
+ .executeTakeFirstOrThrow()
472
+ .then(() => console.log(styleText(['red', 'bold'], '> Deleted')))
473
+ .catch(e => exit('Failed to delete user: ' + e.message));
474
+ }
475
+ catch (e_6) {
476
+ env_7.error = e_6;
477
+ env_7.hasError = true;
478
+ }
479
+ finally {
480
+ __disposeResources(env_7);
481
+ }
482
+ }
483
+ console.log([
484
+ user.isAdmin && styleText('redBright', 'Administrator'),
485
+ 'UUID: ' + user.id,
486
+ 'Name: ' + user.name,
487
+ `Email: ${user.email}, ${user.emailVerified ? 'verified on ' + formatDateRange(user.emailVerified) : styleText(config.auth.email_verification ? 'yellow' : 'dim', 'not verified')}`,
488
+ 'Registered ' + formatDateRange(user.registeredAt),
489
+ `Roles: ${user.roles.length ? user.roles.join(', ') : styleText('dim', '(none)')}`,
490
+ `Tags: ${user.tags.length ? user.tags.join(', ') : styleText('dim', '(none)')}`,
491
+ ]
492
+ .filter(Boolean)
493
+ .join('\n'));
494
+ if (opt.sessions) {
495
+ const sessions = await db.database.selectFrom('sessions').where('userId', '=', user.id).selectAll().execute();
496
+ console.log(styleText('bold', 'Sessions:'));
497
+ if (!sessions.length)
498
+ console.log(styleText('dim', '(none)'));
499
+ else
500
+ for (const session of sessions) {
501
+ console.log(`\t${session.id}\tcreated ${formatDateRange(session.created).padEnd(40)}\texpires ${formatDateRange(session.expires).padEnd(40)}\t${session.elevated ? styleText('yellow', '(elevated)') : ''}`);
502
+ }
503
+ }
504
+ if (opt.passkeys) {
505
+ const passkeys = await db.database.selectFrom('passkeys').where('userId', '=', user.id).selectAll().execute();
506
+ console.log(styleText('bold', 'Passkeys:'));
507
+ for (const passkey of passkeys) {
508
+ console.log(`\t${passkey.id}: created ${formatDateRange(passkey.createdAt).padEnd(40)} used ${passkey.counter} times. ${passkey.deviceType}, ${passkey.backedUp ? '' : 'not '}backed up; transports are [${passkey.transports.join(', ')}], ${passkey.name ? 'named ' + JSON.stringify(passkey.name) : 'unnamed'}.`);
509
+ }
510
+ }
511
+ }
512
+ catch (e_7) {
513
+ env_6.error = e_7;
514
+ env_6.hasError = true;
515
+ }
516
+ finally {
517
+ const result_6 = __disposeResources(env_6);
518
+ if (result_6)
519
+ await result_6;
520
+ }
521
+ });
522
+ program
523
+ .command('toggle-admin')
524
+ .description('Toggle whether a user is an administrator')
525
+ .addArgument(lookup)
526
+ .action(async (_user) => {
527
+ const env_8 = { stack: [], error: void 0, hasError: false };
528
+ try {
529
+ const user = await _user;
530
+ const _ = __addDisposableResource(env_8, db.connect(), true);
531
+ const isAdmin = !user.isAdmin;
532
+ await db.database.updateTable('users').set({ isAdmin }).where('id', '=', user.id).executeTakeFirstOrThrow();
533
+ console.log(`${userText(user)} is ${isAdmin ? 'now' : 'no longer'} an administrator. (${styleText(['whiteBright', 'bold'], isAdmin.toString())})`);
534
+ }
535
+ catch (e_8) {
536
+ env_8.error = e_8;
537
+ env_8.hasError = true;
538
+ }
539
+ finally {
540
+ const result_7 = __disposeResources(env_8);
541
+ if (result_7)
542
+ await result_7;
543
+ }
544
+ });
316
545
  program
317
546
  .command('status')
318
547
  .alias('stats')
319
548
  .description('Get information about the server')
320
549
  .addOption(opts.host)
321
550
  .action(async () => {
322
- const env_4 = { stack: [], error: void 0, hasError: false };
551
+ const env_9 = { stack: [], error: void 0, hasError: false };
323
552
  try {
324
553
  console.log('Axium Server v' + program.version());
325
554
  console.log(styleText('whiteBright', 'Debug mode:'), config.debug ? styleText('yellow', 'enabled') : 'disabled');
326
555
  console.log(styleText('whiteBright', 'Loaded config files:'), config.files.keys().toArray().join(', '));
327
556
  process.stdout.write(styleText('whiteBright', 'Database: '));
328
- const _ = __addDisposableResource(env_4, db.connect(), true);
557
+ const _ = __addDisposableResource(env_9, db.connect(), true);
329
558
  try {
330
559
  console.log(await db.statusText());
331
560
  }
332
561
  catch {
333
562
  output.error('Unavailable');
334
563
  }
335
- console.log(styleText('whiteBright', 'Credentials authentication:'), config.auth.credentials ? styleText('yellow', 'enabled') : 'disabled');
336
564
  console.log(styleText('whiteBright', 'Loaded plugins:'), Array.from(plugins)
337
- .map(plugin => plugin.id)
565
+ .map(plugin => plugin.name)
338
566
  .join(', ') || styleText('dim', '(none)'));
339
567
  for (const plugin of plugins) {
340
568
  if (!plugin.statusText)
@@ -343,14 +571,14 @@ program
343
571
  console.log(await plugin.statusText());
344
572
  }
345
573
  }
346
- catch (e_4) {
347
- env_4.error = e_4;
348
- env_4.hasError = true;
574
+ catch (e_9) {
575
+ env_9.error = e_9;
576
+ env_9.hasError = true;
349
577
  }
350
578
  finally {
351
- const result_4 = __disposeResources(env_4);
352
- if (result_4)
353
- await result_4;
579
+ const result_8 = __disposeResources(env_9);
580
+ if (result_8)
581
+ await result_8;
354
582
  }
355
583
  });
356
584
  program
@@ -368,8 +596,37 @@ program
368
596
  .addOption(opts.force)
369
597
  .addOption(opts.host)
370
598
  .action(async (opt) => {
371
- /* config.save({ auth: { secret: randomBytes(32).toString('base64') } }, true); */
372
599
  await db.init({ ...opt, skip: opt.dbSkip }).catch(handleError);
373
600
  await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(handleError);
374
601
  });
602
+ program
603
+ .command('serve')
604
+ .description('Start the Axium server')
605
+ .option('-p, --port <port>', 'the port to listen on')
606
+ .option('--ssl <prefix>', 'the prefix for the cert.pem and key.pem SSL files')
607
+ .action(async (opt) => {
608
+ const server = await serve({
609
+ secure: opt.ssl ? true : config.web.secure,
610
+ ssl_cert: opt.ssl ? join(opt.ssl, 'cert.pem') : config.web.ssl_cert,
611
+ ssl_key: opt.ssl ? join(opt.ssl, 'key.pem') : config.web.ssl_key,
612
+ });
613
+ const port = !Number.isNaN(Number.parseInt(opt.port ?? '')) ? Number.parseInt(opt.port) : config.web.port;
614
+ server.listen(port, () => {
615
+ console.log('Server is listening on port ' + port);
616
+ });
617
+ });
618
+ program.command('link').description('Link svelte page routes').action(linkRoutes);
619
+ program.command('unlink').description('Unlink svelte page routes').action(unlinkRoutes);
620
+ program
621
+ .command('list-links')
622
+ .description('List linked routes')
623
+ .action(async () => {
624
+ for (const link of listRouteLinks()) {
625
+ const idText = link.id.startsWith('#') ? `(${link.id.slice(1)})` : link.id;
626
+ const toColor = await access(link.to)
627
+ .then(() => 'white')
628
+ .catch(() => 'redBright');
629
+ console.log(`${idText}:\t ${styleText('cyanBright', link.from)}\t->\t${styleText(toColor, link.to)}`);
630
+ }
631
+ });
375
632
  program.parse();
package/dist/config.d.ts CHANGED
@@ -10,8 +10,6 @@ export interface Config extends Record<string, unknown> {
10
10
  disabled: string[];
11
11
  };
12
12
  auth: {
13
- credentials: boolean;
14
- debug: boolean;
15
13
  origin: string;
16
14
  /** In minutes */
17
15
  passkey_probation: number;
@@ -36,20 +34,28 @@ export interface Config extends Record<string, unknown> {
36
34
  console: boolean;
37
35
  };
38
36
  web: {
37
+ assets: string;
38
+ disable_cache: boolean;
39
+ port: number;
39
40
  prefix: string;
41
+ routes: string;
42
+ secure: boolean;
43
+ ssl_key: string;
44
+ ssl_cert: string;
45
+ template: string;
40
46
  };
41
47
  }
42
- export declare const configFiles: Map<string, PartialRecursive<Config>>;
48
+ export declare const configFiles: Map<string, File>;
43
49
  export declare function plainConfig(): Omit<Config, keyof typeof configShortcuts>;
44
50
  declare const configShortcuts: {
45
- findPath: typeof findConfigPath;
51
+ findPath: typeof findConfigPaths;
46
52
  load: typeof loadConfig;
47
53
  loadDefaults: typeof loadDefaultConfigs;
48
54
  plain: typeof plainConfig;
49
55
  save: typeof saveConfig;
50
56
  saveTo: typeof saveConfigTo;
51
57
  set: typeof setConfig;
52
- files: Map<string, PartialRecursive<Config>>;
58
+ files: Map<string, File>;
53
59
  };
54
60
  export declare const config: Config & typeof configShortcuts;
55
61
  export default config;
@@ -62,8 +68,6 @@ export declare const File: z.ZodObject<{
62
68
  disabled: z.ZodOptional<z.ZodArray<z.ZodString>>;
63
69
  }, z.core.$strip>>;
64
70
  auth: z.ZodOptional<z.ZodObject<{
65
- credentials: z.ZodOptional<z.ZodBoolean>;
66
- debug: z.ZodOptional<z.ZodBoolean>;
67
71
  origin: z.ZodOptional<z.ZodString>;
68
72
  passkey_probation: z.ZodOptional<z.ZodNumber>;
69
73
  rp_id: z.ZodOptional<z.ZodString>;
@@ -91,13 +95,22 @@ export declare const File: z.ZodObject<{
91
95
  console: z.ZodOptional<z.ZodBoolean>;
92
96
  }, z.core.$strip>>;
93
97
  web: z.ZodOptional<z.ZodObject<{
98
+ assets: z.ZodOptional<z.ZodString>;
99
+ disable_cache: z.ZodOptional<z.ZodBoolean>;
100
+ port: z.ZodOptional<z.ZodNumber>;
94
101
  prefix: z.ZodOptional<z.ZodString>;
102
+ routes: z.ZodOptional<z.ZodString>;
103
+ secure: z.ZodOptional<z.ZodBoolean>;
104
+ ssl_key: z.ZodOptional<z.ZodString>;
105
+ ssl_cert: z.ZodOptional<z.ZodString>;
106
+ template: z.ZodOptional<z.ZodString>;
95
107
  }, z.core.$strip>>;
96
108
  include: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
97
109
  plugins: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
98
110
  }, z.core.$loose>;
99
111
  export interface File extends PartialRecursive<Config>, z.infer<typeof File> {
100
112
  }
113
+ export declare function addConfigDefaults(other: PartialRecursive<Config>, _target?: Record<string, any>): void;
101
114
  /**
102
115
  * Update the current config
103
116
  */
@@ -132,4 +145,4 @@ export declare function saveConfigTo(path: string, changed: PartialRecursive<Con
132
145
  /**
133
146
  * Find the path to the config file
134
147
  */
135
- export declare function findConfigPath(global: boolean): string;
148
+ export declare function findConfigPaths(): string[];
package/dist/config.js CHANGED
@@ -3,14 +3,15 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join, resolve } from 'node:path/posix';
4
4
  import { deepAssign, omit } from 'utilium';
5
5
  import * as z from 'zod/v4';
6
- import { findDir, logger, output } from './io.js';
6
+ import { _setDebugOutput, dirs, logger, output } from './io.js';
7
7
  import { loadPlugin } from './plugins.js';
8
- export const configFiles = new Map();
8
+ import { _unique } from './state.js';
9
+ export const configFiles = _unique('configFiles', new Map());
9
10
  export function plainConfig() {
10
11
  return omit(config, Object.keys(configShortcuts));
11
12
  }
12
13
  const configShortcuts = {
13
- findPath: findConfigPath,
14
+ findPath: findConfigPaths,
14
15
  load: loadConfig,
15
16
  loadDefaults: loadDefaultConfigs,
16
17
  plain: plainConfig,
@@ -19,18 +20,16 @@ const configShortcuts = {
19
20
  set: setConfig,
20
21
  files: configFiles,
21
22
  };
22
- export const config = {
23
+ export const config = _unique('config', {
23
24
  ...configShortcuts,
24
25
  api: {
25
26
  disable_metadata: false,
26
- cookie_auth: false,
27
+ cookie_auth: true,
27
28
  },
28
29
  apps: {
29
30
  disabled: [],
30
31
  },
31
32
  auth: {
32
- credentials: false,
33
- debug: false,
34
33
  origin: 'https://test.localhost',
35
34
  passkey_probation: 60,
36
35
  rp_id: 'test.localhost',
@@ -52,9 +51,17 @@ export const config = {
52
51
  level: 'info',
53
52
  },
54
53
  web: {
54
+ assets: '',
55
+ disable_cache: false,
56
+ port: 443,
55
57
  prefix: '',
58
+ routes: 'routes',
59
+ secure: true,
60
+ ssl_key: resolve(dirs[0], 'ssl_key.pem'),
61
+ ssl_cert: resolve(dirs[0], 'ssl_cert.pem'),
62
+ template: join(import.meta.dirname, '../web/template.html'),
56
63
  },
57
- };
64
+ });
58
65
  export default config;
59
66
  // config from file
60
67
  export const File = z
@@ -72,8 +79,6 @@ export const File = z
72
79
  .partial(),
73
80
  auth: z
74
81
  .object({
75
- credentials: z.boolean(),
76
- debug: z.boolean(),
77
82
  origin: z.string(),
78
83
  /** In minutes */
79
84
  passkey_probation: z.number(),
@@ -104,13 +109,32 @@ export const File = z
104
109
  .partial(),
105
110
  web: z
106
111
  .object({
112
+ assets: z.string(),
113
+ disable_cache: z.boolean(),
114
+ port: z.number().min(1).max(65535),
107
115
  prefix: z.string(),
116
+ routes: z.string(),
117
+ secure: z.boolean(),
118
+ ssl_key: z.string(),
119
+ ssl_cert: z.string(),
120
+ template: z.string(),
108
121
  })
109
122
  .partial(),
110
123
  include: z.array(z.string()).optional(),
111
124
  plugins: z.array(z.string()).optional(),
112
125
  })
113
126
  .partial();
127
+ export function addConfigDefaults(other, _target = config) {
128
+ for (const [key, value] of Object.entries(other)) {
129
+ if (!(key in _target) || _target[key] === null || _target[key] === undefined || Number.isNaN(_target[key])) {
130
+ _target[key] = value;
131
+ continue;
132
+ }
133
+ if (typeof value == 'object' && value != null && typeof _target[key] == 'object') {
134
+ addConfigDefaults(value, _target[key]);
135
+ }
136
+ }
137
+ }
114
138
  /**
115
139
  * Update the current config
116
140
  */
@@ -119,11 +143,14 @@ export function setConfig(other) {
119
143
  logger.detach(output);
120
144
  if (config.log.console)
121
145
  logger.attach(output, { output: config.log.level });
146
+ _setDebugOutput(config.debug);
122
147
  }
123
148
  /**
124
149
  * Load the config from the provided path
125
150
  */
126
151
  export async function loadConfig(path, options = {}) {
152
+ if (configFiles.has(path))
153
+ return;
127
154
  let json;
128
155
  try {
129
156
  json = JSON.parse(readFileSync(path, 'utf8'));
@@ -131,19 +158,20 @@ export async function loadConfig(path, options = {}) {
131
158
  catch (e) {
132
159
  if (!options.optional)
133
160
  throw e;
134
- config.debug && output.debug(`Skipping config at ${path} (${e.message})`);
161
+ output.debug(`Skipping config at ${path} (${e.message})`);
135
162
  return;
136
163
  }
137
164
  const file = options.strict ? File.parse(json) : json;
138
165
  configFiles.set(path, file);
139
166
  setConfig(file);
167
+ output.debug('Loaded config: ' + path);
140
168
  for (const include of file.include ?? [])
141
169
  await loadConfig(join(dirname(path), include), { optional: true });
142
170
  for (const plugin of file.plugins ?? [])
143
171
  await loadPlugin(plugin.startsWith('.') ? resolve(dirname(path), plugin) : plugin);
144
172
  }
145
173
  export async function loadDefaultConfigs() {
146
- for (const path of [findConfigPath(true), findConfigPath(false)]) {
174
+ for (const path of findConfigPaths()) {
147
175
  if (!existsSync(path))
148
176
  writeFileSync(path, '{}');
149
177
  await loadConfig(path, { optional: true });
@@ -153,7 +181,7 @@ export async function loadDefaultConfigs() {
153
181
  * Update the current config and write the updated config to the appropriate file
154
182
  */
155
183
  export function saveConfig(changed, global = false) {
156
- saveConfigTo(process.env.AXIUM_CONFIG ?? findConfigPath(global), changed);
184
+ saveConfigTo(findConfigPaths().at(global ? 0 : -1), changed);
157
185
  }
158
186
  /**
159
187
  * Update the current config and write the updated config to the provided path
@@ -162,16 +190,17 @@ export function saveConfigTo(path, changed) {
162
190
  setConfig(changed);
163
191
  const config = configFiles.get(path) ?? {};
164
192
  Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
165
- config.debug && output.debug(`Wrote config to ${path}`);
193
+ output.debug(`Wrote config to ${path}`);
166
194
  writeFileSync(path, JSON.stringify(config));
167
195
  }
168
196
  /**
169
197
  * Find the path to the config file
170
198
  */
171
- export function findConfigPath(global) {
199
+ export function findConfigPaths() {
200
+ const paths = dirs.map(dir => join(dir, 'config.json'));
172
201
  if (process.env.AXIUM_CONFIG)
173
- return process.env.AXIUM_CONFIG;
174
- return join(findDir(global), 'config.json');
202
+ paths.push(process.env.AXIUM_CONFIG);
203
+ return paths;
175
204
  }
176
205
  if (process.env.AXIUM_CONFIG)
177
206
  await loadConfig(process.env.AXIUM_CONFIG);