@axium/server 0.26.3 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js ADDED
@@ -0,0 +1,833 @@
1
+ #!/usr/bin/env node
2
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
3
+ if (value !== null && value !== void 0) {
4
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
5
+ var dispose, inner;
6
+ if (async) {
7
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
8
+ dispose = value[Symbol.asyncDispose];
9
+ }
10
+ if (dispose === void 0) {
11
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
12
+ dispose = value[Symbol.dispose];
13
+ if (async) inner = dispose;
14
+ }
15
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
16
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
17
+ env.stack.push({ value: value, dispose: dispose, async: async });
18
+ }
19
+ else if (async) {
20
+ env.stack.push({ async: true });
21
+ }
22
+ return value;
23
+ };
24
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
25
+ return function (env) {
26
+ function fail(e) {
27
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
28
+ env.hasError = true;
29
+ }
30
+ var r, s = 0;
31
+ function next() {
32
+ while (r = env.stack.pop()) {
33
+ try {
34
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
35
+ if (r.dispose) {
36
+ var result = r.dispose.call(r.value);
37
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
38
+ }
39
+ else s |= 1;
40
+ }
41
+ catch (e) {
42
+ fail(e);
43
+ }
44
+ }
45
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
46
+ if (env.hasError) throw env.error;
47
+ }
48
+ return next();
49
+ };
50
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
51
+ var e = new Error(message);
52
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
+ });
54
+ import { apps } from '@axium/core';
55
+ import { AuditFilter, severityNames } from '@axium/core/audit';
56
+ import { formatDateRange } from '@axium/core/format';
57
+ import { io, outputDaemonStatus, pluginText } from '@axium/core/node';
58
+ import { _findPlugin, plugins } from '@axium/core/plugins';
59
+ import { Argument, Option, program } from 'commander';
60
+ import { access } from 'node:fs/promises';
61
+ import { join, resolve } from 'node:path/posix';
62
+ import { createInterface } from 'node:readline/promises';
63
+ import { styleText, parseArgs } from 'node:util';
64
+ import { getByString, isJSON, setByString } from 'utilium';
65
+ import * as z from 'zod';
66
+ import $pkg from '../package.json' with { type: 'json' };
67
+ import { audit, getEvents, styleSeverity } from './audit.js';
68
+ import config, { configFiles, FileSchema, saveConfigTo } from './config.js';
69
+ import * as db from './database.js';
70
+ import { _portActions, _portMethods, restrictedPorts } from './io.js';
71
+ import { linkRoutes, listRouteLinks, unlinkRoutes } from './linking.js';
72
+ import { serve } from './serve.js';
73
+ import { diffUpdate, lookupUser, userText } from './cli.js';
74
+ async function rlConfirm(question = 'Is this ok') {
75
+ const { data, error } = z
76
+ .stringbool()
77
+ .default(false)
78
+ .safeParse(await rl.question(question + ' [y/N]: ').catch(() => io.exit('Aborted.')));
79
+ if (error || !data)
80
+ io.exit('Aborted.');
81
+ }
82
+ async function dbInitTables() {
83
+ const schema = db.getFullSchema({ exclude: Object.keys(db.getUpgradeInfo().current) });
84
+ const delta = db.computeDelta({ tables: {}, indexes: [] }, schema);
85
+ if (db.deltaIsEmpty(delta))
86
+ return;
87
+ for (const text of db.displayDelta(delta))
88
+ console.log(text);
89
+ await rlConfirm();
90
+ await db.applyDelta(delta);
91
+ }
92
+ function configReplacer(opt) {
93
+ return (key, value) => {
94
+ return opt.redact && ['password', 'secret'].includes(key) ? '[redacted]' : value;
95
+ };
96
+ }
97
+ var rl, safe, configFromCLI, noAutoDB, opts, axiumDB, axiumConfig, axiumPlugin, axiumApps, argUserLookup;
98
+ const env_1 = { stack: [], error: void 0, hasError: false };
99
+ try {
100
+ rl = __addDisposableResource(env_1, createInterface({
101
+ input: process.stdin,
102
+ output: process.stdout,
103
+ }), false);
104
+ // Need these before Command is set up (e.g. for CLI integrations)
105
+ ({ safe, config: configFromCLI } = parseArgs({
106
+ options: {
107
+ safe: { type: 'boolean', default: z.stringbool().default(false).parse(process.env.SAFE?.toLowerCase()) },
108
+ config: { type: 'string', short: 'c' },
109
+ },
110
+ allowPositionals: true,
111
+ strict: false,
112
+ }).values);
113
+ await config.loadDefaults(safe);
114
+ if (configFromCLI)
115
+ await config.load(configFromCLI, { safe });
116
+ program
117
+ .version($pkg.version)
118
+ .name('axium')
119
+ .description('Axium server CLI')
120
+ .configureHelp({ showGlobalOptions: true })
121
+ .option('--safe', 'do not execute code from plugins')
122
+ .option('--debug', 'override debug mode')
123
+ .option('--no-debug', 'override debug mode')
124
+ .option('-c, --config <path>', 'path to the config file');
125
+ program.on('option:debug', () => config.set({ debug: true }));
126
+ noAutoDB = ['init', 'serve', 'check'];
127
+ program.hook('preAction', (_, action) => {
128
+ const opt = action.optsWithGlobals();
129
+ opt.force && io.warn('--force: Protections disabled.');
130
+ if (typeof opt.debug == 'boolean') {
131
+ config.set({ debug: opt.debug });
132
+ io._setDebugOutput(opt.debug);
133
+ }
134
+ try {
135
+ db.connect();
136
+ }
137
+ catch (e) {
138
+ if (!noAutoDB.includes(action.name()))
139
+ throw e;
140
+ }
141
+ });
142
+ program.hook('postAction', async (_, action) => {
143
+ if (!noAutoDB.includes(action.name()))
144
+ await db.database.destroy();
145
+ });
146
+ // Options shared by multiple (sub)commands
147
+ opts = {
148
+ // database specific
149
+ host: new Option('-H, --host <host>', 'the host of the database.').argParser(value => {
150
+ const [hostname, port] = value?.split(':') ?? [];
151
+ config.db.host = hostname || config.db.host;
152
+ config.db.port = port && Number.isSafeInteger(parseInt(port)) ? parseInt(port) : config.db.port;
153
+ }),
154
+ check: new Option('--check', 'check the database schema after initialization').default(false),
155
+ force: new Option('-f, --force', 'force the operation').default(false),
156
+ global: new Option('-g, --global', 'apply the operation globally').default(false),
157
+ timeout: new Option('-t, --timeout <ms>', 'how long to wait for commands to complete.').default('1000').argParser(value => {
158
+ const timeout = parseInt(value);
159
+ if (!Number.isSafeInteger(timeout) || timeout < 0)
160
+ io.warn('Invalid timeout value, using default.');
161
+ io.setCommandTimeout(timeout);
162
+ }),
163
+ packagesDir: new Option('-p, --packages-dir <dir>', 'the directory to look for packages in'),
164
+ };
165
+ axiumDB = program.command('db').alias('database').description('Manage the database').addOption(opts.timeout).addOption(opts.host);
166
+ axiumDB
167
+ .command('init')
168
+ .description('Initialize the database')
169
+ .addOption(opts.force)
170
+ .option('-s, --skip', 'If the user, database, or schema already exists, skip trying to create it.')
171
+ .addOption(opts.check)
172
+ .action(async (_localOpts, _) => {
173
+ const opt = _.optsWithGlobals();
174
+ await db.init(opt).catch(io.handleError);
175
+ await dbInitTables().catch(io.handleError);
176
+ });
177
+ axiumDB
178
+ .command('status')
179
+ .alias('stats')
180
+ .description('Check the status of the database')
181
+ .action(async () => {
182
+ try {
183
+ console.log(await db.statText());
184
+ }
185
+ catch {
186
+ io.error('Unavailable');
187
+ process.exitCode = 1;
188
+ }
189
+ });
190
+ axiumDB
191
+ .command('drop')
192
+ .description('Drop the Axium database and user')
193
+ .addOption(opts.force)
194
+ .action(async (opt) => {
195
+ const stats = await db.count('users', 'passkeys', 'sessions').catch(io.exit);
196
+ if (!opt.force)
197
+ for (const key of ['users', 'passkeys', 'sessions']) {
198
+ if (stats[key] == 0)
199
+ continue;
200
+ io.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
201
+ process.exit(2);
202
+ }
203
+ await db._sql('DROP DATABASE axium', 'Dropping database').catch(io.handleError);
204
+ await db._sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges').catch(io.handleError);
205
+ await db._sql('DROP USER axium', 'Dropping user').catch(io.handleError);
206
+ await db
207
+ .getHBA(opt)
208
+ .then(([content, writeBack]) => {
209
+ io.start('Checking for Axium HBA configuration');
210
+ if (!content.includes(db._pgHba))
211
+ throw 'missing.';
212
+ io.done();
213
+ io.start('Removing Axium HBA configuration');
214
+ const newContent = content.replace(db._pgHba, '');
215
+ io.done();
216
+ writeBack(newContent);
217
+ })
218
+ .catch(io.warn);
219
+ });
220
+ axiumDB
221
+ .command('wipe')
222
+ .description('Wipe the database')
223
+ .addOption(opts.force)
224
+ .action(async (opt) => {
225
+ const tables = new Map();
226
+ for (const [plugin, schema] of db.getSchemaFiles()) {
227
+ for (const table of schema.wipe) {
228
+ const maybePlugin = tables.get(table);
229
+ tables.set(table, maybePlugin ? `${maybePlugin}, ${plugin}` : plugin);
230
+ }
231
+ }
232
+ if (!opt.force) {
233
+ const stats = await db.count(...tables.keys()).catch(io.exit);
234
+ const nonEmpty = Object.entries(stats)
235
+ .filter(([, v]) => v)
236
+ .map(([k]) => k);
237
+ if (nonEmpty.length) {
238
+ io.exit(`Some tables are not empty, use --force if you really want to wipe them: ${nonEmpty.join(', ')}`, 2);
239
+ }
240
+ }
241
+ const maxTableName = Math.max(5, ...Array.from(tables.keys()).map(t => t.length));
242
+ console.log('Table' + ' '.repeat(maxTableName - 5), '|', 'Plugin(s)');
243
+ console.log('-'.repeat(maxTableName), '|', '-'.repeat(20));
244
+ for (const [table, plugins] of [...tables].sort((a, b) => a[0].localeCompare(b[0]))) {
245
+ console.log(table + ' '.repeat(maxTableName - table.length), '|', plugins);
246
+ }
247
+ await rlConfirm('Are you sure you want to wipe these tables and any dependents');
248
+ await db.database.deleteFrom(Array.from(tables.keys())).execute().catch(io.handleError);
249
+ });
250
+ axiumDB
251
+ .command('check')
252
+ .description('Check the structure of the database')
253
+ .option('-s, --strict', 'Throw errors instead of emitting warnings for most column problems')
254
+ .action(async (opt) => {
255
+ const env_2 = { stack: [], error: void 0, hasError: false };
256
+ try {
257
+ await io.run('Checking for sudo', 'which sudo').catch(io.handleError);
258
+ await io.run('Checking for psql', 'which psql').catch(io.handleError);
259
+ const throwUnlessRows = (text) => {
260
+ if (text.includes('(0 rows)'))
261
+ throw 'missing.';
262
+ return text;
263
+ };
264
+ await db
265
+ ._sql(`SELECT 1 FROM pg_database WHERE datname = 'axium'`, 'Checking for database')
266
+ .then(throwUnlessRows)
267
+ .catch(io.handleError);
268
+ await db._sql(`SELECT 1 FROM pg_roles WHERE rolname = 'axium'`, 'Checking for user').then(throwUnlessRows).catch(io.handleError);
269
+ io.start('Connecting to database');
270
+ const _ = __addDisposableResource(env_2, db.connect(), true);
271
+ io.done();
272
+ io.start('Getting schema metadata');
273
+ const schemas = await db.database.introspection.getSchemas().catch(io.handleError);
274
+ io.done();
275
+ io.start('Checking for acl schema');
276
+ if (!schemas.find(s => s.name == 'acl'))
277
+ io.exit('missing.');
278
+ io.done();
279
+ io.start('Getting table metadata');
280
+ const tablePromises = await Promise.all([
281
+ db.database.introspection.getTables(),
282
+ db.database.withSchema('acl').introspection.getTables(),
283
+ ]).catch(io.handleError);
284
+ opt._metadata = tablePromises.flat();
285
+ const tables = Object.fromEntries(opt._metadata.map(t => [t.schema == 'public' ? t.name : `${t.schema}.${t.name}`, t]));
286
+ io.done();
287
+ const schema = db.getFullSchema();
288
+ for (const [name, table] of Object.entries(schema.tables)) {
289
+ await db.checkTableTypes(name, table, opt);
290
+ delete tables[name];
291
+ }
292
+ io.start('Checking for extra tables');
293
+ const unchecked = Object.keys(tables).join(', ');
294
+ if (!unchecked.length)
295
+ io.done();
296
+ else if (opt.strict)
297
+ io.exit(unchecked);
298
+ else
299
+ io.warn(unchecked);
300
+ }
301
+ catch (e_2) {
302
+ env_2.error = e_2;
303
+ env_2.hasError = true;
304
+ }
305
+ finally {
306
+ const result_1 = __disposeResources(env_2);
307
+ if (result_1)
308
+ await result_1;
309
+ }
310
+ });
311
+ axiumDB
312
+ .command('clean')
313
+ .description('Remove expired rows')
314
+ .addOption(opts.force)
315
+ .action(async (opt) => {
316
+ await db.clean(opt).catch(io.exit);
317
+ });
318
+ axiumDB
319
+ .command('rotate-password')
320
+ .description('Generate a new password for the database user and update the config')
321
+ .action(db.rotatePassword);
322
+ axiumDB
323
+ .command('schema')
324
+ .description('Get the JSON schema for the database configuration file')
325
+ .option('-j, --json', 'values are JSON encoded')
326
+ .action((opt) => {
327
+ try {
328
+ const schema = z.toJSONSchema(db.SchemaFile, { io: 'input' });
329
+ console.log(opt.json ? JSON.stringify(schema, null, 4) : schema);
330
+ }
331
+ catch (e) {
332
+ io.handleError(e instanceof z.core.$ZodError ? z.prettifyError(e) : e);
333
+ }
334
+ });
335
+ axiumDB
336
+ .command('upgrade')
337
+ .alias('update')
338
+ .alias('up')
339
+ .description('Upgrade the database to the latest version')
340
+ .option('--abort', 'Rollback changes instead of committing them')
341
+ .action(async (opt) => {
342
+ const deltas = [];
343
+ const info = db.getUpgradeInfo();
344
+ let empty = true;
345
+ const from = {}, to = {};
346
+ for (const [name, schema] of db.getSchemaFiles()) {
347
+ if (!(name in info.current))
348
+ io.exit('Plugin is not initialized: ' + name);
349
+ const currentVersion = info.current[name];
350
+ const target = schema.latest ?? schema.versions.length - 1;
351
+ if (currentVersion >= target)
352
+ continue;
353
+ from[name] = currentVersion;
354
+ to[name] = target;
355
+ info.current[name] = target;
356
+ let versions = schema.versions.slice(currentVersion + 1);
357
+ const v0 = schema.versions[0];
358
+ if (v0.delta)
359
+ throw 'Initial version can not be a delta';
360
+ for (const [i, v] of versions.toReversed().entries()) {
361
+ if (v.delta || v == v0)
362
+ continue;
363
+ versions = [db.computeDelta(v0, v), ...versions.slice(-i)];
364
+ break;
365
+ }
366
+ const delta = db.collapseDeltas(versions);
367
+ deltas.push(delta);
368
+ console.log('Upgrading', name, styleText('dim', currentVersion.toString() + '->') + styleText('blueBright', target.toString()) + ':');
369
+ if (!db.deltaIsEmpty(delta))
370
+ empty = false;
371
+ for (const text of db.displayDelta(delta))
372
+ console.log(text);
373
+ }
374
+ if (empty) {
375
+ console.log('Already up to date.');
376
+ return;
377
+ }
378
+ if (opt.abort) {
379
+ io.warn('--abort: Changes will be rolled back instead of being committed.');
380
+ }
381
+ await rlConfirm();
382
+ io.start('Computing delta');
383
+ let delta;
384
+ try {
385
+ delta = db.collapseDeltas(deltas);
386
+ io.done();
387
+ }
388
+ catch (e) {
389
+ io.exit(e);
390
+ }
391
+ io.start('Validating delta');
392
+ try {
393
+ db.validateDelta(delta);
394
+ io.done();
395
+ }
396
+ catch (e) {
397
+ io.exit(e);
398
+ }
399
+ console.log('Applying delta.');
400
+ await db.applyDelta(delta, opt.abort).catch(io.handleError);
401
+ info.upgrades.push({ timestamp: new Date(), from, to });
402
+ db.setUpgradeInfo(info);
403
+ });
404
+ axiumDB
405
+ .command('upgrade-history')
406
+ .alias('update-history')
407
+ .description('Show the history of database upgrades')
408
+ .action(() => {
409
+ const info = db.getUpgradeInfo();
410
+ if (!info.upgrades.length) {
411
+ console.log('No upgrade history.');
412
+ return;
413
+ }
414
+ for (const up of info.upgrades) {
415
+ console.log(styleText(['whiteBright', 'underline'], up.timestamp.toString()) + ':');
416
+ for (const [name, from] of Object.entries(up.from)) {
417
+ console.log(name, styleText('dim', from.toString() + '->') + styleText('blueBright', up.to[name].toString()));
418
+ }
419
+ }
420
+ });
421
+ axiumDB
422
+ .command('versions')
423
+ .description('Show information about database versions')
424
+ .action(() => {
425
+ const { current: currentVersions } = db.getUpgradeInfo();
426
+ const lengths = { name: 4, current: 7, latest: 6, available: 9 };
427
+ const entries = [
428
+ { name: 'Name', current: 'Current', latest: 'Latest', available: 'Available' },
429
+ ];
430
+ for (const [name, file] of db.getSchemaFiles()) {
431
+ const available = (file.versions.length - 1).toString();
432
+ const latest = (file.latest ?? available).toString();
433
+ const current = currentVersions[name]?.toString();
434
+ entries.push({ name, latest, available, current });
435
+ lengths.name = Math.max(lengths.name || 0, name.length);
436
+ lengths.current = Math.max(lengths.current || 0, current.length);
437
+ lengths.latest = Math.max(lengths.latest || 0, latest.length);
438
+ lengths.available = Math.max(lengths.available || 0, available.length);
439
+ }
440
+ for (const [i, entry] of entries.entries()) {
441
+ console.log(...['name', 'current', 'latest', 'available'].map(key => styleText(i === 0 ? ['whiteBright', 'underline'] : entry[key] === undefined ? 'none' : [], entry[key].padStart(lengths[key]))));
442
+ }
443
+ });
444
+ axiumConfig = program
445
+ .command('config')
446
+ .description('Manage the configuration')
447
+ .addOption(opts.global)
448
+ .option('-j, --json', 'values are JSON encoded')
449
+ .option('-r, --redact', 'Do not output sensitive values');
450
+ axiumConfig
451
+ .command('dump')
452
+ .description('Output the entire current configuration')
453
+ .action(() => {
454
+ const opt = axiumConfig.optsWithGlobals();
455
+ const value = config.plain();
456
+ console.log(opt.json ? JSON.stringify(value, configReplacer(opt), 4) : value);
457
+ });
458
+ axiumConfig
459
+ .command('get')
460
+ .description('Get a config value')
461
+ .argument('<key>', 'the key to get')
462
+ .action((key) => {
463
+ const opt = axiumConfig.optsWithGlobals();
464
+ const value = getByString(config.plain(), key);
465
+ console.log(opt.json ? JSON.stringify(value, configReplacer(opt), 4) : value);
466
+ });
467
+ axiumConfig
468
+ .command('set')
469
+ .description('Set a config value. Note setting objects is not supported.')
470
+ .argument('<key>', 'the key to set')
471
+ .argument('<value>', 'the value')
472
+ .action((key, value) => {
473
+ const opt = axiumConfig.optsWithGlobals();
474
+ if (opt.json && !isJSON(value))
475
+ io.exit('Invalid JSON');
476
+ const obj = {};
477
+ setByString(obj, key, opt.json ? JSON.parse(value) : value);
478
+ config.save(obj, opt.global);
479
+ });
480
+ axiumConfig
481
+ .command('list')
482
+ .alias('ls')
483
+ .alias('files')
484
+ .description('List loaded config files')
485
+ .action(() => {
486
+ for (const path of config.files.keys())
487
+ console.log(path);
488
+ });
489
+ axiumConfig
490
+ .command('schema')
491
+ .description('Get the JSON schema for the configuration file')
492
+ .action(() => {
493
+ const opt = axiumConfig.optsWithGlobals();
494
+ try {
495
+ const schema = z.toJSONSchema(FileSchema, { io: 'input' });
496
+ console.log(opt.json ? JSON.stringify(schema, configReplacer(opt), 4) : schema);
497
+ }
498
+ catch (e) {
499
+ io.handleError(e instanceof z.core.$ZodError ? z.prettifyError(e) : e);
500
+ }
501
+ });
502
+ axiumPlugin = program.command('plugin').alias('plugins').description('Manage plugins').addOption(opts.global);
503
+ axiumPlugin
504
+ .command('list')
505
+ .alias('ls')
506
+ .description('List loaded plugins')
507
+ .option('-l, --long', 'use the long listing format')
508
+ .option('--no-versions', 'do not show plugin versions')
509
+ .action((opt) => {
510
+ if (!plugins.size) {
511
+ console.log('No plugins loaded.');
512
+ return;
513
+ }
514
+ if (!opt.long) {
515
+ console.log(Array.from(plugins.keys()).join(', '));
516
+ return;
517
+ }
518
+ console.log(styleText('whiteBright', plugins.size + ' plugin(s) loaded:'));
519
+ for (const plugin of plugins.values()) {
520
+ console.log(plugin.name, opt.versions ? plugin.version : '');
521
+ }
522
+ });
523
+ axiumPlugin
524
+ .command('info')
525
+ .description('Get information about a plugin')
526
+ .argument('<plugin>', 'the plugin to get information about')
527
+ .action((search) => {
528
+ const plugin = _findPlugin(search);
529
+ for (const line of pluginText(plugin))
530
+ console.log(line);
531
+ });
532
+ axiumPlugin
533
+ .command('remove')
534
+ .alias('rm')
535
+ .description('Remove a plugin')
536
+ .argument('<plugin>', 'the plugin to remove')
537
+ .action(async (search, opt) => {
538
+ const plugin = _findPlugin(search);
539
+ await plugin._hooks?.remove?.(opt);
540
+ for (const [path, data] of configFiles) {
541
+ if (!data.plugins)
542
+ continue;
543
+ data.plugins = data.plugins.filter(p => p !== plugin.specifier);
544
+ saveConfigTo(path, data);
545
+ }
546
+ plugins.delete(plugin.name);
547
+ });
548
+ axiumPlugin
549
+ .command('init')
550
+ .alias('setup')
551
+ .alias('install')
552
+ .description('Initialize a plugin. This could include adding tables to the database or linking routes.')
553
+ .addOption(opts.timeout)
554
+ .addOption(opts.check)
555
+ .argument('<plugin>', 'the plugin to initialize')
556
+ .action(async (search, opt) => {
557
+ const env_3 = { stack: [], error: void 0, hasError: false };
558
+ try {
559
+ const plugin = _findPlugin(search);
560
+ if (!plugin)
561
+ io.exit(`Can't find a plugin matching "${search}"`);
562
+ const _ = __addDisposableResource(env_3, db.connect(), true);
563
+ }
564
+ catch (e_3) {
565
+ env_3.error = e_3;
566
+ env_3.hasError = true;
567
+ }
568
+ finally {
569
+ const result_2 = __disposeResources(env_3);
570
+ if (result_2)
571
+ await result_2;
572
+ }
573
+ });
574
+ axiumApps = program.command('apps').description('Manage Axium apps').addOption(opts.global);
575
+ axiumApps
576
+ .command('list')
577
+ .alias('ls')
578
+ .description('List apps added by plugins')
579
+ .option('-l, --long', 'use the long listing format')
580
+ .option('-b, --builtin', 'include built-in apps')
581
+ .action((opt) => {
582
+ if (!apps.size) {
583
+ console.log('No apps.');
584
+ return;
585
+ }
586
+ if (!opt.long) {
587
+ console.log(Array.from(apps.values().map(app => app.name)).join(', '));
588
+ return;
589
+ }
590
+ console.log(styleText('whiteBright', apps.size + ' app(s) loaded:'));
591
+ for (const app of apps.values()) {
592
+ console.log(app.name, styleText('dim', `(${app.id})`));
593
+ }
594
+ });
595
+ argUserLookup = new Argument('<user>', 'the UUID or email of the user to operate on').argParser(lookupUser);
596
+ program
597
+ .command('user')
598
+ .description('Get or change information about a user')
599
+ .addArgument(argUserLookup)
600
+ .option('-S, --sessions', 'show user sessions')
601
+ .option('-P, --passkeys', 'show user passkeys')
602
+ .option('--add-role <role...>', 'add roles to the user')
603
+ .option('--remove-role <role...>', 'remove roles from the user')
604
+ .option('--tag <tag...>', 'Add tags to the user')
605
+ .option('--untag <tag...>', 'Remove tags from the user')
606
+ .option('--delete', 'Delete the user')
607
+ .option('--suspend', 'Suspend the user')
608
+ .addOption(new Option('--unsuspend', 'Un-suspend the user').conflicts('suspend'))
609
+ .action(async (_user, opt) => {
610
+ let user = await _user;
611
+ const [updatedRoles, roles, rolesDiff] = diffUpdate(user.roles, opt.addRole, opt.removeRole);
612
+ const [updatedTags, tags, tagsDiff] = diffUpdate(user.tags, opt.tag, opt.untag);
613
+ const changeSuspend = (opt.suspend || opt.unsuspend) && user.isSuspended != (opt.suspend ?? !opt.unsuspend);
614
+ if (updatedRoles || updatedTags || changeSuspend) {
615
+ user = await db.database
616
+ .updateTable('users')
617
+ .set({ roles, tags, isSuspended: !changeSuspend ? user.isSuspended : (opt.suspend ?? !opt.unsuspend) })
618
+ .returningAll()
619
+ .executeTakeFirstOrThrow()
620
+ .then(u => {
621
+ if (updatedRoles && rolesDiff)
622
+ console.log(`> Updated roles: ${rolesDiff}`);
623
+ if (updatedTags && tagsDiff)
624
+ console.log(`> Updated tags: ${tagsDiff}`);
625
+ if (changeSuspend)
626
+ console.log(opt.suspend ? '> Suspended' : '> Un-suspended');
627
+ return u;
628
+ })
629
+ .catch(e => io.exit('Failed to update user: ' + e.message));
630
+ }
631
+ if (opt.delete) {
632
+ const confirmed = await rl
633
+ .question(`Are you sure you want to delete ${userText(user, true)}? (y/N) `)
634
+ .then(v => z.stringbool().parseAsync(v))
635
+ .catch(() => false);
636
+ if (!confirmed)
637
+ console.log(styleText('dim', '> Delete aborted.'));
638
+ else
639
+ await db.database
640
+ .deleteFrom('users')
641
+ .where('id', '=', user.id)
642
+ .executeTakeFirstOrThrow()
643
+ .then(() => console.log(styleText(['red', 'bold'], '> Deleted')))
644
+ .catch(e => io.exit('Failed to delete user: ' + e.message));
645
+ }
646
+ console.log([
647
+ user.isSuspended && styleText('yellowBright', 'Suspended'),
648
+ user.isAdmin && styleText('redBright', 'Administrator'),
649
+ 'UUID: ' + user.id,
650
+ 'Name: ' + user.name,
651
+ `Email: ${user.email}, ${user.emailVerified ? 'verified on ' + formatDateRange(user.emailVerified) : styleText(config.auth.email_verification ? 'yellow' : 'dim', 'not verified')}`,
652
+ 'Registered ' + formatDateRange(user.registeredAt),
653
+ `Roles: ${user.roles.length ? user.roles.join(', ') : styleText('dim', '(none)')}`,
654
+ `Tags: ${user.tags.length ? user.tags.join(', ') : styleText('dim', '(none)')}`,
655
+ ]
656
+ .filter(v => v)
657
+ .join('\n'));
658
+ if (opt.sessions) {
659
+ const sessions = await db.database.selectFrom('sessions').where('userId', '=', user.id).selectAll().execute();
660
+ console.log(styleText('bold', 'Sessions:'));
661
+ if (!sessions.length)
662
+ console.log(styleText('dim', '(none)'));
663
+ else
664
+ for (const session of sessions) {
665
+ console.log(`\t${session.id}\tcreated ${formatDateRange(session.created).padEnd(40)}\texpires ${formatDateRange(session.expires).padEnd(40)}\t${session.elevated ? styleText('yellow', '(elevated)') : ''}`);
666
+ }
667
+ }
668
+ if (opt.passkeys) {
669
+ const passkeys = await db.database.selectFrom('passkeys').where('userId', '=', user.id).selectAll().execute();
670
+ console.log(styleText('bold', 'Passkeys:'));
671
+ for (const passkey of passkeys) {
672
+ 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'}.`);
673
+ }
674
+ }
675
+ });
676
+ program
677
+ .command('toggle-admin')
678
+ .description('Toggle whether a user is an administrator')
679
+ .addArgument(argUserLookup)
680
+ .action(async (_user) => {
681
+ const user = await _user;
682
+ const isAdmin = !user.isAdmin;
683
+ await db.database.updateTable('users').set({ isAdmin }).where('id', '=', user.id).executeTakeFirstOrThrow();
684
+ await audit('admin_change', undefined, { user: user.id });
685
+ console.log(`${userText(user)} is ${isAdmin ? 'now' : 'no longer'} an administrator. (${styleText(['whiteBright', 'bold'], isAdmin.toString())})`);
686
+ });
687
+ program
688
+ .command('status')
689
+ .alias('stats')
690
+ .description('Get information about the server')
691
+ .addOption(opts.host)
692
+ .action(async () => {
693
+ console.log('Axium Server v' + $pkg.version);
694
+ console.log(styleText('whiteBright', 'Debug mode:'), config.debug ? styleText('yellow', 'enabled') : 'disabled');
695
+ const configFiles = config.files.keys().toArray();
696
+ console.log(styleText('whiteBright', 'Loaded config files:'), styleText(['dim', 'bold'], `(${configFiles.length})`), configFiles.join(', '));
697
+ outputDaemonStatus('axium');
698
+ process.stdout.write(styleText('whiteBright', 'Database: '));
699
+ try {
700
+ console.log(await db.statText());
701
+ }
702
+ catch {
703
+ console.log(styleText('red', 'Unavailable'));
704
+ }
705
+ console.log(styleText('whiteBright', 'Loaded plugins:'), styleText(['dim', 'bold'], `(${plugins.size || 'none'})`), Array.from(plugins.keys()).join(', '));
706
+ for (const plugin of plugins.values()) {
707
+ if (!plugin._hooks?.statusText)
708
+ continue;
709
+ const text = await plugin._hooks?.statusText();
710
+ console.log(styleText('bold', plugin.name), plugin.version + ':', text.includes('\n') ? '\n' + text : text);
711
+ }
712
+ });
713
+ program
714
+ .command('ports')
715
+ .description('Enable or disable use of restricted ports (e.g. 443)')
716
+ .addArgument(new Argument('<action>', 'The action to take').choices(_portActions))
717
+ .addOption(new Option('-m, --method <method>', 'the method to use').choices(_portMethods).default('node-cap'))
718
+ .option('-N, --node <path>', 'the path to the node binary')
719
+ .action(async (action, opt) => {
720
+ await restrictedPorts({ ...opt, action }).catch(io.handleError);
721
+ });
722
+ program
723
+ .command('init')
724
+ .description('Install Axium server')
725
+ .addOption(opts.force)
726
+ .addOption(opts.host)
727
+ .addOption(opts.check)
728
+ .addOption(opts.packagesDir)
729
+ .option('-s, --skip', 'Skip already initialized steps')
730
+ .action(async (opt) => {
731
+ await db.init(opt).catch(io.handleError);
732
+ await dbInitTables().catch(io.handleError);
733
+ await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(io.handleError);
734
+ });
735
+ program
736
+ .command('serve')
737
+ .description('Start the Axium server')
738
+ .option('-p, --port <port>', 'the port to listen on')
739
+ .option('--ssl <prefix>', 'the prefix for the cert.pem and key.pem SSL files')
740
+ .option('-b, --build <path>', 'the path to the handler build')
741
+ .action(async (opt) => {
742
+ const server = await serve({
743
+ secure: opt.ssl ? true : config.web.secure,
744
+ ssl_cert: opt.ssl ? join(opt.ssl, 'cert.pem') : config.web.ssl_cert,
745
+ ssl_key: opt.ssl ? join(opt.ssl, 'key.pem') : config.web.ssl_key,
746
+ build: opt.build ? resolve(opt.build) : config.web.build,
747
+ });
748
+ const port = !Number.isNaN(Number.parseInt(opt.port ?? 'NaN')) ? Number.parseInt(opt.port) : config.web.port;
749
+ server.listen(port, () => {
750
+ console.log('Server is listening on port ' + port);
751
+ });
752
+ });
753
+ program
754
+ .command('link')
755
+ .description('Link routes provided by plugins and the server')
756
+ .addOption(opts.packagesDir)
757
+ .addOption(new Option('-l, --list', 'list route links').conflicts('delete'))
758
+ .option('-d, --delete', 'delete route links')
759
+ .argument('[name...]', 'List of plugin names to operate on. If not specified, operates on all plugins and built-in routes.')
760
+ .action(async function (names) {
761
+ const opt = this.optsWithGlobals();
762
+ if (names.length)
763
+ opt.only = names;
764
+ if (opt.list) {
765
+ for (const link of listRouteLinks(opt)) {
766
+ const idText = link.id.startsWith('#') ? `(${link.id.slice(1)})` : link.id;
767
+ const fromColor = await access(link.from)
768
+ .then(() => 'cyanBright')
769
+ .catch(() => 'redBright');
770
+ console.log(`${idText}:\t ${styleText(fromColor, link.from)}\t->\t${link.to.replace(/.*\/node_modules\//, styleText('dim', '$&'))}`);
771
+ }
772
+ return;
773
+ }
774
+ if (opt.delete) {
775
+ unlinkRoutes(opt);
776
+ return;
777
+ }
778
+ linkRoutes(opt);
779
+ });
780
+ program
781
+ .command('audit')
782
+ .description('View audit logs')
783
+ .option('-x, --extra', 'Include the extra object when listing events')
784
+ .option('-t, --include-tags', 'Include tags when listing events')
785
+ .addOption(new Option('-s, --summary', 'Summarize audit log entries instead of displaying individual ones').conflicts(['extra', 'includeTags']))
786
+ .optionsGroup('Filters:')
787
+ .option('--since <date>', 'Filter for events since a date')
788
+ .option('--until <date>', 'Filter for events until a date')
789
+ .option('--user <uuid|null>', 'Filter for events triggered by a user')
790
+ .addOption(new Option('--severity <level>', 'Filter for events at or above a severity level').choices(severityNames))
791
+ .option('--source <source>', 'Filter by source')
792
+ .option('--tag <tag...>', 'Filter by tag(s)')
793
+ .option('--event <event>', 'Filter by event name')
794
+ .action(async (opt) => {
795
+ const filter = await AuditFilter.parseAsync(opt).catch(e => io.exit('Invalid filter: ' + z.prettifyError(e)));
796
+ const events = await getEvents(filter).execute();
797
+ if (opt.summary) {
798
+ const groups = Object.groupBy(events, e => e.severity);
799
+ const maxGroupLength = Math.max(...Object.values(groups).map(g => g.length.toString().length), 0);
800
+ for (const [severity, group] of Object.entries(groups)) {
801
+ if (!group?.length)
802
+ continue;
803
+ console.log(styleText('white', group.length.toString().padStart(maxGroupLength)), styleSeverity(severity, true), 'events. Latest occurred', group.at(-1).timestamp.toLocaleString());
804
+ }
805
+ return;
806
+ }
807
+ let maxSource = 0, maxName = 0, maxTags = 0, maxExtra = 0;
808
+ for (const event of events) {
809
+ maxSource = Math.max(maxSource, event.source.length);
810
+ maxName = Math.max(maxName, event.name.length);
811
+ event._tags = !event.tags.length
812
+ ? ''
813
+ : opt.includeTags
814
+ ? '# ' + event.tags.join(', ')
815
+ : `(${event.tags.length} tag${event.tags.length == 1 ? '' : 's'})`;
816
+ maxTags = Math.max(maxTags, event._tags.length);
817
+ const extraKeys = Object.keys(event.extra);
818
+ event._extra = !extraKeys.length ? '' : opt.extra ? JSON.stringify(event.extra) : '+' + extraKeys.length;
819
+ maxExtra = Math.max(maxExtra, event._extra.length);
820
+ }
821
+ for (const event of events) {
822
+ console.log(styleSeverity(event.severity, true), styleText('dim', io.prettyDate(event.timestamp)), event.source.padEnd(maxSource), styleText('whiteBright', event.name.padEnd(maxName)), styleText('gray', event._tags.padEnd(maxTags)), 'by', event.userId ? event.userId : styleText(['dim', 'italic'], 'unknown'.padEnd(36)), styleText('blue', event._extra));
823
+ }
824
+ });
825
+ await program.parseAsync();
826
+ }
827
+ catch (e_1) {
828
+ env_1.error = e_1;
829
+ env_1.hasError = true;
830
+ }
831
+ finally {
832
+ __disposeResources(env_1);
833
+ }