@axium/server 0.29.0 → 0.31.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/database.js CHANGED
@@ -63,6 +63,7 @@ import * as z from 'zod';
63
63
  import config from './config.js';
64
64
  import rawSchema from './db.json' with { type: 'json' };
65
65
  import { dirs, systemDir } from './io.js';
66
+ pg.types.setTypeParser(pg.types.builtins.INT8, BigInt);
66
67
  const sym = Symbol.for('Axium:database');
67
68
  export let database;
68
69
  export function connect() {
@@ -230,8 +231,55 @@ export async function init(opt) {
230
231
  await result_1;
231
232
  }
232
233
  }
234
+ const numberTypes = [
235
+ 'integer',
236
+ 'int2',
237
+ 'int4',
238
+ 'int8',
239
+ 'smallint',
240
+ 'real',
241
+ 'double precision',
242
+ 'float4',
243
+ 'float8',
244
+ 'decimal',
245
+ 'numeric',
246
+ 'serial',
247
+ ];
248
+ const bigintTypes = ['bigint', 'bigserial'];
249
+ const booleanTypes = ['boolean', 'bool'];
250
+ const stringTypes = ['varchar', 'char', 'text'];
251
+ const dateTypes = ['date', 'datetime', 'time', 'timetz', 'timestamp', 'timestamptz'];
252
+ const binaryTypes = ['binary', 'bytea', 'varbinary', 'blob'];
253
+ const numericRangeTypes = ['int4range', 'numrange'];
254
+ const stringRangeTypes = ['tsrange', 'tstzrange', 'daterange'];
255
+ const multirangeTypes = ['int4multirange', 'int8multirange', 'nummultirange', 'tsmultirange', 'tstzmultirange', 'datemultirange'];
256
+ const _primitive = z.literal([
257
+ ...numberTypes,
258
+ ...bigintTypes,
259
+ ...booleanTypes,
260
+ ...stringTypes,
261
+ ...dateTypes,
262
+ ...binaryTypes,
263
+ ...numericRangeTypes,
264
+ ...stringRangeTypes,
265
+ ...multirangeTypes,
266
+ 'uuid',
267
+ 'json',
268
+ 'jsonb',
269
+ ]);
270
+ const _ColumnType = z.union([
271
+ _primitive,
272
+ z.templateLiteral([
273
+ z.literal(['char', 'varchar', 'binary', 'varbinary', 'datetime', 'time', 'timetz', 'timestamp', 'timestamptz']),
274
+ '(',
275
+ z.int().nonnegative(),
276
+ ')',
277
+ ]),
278
+ z.templateLiteral([z.literal(['decimal', 'numeric']), '(', z.int().nonnegative(), z.literal([',', ', ']), z.int().nonnegative(), ')']),
279
+ ]);
280
+ const ColumnType = z.union([_ColumnType, z.templateLiteral([_ColumnType, '[', z.int().nonnegative().optional(), ']'])]);
233
281
  export const Column = z.strictObject({
234
- type: z.string(),
282
+ type: ColumnType,
235
283
  required: z.boolean().default(false),
236
284
  unique: z.boolean().default(false),
237
285
  primary: z.boolean().default(false),
@@ -275,7 +323,7 @@ export const SchemaDecl = z.strictObject({
275
323
  indexes: IndexString.array().optional().default([]),
276
324
  });
277
325
  export const ColumnDelta = z.strictObject({
278
- type: z.string().optional(),
326
+ type: ColumnType.optional(),
279
327
  default: z.string().optional(),
280
328
  ops: z.literal(['drop_default', 'set_required', 'drop_required']).array().optional(),
281
329
  });
@@ -317,8 +365,7 @@ export function* getSchemaFiles() {
317
365
  yield [name, SchemaFile.parse(plugin._db)];
318
366
  }
319
367
  catch (e) {
320
- const text = e instanceof z.core.$ZodError ? z.prettifyError(e) : e instanceof Error ? e.message : String(e);
321
- throw `Invalid database configuration for plugin "${name}":\n${text}`;
368
+ throw `Invalid database configuration for plugin "${name}":\n${io.errorText(e)}`;
322
369
  }
323
370
  }
324
371
  }
