@axium/server 0.2.2 → 0.3.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/cli.js CHANGED
@@ -1,24 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { Option, program } from 'commander';
2
+ import { Argument, Option, program } from 'commander';
3
3
  import { styleText } from 'node:util';
4
- import { getByString, isJSON, pick, setByString } from 'utilium';
4
+ import { getByString, isJSON, setByString } from 'utilium';
5
5
  import $pkg from '../package.json' with { type: 'json' };
6
6
  import * as config from './config.js';
7
7
  import * as db from './database.js';
8
- import { exit, output } from './io.js';
8
+ import { _portActions, _portMethods, exit, output, restrictedPorts } from './io.js';
9
9
  program
10
10
  .version($pkg.version)
11
11
  .name('axium')
12
12
  .description('Axium server CLI')
13
13
  .configureHelp({ showGlobalOptions: true })
14
- .option('-D, --debug', 'override debug mode', false)
14
+ .option('--debug', 'override debug mode')
15
+ .option('--no-debug', 'override debug mode')
15
16
  .option('-c, --config <path>', 'path to the config file');
16
- program.on('option:debug', () => config.set(pick(program.opts(), 'debug')));
17
+ program.on('option:debug', () => config.set({ debug: true }));
17
18
  program.on('option:config', () => config.load(program.opts().config));
18
19
  program.hook('preAction', function (_, action) {
19
20
  config.loadDefaults();
20
21
  const opt = action.optsWithGlobals();
21
22
  opt.force && output.warn('--force: Protections disabled.');
23
+ if (opt.debug === false)
24
+ config.set({ debug: false });
22
25
  });
23
26
  // Options shared by multiple (sub)commands
24
27
  const opts = {
@@ -36,30 +39,13 @@ const axiumDB = program
36
39
  .description('manage the database')
37
40
  .option('-t, --timeout <ms>', 'how long to wait for commands to complete.', '1000')
38
41
  .addOption(opts.host);
39
- function db_output(state, message = '') {
40
- switch (state) {
41
- case 'start':
42
- process.stdout.write(message + '... ');
43
- break;
44
- case 'log':
45
- case 'warn':
46
- process.stdout.write(styleText('yellow', message));
47
- break;
48
- case 'error':
49
- process.stdout.write(styleText('red', message));
50
- break;
51
- case 'done':
52
- console.log('done.');
53
- break;
54
- }
55
- }
56
42
  axiumDB
57
43
  .command('init')
58
44
  .description('initialize the database')
59
45
  .addOption(opts.force)
60
46
  .option('-s, --skip', 'Skip existing database and/or user')
61
47
  .action(async (opt) => {
62
- await db.init({ ...opt, output: db_output }).catch((e) => {
48
+ await db.init(opt).catch((e) => {
63
49
  if (typeof e == 'number')
64
50
  process.exit(e);
65
51
  else
@@ -95,7 +81,7 @@ axiumDB
95
81
  output.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
96
82
  process.exit(2);
97
83
  }
98
- await db.uninstall({ ...opt, output: db_output }).catch(exit);
84
+ await db.uninstall(opt).catch(exit);
99
85
  await db.database.destroy();
100
86
  });
101
87
  axiumDB
@@ -111,12 +97,11 @@ axiumDB
111
97
  output.warn(`Database has existing ${key}. Use --force if you really want to wipe the database.`);
112
98
  process.exit(2);
113
99
  }
114
- await db.wipe({ ...opt, output: db_output }).catch(exit);
100
+ await db.wipe(opt).catch(exit);
115
101
  await db.database.destroy();
116
102
  });
117
103
  const axiumConfig = program
118
104
  .command('config')
119
- .alias('conf')
120
105
  .description('manage the configuration')
121
106
  .option('-j, --json', 'values are JSON encoded')
122
107
  .option('-g, --global', 'apply to the global config');
@@ -176,17 +161,34 @@ program
176
161
  await db.database.destroy();
177
162
  console.log('Credentials authentication:', config.auth.credentials ? styleText('yellow', 'enabled') : 'disabled');
178
163
  });
164
+ program
165
+ .command('ports')
166
+ .description('Enable or disable use of restricted ports (e.g. 443)')
167
+ .addArgument(new Argument('<action>', 'The action to take').choices(_portActions))
168
+ .addOption(new Option('-m, --method <method>', 'the method to use').choices(_portMethods).default('node-cap'))
169
+ .action((action, opt) => {
170
+ try {
171
+ restrictedPorts({ ...opt, action });
172
+ }
173
+ catch (e) {
174
+ if (typeof e == 'number')
175
+ process.exit(e);
176
+ else
177
+ exit(e);
178
+ }
179
+ });
179
180
  program