@@ -334,10 +381,16 @@ export function getFullSchema(opt = {}) {
334
381
  let currentSchema = { tables: {}, indexes: [] };
335
382
  fullSchema.versions[pluginName] = file.latest;
336
383
  for (const [version, schema] of file.versions.entries()) {
337
- if (schema.delta)
338
- applyDeltaToSchema(currentSchema, schema);
339
- else
384
+ if (!schema.delta)
340
385
  currentSchema = schema;
386
+ else {
387
+ try {
388
+ applyDeltaToSchema(currentSchema, schema);
389
+ }
390
+ catch (e) {
391
+ throw `Failed to apply version ${version - 1}->${version} delta to ${pluginName}: ${io.errorText(e)}`;
392
+ }
393
+ }
341
394
  if (version === file.latest)
342
395
  break;
343
396
  }
@@ -375,20 +428,20 @@ export function setUpgradeInfo(info) {
375
428
  }
376
429
  export function applyTableDeltaToSchema(table, delta) {
377
430
  for (const column of delta.drop_columns) {
378
- if (column in table)
431
+ if (column in table.columns)
379
432
  delete table.columns[column];
380
433
  else
381
- throw `Can't drop column ${column} because it does not exist`;
434
+ throw `can't drop column ${column} because it does not exist`;
382
435
  }
383
436
  for (const [name, column] of Object.entries(delta.add_columns)) {
384
- if (name in table)
385
- throw `Can't add column ${name} because it already exists`;
437
+ if (name in table.columns)
438
+ throw `can't add column ${name} because it already exists`;
386
439
  table.columns[name] = column;
387
440
  }
388
441
  for (const [name, columnDelta] of Object.entries(delta.alter_columns)) {
389
442
  const column = table.columns[name];
390
443
  if (!column)
391
- throw `Can't modify column ${name} because it does not exist`;
444
+ throw `can't modify column ${name} because it does not exist`;
392
445
  if (columnDelta.type)
393
446
  column.type = columnDelta.type;
394
447
  if (columnDelta.default)
@@ -411,11 +464,11 @@ export function applyTableDeltaToSchema(table, delta) {
411
464
  if (table.constraints[name])
412
465
  delete table.constraints[name];
413
466
  else
414
- throw `Can't drop constraint ${name} because it does not exist`;
467
+ throw `can't drop constraint ${name} because it does not exist`;
415
468
  }
416
469
  for (const [name, constraint] of Object.entries(delta.add_constraints)) {
417
470
  if (table.constraints[name])
418
- throw `Can't add constraint ${name} because it already exists`;
471
+ throw `can't add constraint ${name} because it already exists`;
419
472
  table.constraints[name] = constraint;
420
473
  }
421
474
  }
@@ -424,11 +477,11 @@ export function applyDeltaToSchema(schema, delta) {
424
477
  if (tableName in schema.tables)
425
478
  delete schema.tables[tableName];
426
479
  else
427
- throw `Can't drop table ${tableName} because it does not exist`;
480
+ throw `can't drop table ${tableName} because it does not exist`;
428
481
  }
429
482
  for (const [tableName, table] of Object.entries(delta.add_tables)) {
430
483
  if (tableName in schema.tables)
431
- throw `Can't add table ${tableName} because it already exists`;
484
+ throw `can't add table ${tableName} because it already exists`;
432
485
  else
433
486
  schema.tables[tableName] = table;
434
487
  }
@@ -436,7 +489,7 @@ export function applyDeltaToSchema(schema, delta) {
436
489
  if (tableName in schema.tables)
437
490
  applyTableDeltaToSchema(schema.tables[tableName], tableDelta);
438
491
  else
439
- throw `Can't modify table ${tableName} because it does not exist`;
492
+ throw `can't modify table ${tableName} because it does not exist`;
440
493
  }
441
494
  }
442
495
  export function validateDelta(delta) {
@@ -734,10 +787,10 @@ export async function applyDelta(delta, forceAbort = false) {
734
787
  /**
735
788
  * Checks that a table has the expected column types, nullability, and default values.
736
789
  */
737
- export async function checkTableTypes(tableName, types, opt) {
790
+ export async function checkTableTypes(tableName, types, opt, tableMetadata) {
738
791
  io.start(`Checking table ${tableName}`);
739
- const dbTables = opt._metadata || (await database.introspection.getTables());
740
- const table = dbTables.find(t => (t.schema == 'public' ? t.name : `${t.schema}.${t.name}`) === tableName);
792
+ tableMetadata ||= await database.introspection.getTables();
793
+ const table = tableMetadata.find(t => (t.schema == 'public' ? t.name : `${t.schema}.${t.name}`) === tableName);
741
794
  if (!table)
742
795
  throw 'missing.';
743
796
  const columns = Object.fromEntries(table.columns.map(c => [c.name, c]));
package/dist/db.json CHANGED
@@ -65,6 +65,16 @@
65
65
  }
66
66
  },
67
67
  "indexes": ["sessions:id", "passkeys:userId"]
68
+ },
69
+ {
70
+ "delta": true,
71
+ "alter_tables": {
72
+ "users": {
73
+ "alter_columns": {
74
+ "name": { "ops": ["set_required"] }
75
+ }
76
+ }
77
+ }
68
78
  }
69
79
  ],
70
80
  "wipe": ["users", "verifications", "passkeys", "sessions", "audit_log"]
package/dist/linking.js CHANGED
@@ -40,7 +40,7 @@ export function linkRoutes(options = {}) {
40
40
  unlinkSync(from);
41
41
  }
42
42
  catch (e) {
43
- io.exit(e && e instanceof Error ? e.message : e.toString());
43
+ io.exit(e);
44
44
  }
45
45
  io.done();
46
46
  io.start('Re-linking ' + text);
@@ -50,7 +50,7 @@ export function linkRoutes(options = {}) {
50
50
  io.done();
51
51
  }
52
52
  catch (e) {
53
- io.exit(e && e instanceof Error ? e.message : e.toString());
53
+ io.exit(e);
54
54
  }
55
55
  }
56
56
  }
@@ -65,7 +65,7 @@ export function unlinkRoutes(options = {}) {
65
65
  io.done();
66
66
  }
67
67
  catch (e) {
68
- io.exit(e && e instanceof Error ? e.message : e.toString());
68
+ io.exit(e);
69
69
  }
70
70
  }
71
71
  }
package/dist/main.js CHANGED
@@ -60,17 +60,17 @@ import { Argument, Option, program } from 'commander';
60
60
  import { access } from 'node:fs/promises';
61
61
  import { join, resolve } from 'node:path/posix';
62
62
  import { createInterface } from 'node:readline/promises';
63
- import { styleText, parseArgs } from 'node:util';
63
+ import { parseArgs, styleText } from 'node:util';
64
64
  import { getByString, isJSON, setByString } from 'utilium';
65
65
  import * as z from 'zod';
66
66
  import $pkg from '../package.json' with { type: 'json' };
67
67
  import { audit, getEvents, styleSeverity } from './audit.js';
68
+ import { diffUpdate, lookupUser, userText } from './cli.js';
68
69
  import config, { configFiles, FileSchema, saveConfigTo } from './config.js';
69
70
  import * as db from './database.js';
70
71
  import { _portActions, _portMethods, restrictedPorts } from './io.js';
71
72
  import { linkRoutes, listRouteLinks, unlinkRoutes } from './linking.js';
72
73
  import { serve } from './serve.js';