180
181
  .command('init')
181
182
  .description('Install Axium server')
182
183
  .addOption(opts.force)
183
184
  .addOption(opts.host)
184
185
  .action(async (opt) => {
185
- await db.init({ ...opt, skip: opt.dbSkip, output: db_output }).catch((e) => {
186
+ await db.init({ ...opt, skip: opt.dbSkip }).catch((e) => {
186
187
  if (typeof e == 'number')
187
188
  process.exit(e);
188
189
  else
189
190
  exit(e);
190
191
  });
192
+ restrictedPorts({ method: 'node-cap', action: 'enable' });
191
193
  });
192
194
  program.parse();
package/dist/config.d.ts CHANGED
@@ -43,10 +43,10 @@ export declare const Log: z.ZodObject<{
43
43
  level: z.ZodEnum<["error", "warn", "notice", "info", "debug"]>;
44
44
  console: z.ZodBoolean;
45
45
  }, "strip", z.ZodTypeAny, {
46
- level: "debug" | "error" | "warn" | "notice" | "info";
46
+ level: "warn" | "error" | "debug" | "notice" | "info";
47
47
  console: boolean;
48
48
  }, {
49
- level: "debug" | "error" | "warn" | "notice" | "info";
49
+ level: "warn" | "error" | "debug" | "notice" | "info";
50
50
  console: boolean;
51
51
  }>;
52
52
  export type Log = z.infer<typeof Log>;
@@ -67,13 +67,13 @@ export declare const Config: z.ZodObject<{
67
67
  secret: z.ZodOptional<z.ZodString>;
68
68
  secure_cookies: z.ZodOptional<z.ZodBoolean>;
69
69
  }, "strip", z.ZodTypeAny, {
70
- credentials?: boolean | undefined;
71
70
  debug?: boolean | undefined;
71
+ credentials?: boolean | undefined;
72
72
  secret?: string | undefined;
73
73
  secure_cookies?: boolean | undefined;
74
74
  }, {
75
- credentials?: boolean | undefined;
76
75
  debug?: boolean | undefined;
76
+ credentials?: boolean | undefined;
77
77
  secret?: string | undefined;
78
78
  secure_cookies?: boolean | undefined;
79
79
  }>;
@@ -101,10 +101,10 @@ export declare const Config: z.ZodObject<{
101
101
  level: z.ZodOptional<z.ZodEnum<["error", "warn", "notice", "info", "debug"]>>;
102
102
  console: z.ZodOptional<z.ZodBoolean>;
103
103
  }, "strip", z.ZodTypeAny, {
104
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
104
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
105
105
  console?: boolean | undefined;
106
106
  }, {
107
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
107
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
108
108
  console?: boolean | undefined;
109
109
  }>;
110
110
  web: z.ZodObject<{
@@ -115,10 +115,14 @@ export declare const Config: z.ZodObject<{
115
115
  prefix?: string | undefined;
116
116
  }>;
117
117
  }, "strip", z.ZodTypeAny, {
118
+ log: {
119
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
120
+ console?: boolean | undefined;
121
+ };
118
122
  debug: boolean;
119
123
  auth: {
120
- credentials?: boolean | undefined;
121
124
  debug?: boolean | undefined;
125
+ credentials?: boolean | undefined;
122
126
  secret?: string | undefined;
123
127
  secure_cookies?: boolean | undefined;
124
128
  };
@@ -129,18 +133,18 @@ export declare const Config: z.ZodObject<{
129
133
  user?: string | undefined;
130
134
  database?: string | undefined;
131
135
  };
132
- log: {
133
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
134
- console?: boolean | undefined;
135
- };
136
136
  web: {
137
137
  prefix?: string | undefined;
138
138
  };
139
139
  }, {
140
+ log: {
141
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
142
+ console?: boolean | undefined;
143
+ };
140
144
  debug: boolean;
141
145
  auth: {
142
- credentials?: boolean | undefined;
143
146
  debug?: boolean | undefined;
147
+ credentials?: boolean | undefined;
144
148
  secret?: string | undefined;
145
149
  secure_cookies?: boolean | undefined;
146
150
  };
@@ -151,10 +155,6 @@ export declare const Config: z.ZodObject<{
151
155
  user?: string | undefined;
152
156
  database?: string | undefined;
153
157
  };
154
- log: {
155
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
156
- console?: boolean | undefined;
157
- };
158
158
  web: {
159
159
  prefix?: string | undefined;
160
160
  };
@@ -166,13 +166,13 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
166
166
  secret: z.ZodOptional<z.ZodOptional<z.ZodString>>;
167
167
  secure_cookies: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
168
168
  }, "strip", z.ZodTypeAny, {
169
- credentials?: boolean | undefined;
170
169
  debug?: boolean | undefined;
170
+ credentials?: boolean | undefined;
171
171
  secret?: string | undefined;
172
172
  secure_cookies?: boolean | undefined;
173
173
  }, {
174
- credentials?: boolean | undefined;
175
174
  debug?: boolean | undefined;
175
+ credentials?: boolean | undefined;
176
176
  secret?: string | undefined;
177
177
  secure_cookies?: boolean | undefined;
178
178
  }>>;
@@ -200,10 +200,10 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
200
200
  level: z.ZodOptional<z.ZodOptional<z.ZodEnum<["error", "warn", "notice", "info", "debug"]>>>;
201
201
  console: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
202
202
  }, "strip", z.ZodTypeAny, {
203
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
203
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
204
204
  console?: boolean | undefined;
205
205
  }, {
206
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
206
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
207
207
  console?: boolean | undefined;
208
208
  }>>;
209
209
  web: z.ZodOptional<z.ZodObject<{
@@ -216,10 +216,14 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
216
216
  }, {
217
217
  include: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
218
218
  }>, "strip", z.ZodTypeAny, {
219
+ log?: {
220
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
221
+ console?: boolean | undefined;
222
+ } | undefined;
219
223
  debug?: boolean | undefined;
220
224
  auth?: {
221
- credentials?: boolean | undefined;
222
225
  debug?: boolean | undefined;
226
+ credentials?: boolean | undefined;
223
227
  secret?: string | undefined;
224
228
  secure_cookies?: boolean | undefined;
225
229
  } | undefined;
@@ -230,19 +234,19 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
230
234
  user?: string | undefined;
231
235
  database?: string | undefined;
232
236
  } | undefined;
233
- log?: {
234
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
235
- console?: boolean | undefined;
236
- } | undefined;
237
237
  web?: {
238
238
  prefix?: string | undefined;
239
239
  } | undefined;
240
240
  include?: string[] | undefined;
241
241
  }, {
242
+ log?: {
243
+ level?: "warn" | "error" | "debug" | "notice" | "info" | undefined;
244
+ console?: boolean | undefined;
245
+ } | undefined;
242
246
  debug?: boolean | undefined;
243
247
  auth?: {
244
- credentials?: boolean | undefined;
245
248
  debug?: boolean | undefined;
249
+ credentials?: boolean | undefined;
246
250
  secret?: string | undefined;
247
251
  secure_cookies?: boolean | undefined;
248
252
  } | undefined;
@@ -253,10 +257,6 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
253
257
  user?: string | undefined;
254
258
  database?: string | undefined;
255
259
  } | undefined;