73
- import { diffUpdate, lookupUser, userText } from './cli.js';
74
74
  async function rlConfirm(question = 'Is this ok') {
75
75
  const { data, error } = z
76
76
  .stringbool()
@@ -122,13 +122,11 @@ try {
122
122
  .name('axium')
123
123
  .description('Axium server CLI')
124
124
  .configureHelp({ showGlobalOptions: true })
125
- .option('--safe', 'do not execute code from plugins')
125
+ .option('--safe', 'do not execute code from plugins', false)
126
126
  .option('--debug', 'override debug mode')
127
127
  .option('--no-debug', 'override debug mode')
128
- .option('-c, --config <path>', 'path to the config file');
129
- program.on('option:debug', () => config.set({ debug: true }));
130
- noAutoDB = ['init', 'serve', 'check'];
131
- program.hook('preAction', (_, action) => {
128
+ .option('-c, --config <path>', 'path to the config file')
129
+ .hook('preAction', (_, action) => {
132
130
  const opt = action.optsWithGlobals();
133
131
  opt.force && io.warn('--force: Protections disabled.');
134
132
  if (typeof opt.debug == 'boolean') {
@@ -142,23 +140,19 @@ try {
142
140
  if (!noAutoDB.includes(action.name()))
143
141
  throw e;
144
142
  }
145
- });
146
- program.hook('postAction', async (_, action) => {
143
+ })
144
+ .hook('postAction', async (_, action) => {
147
145
  if (!noAutoDB.includes(action.name()))
148
146
  await db.database.destroy();
149
- });
147
+ })
148
+ .on('option:debug', () => config.set({ debug: true }));
149
+ noAutoDB = ['init', 'serve', 'check'];
150
150
  // Options shared by multiple (sub)commands
151
151
  opts = {
152
- // database specific
153
- host: new Option('-H, --host <host>', 'the host of the database.').argParser(value => {
154
- const [hostname, port] = value?.split(':') ?? [];
155
- config.db.host = hostname || config.db.host;
156
- config.db.port = port && Number.isSafeInteger(parseInt(port)) ? parseInt(port) : config.db.port;
157
- }),
158
152
  check: new Option('--check', 'check the database schema after initialization').default(false),
159
153
  force: new Option('-f, --force', 'force the operation').default(false),
160
154
  global: new Option('-g, --global', 'apply the operation globally').default(false),
161
- timeout: new Option('-t, --timeout <ms>', 'how long to wait for commands to complete.').default('1000').argParser(value => {
155
+ timeout: new Option('-t, --timeout <ms>', 'how long to wait for commands to complete.').default(1000).argParser(value => {
162
156
  const timeout = parseInt(value);
163
157
  if (!Number.isSafeInteger(timeout) || timeout < 0)
164
158
  io.warn('Invalid timeout value, using default.');
@@ -166,17 +160,17 @@ try {
166
160
  }),
167
161
  packagesDir: new Option('-p, --packages-dir <dir>', 'the directory to look for packages in'),
168
162
  };
169
- axiumDB = program.command('db').alias('database').description('Manage the database').addOption(opts.timeout).addOption(opts.host);
163
+ axiumDB = program.command('db').alias('database').description('Manage the database').addOption(opts.timeout);
170
164
  axiumDB
171
165
  .command('init')
172
166
  .description('Initialize the database')
173
167
  .addOption(opts.force)
174
- .option('-s, --skip', 'If the user, database, or schema already exists, skip trying to create it.')
168
+ .option('-s, --skip', 'If the user, database, or schema already exists, skip trying to create it.', false)
175
169
  .addOption(opts.check)
176
- .action(async (_localOpts, _) => {
177
- const opt = _.optsWithGlobals();
178
- await db.init(opt).catch(io.handleError);
179
- await dbInitTables().catch(io.handleError);
170
+ .action(async function axium_db_init() {
171
+ const opt = this.optsWithGlobals();
172
+ await db.init(opt).catch(io.exit);
173
+ await dbInitTables().catch(io.exit);
180
174
  });
181
175
  axiumDB
182
176
  .command('status')
@@ -204,9 +198,9 @@ try {
204
198
  io.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
205
199
  process.exit(2);
206
200
  }
207
- await db._sql('DROP DATABASE axium', 'Dropping database').catch(io.handleError);
208
- await db._sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges').catch(io.handleError);
209
- await db._sql('DROP USER axium', 'Dropping user').catch(io.handleError);
201
+ await db._sql('DROP DATABASE axium', 'Dropping database').catch(io.exit);
202
+ await db._sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges').catch(io.exit);
203
+ await db._sql('DROP USER axium', 'Dropping user').catch(io.exit);
210
204
  await db
211
205
  .getHBA(opt)
212
206
  .then(([content, writeBack]) => {
@@ -249,7 +243,7 @@ try {
249
243
  console.log(table + ' '.repeat(maxTableName - table.length), '|', plugins);
250
244
  }
251
245
  await rlConfirm('Are you sure you want to wipe these tables and any dependents');
252
- await db.database.deleteFrom(Array.from(tables.keys())).execute().catch(io.handleError);
246
+ await db.database.deleteFrom(Array.from(tables.keys())).execute().catch(io.exit);
253
247
  });
254
248
  axiumDB
255
249
  .command('check')
@@ -258,23 +252,20 @@ try {
258
252
  .action(async (opt) => {
259
253
  const env_2 = { stack: [], error: void 0, hasError: false };
260
254
  try {
261
- await io.run('Checking for sudo', 'which sudo').catch(io.handleError);
262
- await io.run('Checking for psql', 'which psql').catch(io.handleError);
255
+ await io.run('Checking for sudo', 'which sudo').catch(io.exit);
256
+ await io.run('Checking for psql', 'which psql').catch(io.exit);
263
257
  const throwUnlessRows = (text) => {
264
258
  if (text.includes('(0 rows)'))
265
259
  throw 'missing.';
266
260
  return text;
267
261
  };
268
- await db
269
- ._sql(`SELECT 1 FROM pg_database WHERE datname = 'axium'`, 'Checking for database')
270
- .then(throwUnlessRows)
271
- .catch(io.handleError);
272
- await db._sql(`SELECT 1 FROM pg_roles WHERE rolname = 'axium'`, 'Checking for user').then(throwUnlessRows).catch(io.handleError);
262
+ await db._sql(`SELECT 1 FROM pg_database WHERE datname = 'axium'`, 'Checking for database').then(throwUnlessRows).catch(io.exit);
263
+ await db._sql(`SELECT 1 FROM pg_roles WHERE rolname = 'axium'`, 'Checking for user').then(throwUnlessRows).catch(io.exit);
273
264
  io.start('Connecting to database');
274
265
  const _ = __addDisposableResource(env_2, db.connect(), true);
275
266
  io.done();
276
267
  io.start('Getting schema metadata');
277
- const schemas = await db.database.introspection.getSchemas().catch(io.handleError);
268
+ const schemas = await db.database.introspection.getSchemas().catch(io.exit);
278
269
  io.done();
279
270
  io.start('Checking for acl schema');
280
271
  if (!schemas.find(s => s.name == 'acl'))
@@ -284,13 +275,21 @@ try {
284
275
  const tablePromises = await Promise.all([
285
276
  db.database.introspection.getTables(),
286
277
  db.database.withSchema('acl').introspection.getTables(),
287
- ]).catch(io.handleError);
288
- opt._metadata = tablePromises.flat();
289
- const tables = Object.fromEntries(opt._metadata.map(t => [t.schema == 'public' ? t.name : `${t.schema}.${t.name}`, t]));
278
+ ]).catch(io.exit);
279
+ const tableMetadata = tablePromises.flat();
280
+ const tables = Object.fromEntries(tableMetadata.map(t => [t.schema == 'public' ? t.name : `${t.schema}.${t.name}`, t]));
290
281
  io.done();
291
- const schema = db.getFullSchema();
282
+ io.start('Resolving database schemas');
283
+ let schema;
284
+ try {
285
+ schema = db.getFullSchema();
286
+ io.done();
287
+ }
288
+ catch (e) {
289
+ io.exit(e);
290
+ }
292
291
  for (const [name, table] of Object.entries(schema.tables)) {
293
- await db.checkTableTypes(name, table, opt);
292
+ await db.checkTableTypes(name, table, opt, tableMetadata);
294
293
  delete tables[name];
295
294
  }
296
295
  io.start('Checking for extra tables');
@@ -327,13 +326,13 @@ try {
327
326
  .command('schema')
328
327
  .description('Get the JSON schema for the database configuration file')
329
328
  .option('-j, --json', 'values are JSON encoded')
330
- .action((opt) => {
329
+ .action(opt => {
331
330
  try {
332
331
  const schema = z.toJSONSchema(db.SchemaFile, { io: 'input' });
333
332
  console.log(opt.json ? JSON.stringify(schema, null, 4) : schema);
334
333
  }
335
334
  catch (e) {
336
- io.handleError(e instanceof z.core.$ZodError ? z.prettifyError(e) : e);
335
+ io.exit(e);
337
336
  }
338
337
  });
339
338
  axiumDB
@@ -342,7 +341,7 @@ try {
342
341
  .alias('up')
343
342
  .description('Upgrade the database to the latest version')
344
343
  .option('--abort', 'Rollback changes instead of committing them')
345
- .action(async (opt) => {
344
+ .action(async function axium_db_upgrade(opt) {
346
345
  const deltas = [];
347
346
  const info = db.getUpgradeInfo();
348
347
  let empty = true;
@@ -401,7 +400,7 @@ try {
401
400
  io.exit(e);
402
401
  }
403
402
  console.log('Applying delta.');
404
- await db.applyDelta(delta, opt.abort).catch(io.handleError);
403
+ await db.applyDelta(delta, opt.abort).catch(io.exit);
405
404
  info.upgrades.push({ timestamp: new Date(), from, to });
406
405
  db.setUpgradeInfo(info);
407
406
  });
@@ -442,20 +441,20 @@ try {
442
441
  lengths.available = Math.max(lengths.available || 0, available.length);
443
442
  }
444
443
  for (const [i, entry] of entries.entries()) {
445
- console.log(...['name', 'current', 'latest', 'available'].map(key => styleText(i === 0 ? ['whiteBright', 'underline'] : entry[key] === undefined ? 'none' : [], entry[key].padStart(lengths[key]))));
444
+ console.log(...['name', 'current', 'latest', 'available'].map(key => styleText(i === 0 ? ['whiteBright', 'underline'] : entry[key] === undefined ? 'reset' : [], entry[key].padStart(lengths[key]))));
446
445
  }
447
446
  });
448
447
  axiumConfig = program
449
448
  .command('config')
450
449
  .description('Manage the configuration')
451
450
  .addOption(opts.global)
452
- .option('-j, --json', 'values are JSON encoded')
453
- .option('-r, --redact', 'Do not output sensitive values');
451
+ .option('-j, --json', 'values are JSON encoded', false)
452
+ .option('-r, --redact', 'Do not output sensitive values', false);
454
453
  axiumConfig
455
454
  .command('dump')
456
455
  .description('Output the entire current configuration')
457
- .action(() => {
458
- const opt = axiumConfig.optsWithGlobals();
456
+ .action(function axium_config_dump() {
457
+ const opt = this.optsWithGlobals();
459
458
  const value = config.plain();
460
459
  console.log(opt.json ? JSON.stringify(value, configReplacer(opt), 4) : value);
461
460
  });
@@ -463,8 +462,8 @@ try {
463
462
  .command('get')
464
463
  .description('Get a config value')
465
464
  .argument('<key>', 'the key to get')
466
- .action((key) => {
467
- const opt = axiumConfig.optsWithGlobals();
465
+ .action(function axium_config_get(key) {
466
+ const opt = this.optsWithGlobals();
468
467
  const value = getByString(config.plain(), key);
469
468
  console.log(opt.json ? JSON.stringify(value, configReplacer(opt), 4) : value);
470
469
  });
@@ -473,8 +472,8 @@ try {
473
472
  .description('Set a config value. Note setting objects is not supported.')
474
473
  .argument('<key>', 'the key to set')
475
474
  .argument('<value>', 'the value')
476
- .action((key, value) => {
477
- const opt = axiumConfig.optsWithGlobals();
475
+ .action(function axium_config_set(key, value) {
476
+ const opt = this.optsWithGlobals();
478
477
  if (opt.json && !isJSON(value))
479
478
  io.exit('Invalid JSON');
480
479
  const obj = {};
@@ -500,7 +499,7 @@ try {
500
499
  console.log(opt.json ? JSON.stringify(schema, configReplacer(opt), 4) : schema);
501
500
  }
502
501
  catch (e) {
503
- io.handleError(e instanceof z.core.$ZodError ? z.prettifyError(e) : e);
502
+ io.exit(e);
504
503
  }
505
504
  });
506
505
  axiumPlugin = program.command('plugin').alias('plugins').description('Manage plugins').addOption(opts.global);
@@ -510,7 +509,7 @@ try {
510
509
  .description('List loaded plugins')
511
510
  .option('-l, --long', 'use the long listing format')
512
511
  .option('--no-versions', 'do not show plugin versions')
513
- .action((opt) => {
512
+ .action(opt => {
514
513
  if (!plugins.size) {
515
514
  console.log('No plugins loaded.');
516
515
  return;
@@ -557,7 +556,7 @@ try {
557
556
  .addOption(opts.timeout)
558
557
  .addOption(opts.check)
559
558
  .argument('<plugin>', 'the plugin to initialize')
560
- .action(async (search, opt) => {
559
+ .action(async (search) => {
561
560
  const env_3 = { stack: [], error: void 0, hasError: false };
562
561
  try {
563
562
  const plugin = _findPlugin(search);
@@ -582,7 +581,7 @@ try {
582
581
  .description('List apps added by plugins')
583
582
  .option('-l, --long', 'use the long listing format')
584
583
  .option('-b, --builtin', 'include built-in apps')
585
- .action((opt) => {
584
+ .action(opt => {
586
585
  if (!apps.size) {
587
586
  console.log('No apps.');
588
587
  return;
@@ -692,7 +691,6 @@ try {
692
691
  .command('status')
693
692
  .alias('stats')
694
693
  .description('Get information about the server')
695
- .addOption(opts.host)
696
694
  .action(async () => {
697
695
  console.log('Axium Server v' + $pkg.version);
698
696
  console.log(styleText('whiteBright', 'Debug mode:'), config.debug ? styleText('yellow', 'enabled') : 'disabled');
@@ -721,37 +719,37 @@ try {
721
719
  .addOption(new Option('-m, --method <method>', 'the method to use').choices(_portMethods).default('node-cap'))
722
720
  .option('-N, --node <path>', 'the path to the node binary')
723
721
  .action(async (action, opt) => {
724
- await restrictedPorts({ ...opt, action }).catch(io.handleError);
722
+ await restrictedPorts({ ...opt, action }).catch(io.exit);
725
723
  });
726
724
  program
727
725
  .command('init')
728
726
  .description('Install Axium server')
729
727
  .addOption(opts.force)
730
- .addOption(opts.host)
731
728
  .addOption(opts.check)
732
729
  .addOption(opts.packagesDir)
733
- .option('-s, --skip', 'Skip already initialized steps')
730
+ .option('-s, --skip', 'Skip already initialized steps', false)
734
731
  .action(async (opt) => {
735
- await db.init(opt).catch(io.handleError);
736
- await dbInitTables().catch(io.handleError);
737
- await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(io.handleError);
732
+ await db.init(opt).catch(io.exit);
733
+ await dbInitTables().catch(io.exit);
734
+ await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(io.exit);
738
735
  });
739
736
  program
740
737
  .command('serve')
741
738
  .description('Start the Axium server')
742
- .option('-p, --port <port>', 'the port to listen on')
739
+ .option('-p, --port <port>', 'the port to listen on', Number.parseInt, config.web.port)
743
740
  .option('--ssl <prefix>', 'the prefix for the cert.pem and key.pem SSL files')
744
741
  .option('-b, --build <path>', 'the path to the handler build')
745
742
  .action(async (opt) => {
743
+ if (opt.port < 1 || opt.port > 65535)
744
+ io.exit('Invalid port');
746
745
  const server = await serve({
747
746
  secure: opt.ssl ? true : config.web.secure,
748
747
  ssl_cert: opt.ssl ? join(opt.ssl, 'cert.pem') : config.web.ssl_cert,
749
748
  ssl_key: opt.ssl ? join(opt.ssl, 'key.pem') : config.web.ssl_key,
750
749
  build: opt.build ? resolve(opt.build) : config.web.build,
751
750
  });
752
- const port = !Number.isNaN(Number.parseInt(opt.port ?? 'NaN')) ? Number.parseInt(opt.port) : config.web.port;
753
- server.listen(port, () => {
754
- console.log('Server is listening on port ' + port);
751
+ server.listen(opt.port, () => {
752
+ console.log('Server is listening on port ' + opt.port);
755
753
  });
756
754
  });
757
755
  program
@@ -761,12 +759,11 @@ try {
761
759
  .addOption(new Option('-l, --list', 'list route links').conflicts('delete'))
762
760
  .option('-d, --delete', 'delete route links')
763
761
  .argument('[name...]', 'List of plugin names to operate on. If not specified, operates on all plugins and built-in routes.')
764
- .action(async function (names) {
762
+ .action(async function axium_link(names) {
765
763
  const opt = this.optsWithGlobals();
766
- if (names.length)
767
- opt.only = names;
764
+ const linkOpts = { only: names };
768
765
  if (opt.list) {
769
- for (const link of listRouteLinks(opt)) {
766
+ for (const link of listRouteLinks(linkOpts)) {
770
767
  const idText = link.id.startsWith('#') ? `(${link.id.slice(1)})` : link.id;
771
768
  const fromColor = await access(link.from)
772
769
  .then(() => 'cyanBright')
@@ -776,10 +773,10 @@ try {
776
773
  return;
777
774
  }
778
775
  if (opt.delete) {
779
- unlinkRoutes(opt);
776
+ unlinkRoutes(linkOpts);
780
777
  return;
781
778
  }
782
- linkRoutes(opt);
779
+ linkRoutes(linkOpts);
783
780
  });
784
781
  program
785
782
  .command('audit')
@@ -32,7 +32,8 @@ export declare function isRedirect(e: unknown): e is Redirect;
32
32
  */
33
33
  export declare function redirect(location: string, status?: number): never;
34
34
  export declare function json(data: object, init?: ResponseInit): Response;
35
- export declare function parseBody<const Schema extends z.ZodType, const Result extends z.infer<Schema> = z.infer<Schema>>(request: Request, schema: Schema): Promise<Result>;
35
+ export declare function parseBody<const Schema extends z.ZodType>(request: Request, schema: Schema): Promise<z.infer<Schema>>;
36
+ export declare function parseSearch<const Schema extends z.ZodType>(request: Request, schema: Schema): z.infer<Schema>;
36
37
  export declare function getToken(request: Request, sensitive?: boolean): string | undefined;
37
38
  export interface CreateSessionOptions {
38
39
  elevated?: boolean;
package/dist/requests.js CHANGED
@@ -44,6 +44,16 @@ export async function parseBody(request, schema) {
44
44
  error(400, e instanceof z.core.$ZodError ? z.prettifyError(e) : 'invalid body');
45
45
  }
46
46
  }
47
+ export function parseSearch(request, schema) {
48
+ const url = new URL(request.url);
49
+ const searchParams = Object.fromEntries(url.searchParams.entries());
50
+ try {
51
+ return schema.parse(searchParams);
52
+ }
53
+ catch (e) {
54
+ error(400, e instanceof z.core.$ZodError ? z.prettifyError(e) : 'invalid query parameters');
55
+ }
56
+ }
47
57
  export function getToken(request, sensitive = false) {
48
58
  const header_token = request.headers.get('Authorization')?.replace('Bearer ', '');
49
59
  if (header_token)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -48,9 +48,9 @@
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@axium/client": ">=0.9.5",
51
- "@axium/core": ">=0.14.0",
51
+ "@axium/core": ">=0.15.0",
52
52
  "kysely": "^0.28.0",
53
- "utilium": "^2.3.8",
53
+ "utilium": "^2.6.0",
54
54
  "zod": "^4.0.5"
55
55
  },
56
56
  "dependencies": {