256
- log?: {
257
- level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
258
- console?: boolean | undefined;
259
- } | undefined;
260
260
  web?: {
261
261
  prefix?: string | undefined;
262
262
  } | undefined;
package/dist/config.js CHANGED
@@ -90,7 +90,7 @@ export function load(path, options = {}) {
90
90
  catch (e) {
91
91
  if (!options.optional)
92
92
  throw e;
93
- logger.debug(`Skipping config at ${path} (${e.message})`);
93
+ debug && output.debug(`Skipping config at ${path} (${e.message})`);
94
94
  return;
95
95
  }
96
96
  const config = options.strict ? File.parse(json) : json;
@@ -116,7 +116,7 @@ export function saveTo(path, changed) {
116
116
  set(changed);
117
117
  const config = files.get(path) ?? {};
118
118
  Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
119
- logger.debug(`Wrote config to ${path}`);
119
+ debug && output.debug(`Wrote config to ${path}`);
120
120
  writeFileSync(path, JSON.stringify(config));
121
121
  }
122
122
  /**
@@ -2,6 +2,7 @@ import type { AdapterAccountType as db } from '@auth/core/adapters';
2
2
  import { Kysely, type GeneratedAlways } from 'kysely';
3
3
  import type { Preferences } from './auth.js';
4
4
  import * as config from './config.js';
5
+ import { type MaybeOutput } from './io.js';
5
6
  export interface Schema {
6
7
  User: {
7
8
  id: GeneratedAlways<string>;
@@ -58,15 +59,9 @@ export interface Stats {
58
59
  }
59
60
  export declare function status(): Promise<Stats>;
60
61
  export declare function statusText(): Promise<string>;
61
- export type OpOutputState = 'done' | 'log' | 'warn' | 'error' | 'start';
62
- export type OpOutput = {
63
- (state: 'done'): void;
64
- (state: Exclude<OpOutputState, 'done'>, message: string): void;
65
- };
66
- export interface OpOptions {
62
+ export interface OpOptions extends MaybeOutput {
67
63
  timeout: number;
68
64
  force: boolean;
69
- output?: OpOutput;
70
65
  }
71
66
  export interface InitOptions extends OpOptions {
72
67
  skip: boolean;
package/dist/database.js CHANGED
@@ -55,7 +55,7 @@ import { exec } from 'node:child_process';
55
55
  import { randomBytes } from 'node:crypto';
56
56
  import pg from 'pg';
57
57
  import * as config from './config.js';
58
- import { logger } from './io.js';
58
+ import { _fixOutput } from './io.js';
59
59
  export let database;
60
60
  export function connect() {
61
61
  if (database)
@@ -87,12 +87,6 @@ export async function statusText() {
87
87
  throw typeof error == 'object' && 'message' in error ? error.message : error;
88
88
  }
89
89
  }
90
- /**
91
- * TS can't tell when we do this inline
92
- */
93
- function _fixOutput(opt) {
94
- opt.output ??= () => { };
95
- }
96
90
  /**
97
91
  * Convenience function for `sudo -u postgres psql -c "${command}"`, plus `report` coolness.
98
92
  * @internal
@@ -134,7 +128,7 @@ export async function init(opt) {
134
128
  _fixOutput(opt);
135
129
  if (!config.db.password) {
136
130
  config.save({ db: { password: randomBytes(32).toString('base64') } }, true);
137
- logger.debug('Generated password and wrote to global config');
131
+ opt.output('debug', 'Generated password and wrote to global config');
138
132
  }
139
133
  const _sql = (command, message) => execSQL(opt, command, message);
140
134
  await _sql('CREATE DATABASE axium', 'Creating database').catch(async (error) => {
package/dist/io.d.ts CHANGED
@@ -19,3 +19,41 @@ export declare const output: {
19
19
  export declare function exit(message: string | Error, code?: number): never;
20
20
  /** Convenience function for `example... [done. / error]` */
21
21
  export declare function report<T>(promise: Promise<T>, message: string, success?: string): Promise<T>;
22
+ export type OutputState = 'done' | 'log' | 'warn' | 'error' | 'start' | 'debug';
23
+ export interface Output {
24
+ (state: 'done'): void;
25
+ (state: Exclude<OutputState, 'done'>, message: string): void;
26
+ }
27
+ export interface MaybeOutput {
28
+ output?: Output | null | false;
29
+ }
30
+ export interface WithOutput {
31
+ output: Output;
32
+ }
33
+ export declare function defaultOutput(state: 'done'): void;
34
+ export declare function defaultOutput(state: Exclude<OutputState, 'done'>, message: string): void;
35
+ /**
36
+ * TS can't tell when we do this inline
37
+ * @internal
38
+ */
39
+ export declare function _fixOutput<T extends MaybeOutput>(opt: T): asserts opt is T & WithOutput;
40
+ /** @internal */
41
+ export declare const _portMethods: readonly ["node-cap"];
42
+ /** @internal */
43
+ export declare const _portActions: readonly ["enable", "disable"];
44
+ /**
45
+ * Options for working with restricted ports.
46
+ *
47
+ * Method:
48
+ * - `node-cap`: Use the `cap_net_bind_service` capability on the node binary.
49
+ */
50
+ export interface PortOptions extends MaybeOutput {
51
+ method: (typeof _portMethods)[number];
52
+ action: (typeof _portActions)[number];
53
+ }
54
+ /**
55
+ * This changes if Axium can use restricted ports (like 80 and 443) without root privileges.
56
+ * Use of these ports is needed so the origin doesn't have a port.
57
+ * If the origin has a port, passkeys do not work correctly with some password managers.
58
+ */
59
+ export declare function restrictedPorts(opt: PortOptions): void;
package/dist/io.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Logger } from 'logzen';
2
- import { mkdirSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ import * as fs from 'node:fs';
3
4
  import { homedir } from 'node:os';
4
5
  import { join } from 'node:path/posix';
5
6
  import { styleText } from 'node:util';
7
+ import { debug } from './config.js';
6
8
  /**
7
9
  * Find the Axium directory.
8
10
  * This directory includes things like config files, secrets, etc.
@@ -17,8 +19,8 @@ export function findDir(global) {
17
19
  return '.axium';
18
20
  }
19
21
  if (process.getuid?.() === 0)
20
- mkdirSync('/etc/axium', { recursive: true });
21
- mkdirSync(findDir(false), { recursive: true });
22
+ fs.mkdirSync('/etc/axium', { recursive: true });
23
+ fs.mkdirSync(findDir(false), { recursive: true });
22
24
  export const logger = new Logger({
23
25
  hideWarningStack: true,
24
26
  noGlobalConsole: true,
@@ -61,3 +63,91 @@ export async function report(promise, message, success = 'done.') {
61
63
  throw typeof error == 'object' && 'message' in error ? error.message : error;
62
64
  }
63
65
  }
66
+ export function defaultOutput(state, message = '') {
67
+ switch (state) {
68
+ case 'start':
69
+ process.stdout.write(message + '... ');
70
+ break;
71
+ case 'debug':
72
+ debug && output.debug(message);
73
+ break;
74
+ case 'log':
75
+ console.log(message);
76
+ break;
77
+ case 'warn':
78
+ process.stdout.write(styleText('yellow', message));
79
+ break;
80
+ case 'error':
81
+ process.stdout.write(styleText('red', message));
82
+ break;
83
+ case 'done':
84
+ console.log('done.');
85
+ break;
86
+ }
87
+ }
88
+ /**
89
+ * TS can't tell when we do this inline
90
+ * @internal
91
+ */
92
+ export function _fixOutput(opt) {
93
+ if (opt.output === false)
94
+ opt.output = () => { };
95
+ else
96
+ opt.output ??= defaultOutput;
97
+ }
98
+ /** @internal */
99
+ export const _portMethods = ['node-cap'];
100
+ /** @internal */
101
+ export const _portActions = ['enable', 'disable'];
102
+ /**
103
+ * This changes if Axium can use restricted ports (like 80 and 443) without root privileges.
104
+ * Use of these ports is needed so the origin doesn't have a port.
105
+ * If the origin has a port, passkeys do not work correctly with some password managers.
106
+ */
107
+ export function restrictedPorts(opt) {
108
+ _fixOutput(opt);
109
+ opt.output('start', 'Checking for root privileges');
110
+ if (process.getuid?.() != 0)
111
+ throw 'root privileges are needed to change restricted ports.';
112
+ opt.output('done');
113
+ opt.output('start', 'Checking ports method');
114
+ if (!_portMethods.includes(opt.method))
115
+ throw 'invalid';
116
+ opt.output('done');
117
+ opt.output('start', 'Checking ports action');
118
+ if (!_portActions.includes(opt.action))
119
+ throw 'invalid';
120
+ opt.output('done');
121
+ switch (opt.method) {
122
+ case 'node-cap': {
123
+ opt.output('start', 'Finding setcap');
124
+ let setcap = execSync('command -v setcap', { encoding: 'utf-8' }).trim();
125
+ if (setcap)
126
+ opt.output('done');
127
+ else {
128
+ opt.output('warn', 'not in path.');
129
+ opt.output('start', 'Checking for /usr/sbin/setcap');
130
+ fs.accessSync('/usr/sbin/setcap', fs.constants.X_OK);
131
+ setcap = '/usr/sbin/setcap';
132
+ opt.output('done');
133
+ }
134
+ opt.output('debug', 'Using setup at ' + setcap);
135
+ opt.output('start', 'Finding node');
136
+ let node = execSync('command -v node', { encoding: 'utf-8' }).trim();
137
+ if (node)
138
+ opt.output('done');
139
+ else {
140
+ opt.output('warn', 'not in path.');
141
+ opt.output('start', 'Checking for /usr/bin/node');
142
+ fs.accessSync('/usr/bin/node', fs.constants.X_OK);
143
+ node = '/usr/bin/node';
144
+ opt.output('done');
145
+ }
146
+ opt.output('debug', 'Using node at ' + node);
147
+ opt.output('start', 'Setting ports capability');
148
+ execSync(`${setcap} cap_net_bind_service=${opt.action == 'enable' ? '+' : '-'}ep ${node}`, { encoding: 'utf-8' });
149
+ opt.output('done');
150
+ break;
151
+ }
152
+ }
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "funding": {
6
6
  "type": "individual",