@axium/server 0.8.0 → 0.10.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 (72) hide show
  1. package/{web/lib → assets}/styles.css +7 -1
  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 +9 -31
  17. package/dist/auth.js +20 -34
  18. package/dist/cli.js +200 -54
  19. package/dist/config.d.ts +52 -480
  20. package/dist/config.js +89 -56
  21. package/dist/database.d.ts +3 -3
  22. package/dist/database.js +57 -24
  23. package/dist/io.d.ts +6 -4
  24. package/dist/io.js +26 -19
  25. package/dist/plugins.d.ts +5 -2
  26. package/dist/plugins.js +16 -14
  27. package/dist/requests.d.ts +11 -0
  28. package/dist/requests.js +58 -0
  29. package/dist/routes.d.ts +12 -13
  30. package/dist/routes.js +21 -22
  31. package/dist/serve.d.ts +7 -0
  32. package/dist/serve.js +11 -0
  33. package/dist/state.d.ts +4 -0
  34. package/dist/state.js +22 -0
  35. package/dist/sveltekit.d.ts +8 -0
  36. package/dist/sveltekit.js +90 -0
  37. package/package.json +18 -10
  38. package/{web/routes → routes}/account/+page.svelte +115 -48
  39. package/svelte.config.js +36 -0
  40. package/web/hooks.server.ts +15 -4
  41. package/web/lib/ClipboardCopy.svelte +42 -0
  42. package/web/lib/Dialog.svelte +0 -1
  43. package/web/lib/FormDialog.svelte +9 -2
  44. package/web/lib/icons/Icon.svelte +3 -12
  45. package/web/template.html +18 -0
  46. package/web/tsconfig.json +3 -2
  47. package/web/api/metadata.ts +0 -35
  48. package/web/api/passkeys.ts +0 -56
  49. package/web/api/readme.md +0 -1
  50. package/web/api/register.ts +0 -83
  51. package/web/api/schemas.ts +0 -22
  52. package/web/api/session.ts +0 -33
  53. package/web/api/users.ts +0 -340
  54. package/web/api/utils.ts +0 -66
  55. package/web/app.html +0 -14
  56. package/web/auth.ts +0 -8
  57. package/web/index.server.ts +0 -1
  58. package/web/index.ts +0 -1
  59. package/web/lib/auth.ts +0 -12
  60. package/web/lib/index.ts +0 -5
  61. package/web/routes/+layout.svelte +0 -6
  62. package/web/routes/[...path]/+page.server.ts +0 -13
  63. package/web/routes/[appId]/[...page]/+page.server.ts +0 -14
  64. package/web/routes/api/[...path]/+server.ts +0 -49
  65. package/web/utils.ts +0 -26
  66. /package/{web/lib → assets}/icons/light.svg +0 -0
  67. /package/{web/lib → assets}/icons/regular.svg +0 -0
  68. /package/{web/lib → assets}/icons/solid.svg +0 -0
  69. /package/{web/routes → routes}/_axium/default/+page.svelte +0 -0
  70. /package/{web/routes → routes}/login/+page.svelte +0 -0
  71. /package/{web/routes → routes}/logout/+page.svelte +0 -0
  72. /package/{web/routes → routes}/register/+page.svelte +0 -0
package/dist/config.js CHANGED
@@ -1,55 +1,17 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
1
  import { levelText } from 'logzen';
3
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
3
  import { dirname, join, resolve } from 'node:path/posix';
5
4
  import { deepAssign, omit } from 'utilium';
6
- import * as z from 'zod';
7
- import { findDir, logger, output } from './io.js';
5
+ import * as z from 'zod/v4';
6
+ import { _setDebugOutput, dirs, logger, output } from './io.js';
8
7
  import { loadPlugin } from './plugins.js';
9
- export const Schema = z
10
- .object({
11
- api: z.object({
12
- disable_metadata: z.boolean(),
13
- cookie_auth: z.boolean(),
14
- }),
15
- apps: z.object({
16
- disabled: z.array(z.string()),
17
- }),
18
- auth: z.object({
19
- credentials: z.boolean(),
20
- debug: z.boolean(),
21
- origin: z.string(),
22
- /** In minutes */
23
- passkey_probation: z.number(),
24
- rp_id: z.string(),
25
- rp_name: z.string(),
26
- secure_cookies: z.boolean(),
27
- /** In minutes */
28
- verification_timeout: z.number(),
29
- }),
30
- db: z.object({
31
- host: z.string(),
32
- port: z.number(),
33
- password: z.string(),
34
- user: z.string(),
35
- database: z.string(),
36
- }),
37
- debug: z.boolean(),
38
- log: z.object({
39
- level: z.enum(levelText),
40
- console: z.boolean(),
41
- }),
42
- web: z.object({
43
- prefix: z.string(),
44
- }),
45
- })
46
- .passthrough();
47
- export const configFiles = new Map();
8
+ import { _unique } from './state.js';
9
+ export const configFiles = _unique('configFiles', new Map());
48
10
  export function plainConfig() {
49
11
  return omit(config, Object.keys(configShortcuts));
50
12
  }
51
13
  const configShortcuts = {
52
- findPath: findConfigPath,
14
+ findPath: findConfigPaths,
53
15
  load: loadConfig,
54
16
  loadDefaults: loadDefaultConfigs,
55
17
  plain: plainConfig,
@@ -58,24 +20,23 @@ const configShortcuts = {
58
20
  set: setConfig,
59
21
  files: configFiles,
60
22
  };
61
- export const config = {
23
+ export const config = _unique('config', {
62
24
  ...configShortcuts,
63
25
  api: {
64
26
  disable_metadata: false,
65
- cookie_auth: false,
27
+ cookie_auth: true,
66
28
  },
67
29
  apps: {
68
30
  disabled: [],
69
31
  },
70
32
  auth: {
71
- credentials: false,
72
- debug: false,
73
33
  origin: 'https://test.localhost',
74
34
  passkey_probation: 60,
75
35
  rp_id: 'test.localhost',
76
36
  rp_name: 'Axium',
77
37
  secure_cookies: true,
78
38
  verification_timeout: 60,
39
+ email_verification: false,
79
40
  },
80
41
  db: {
81
42
  database: process.env.PGDATABASE || 'axium',
@@ -91,16 +52,83 @@ export const config = {
91
52
  },
92
53
  web: {
93
54
  prefix: '',
55
+ assets: '',
56
+ secure: true,
57
+ port: 443,
58
+ ssl_key: resolve(dirs[0], 'ssl_key.pem'),
59
+ ssl_cert: resolve(dirs[0], 'ssl_cert.pem'),
94
60
  },
95
- };
61
+ });
96
62
  export default config;
97
63
  // config from file
98
- export const File = Schema.deepPartial()
99
- .extend({
64
+ export const File = z
65
+ .looseObject({
66
+ api: z
67
+ .object({
68
+ disable_metadata: z.boolean(),
69
+ cookie_auth: z.boolean(),
70
+ })
71
+ .partial(),
72
+ apps: z
73
+ .object({
74
+ disabled: z.array(z.string()),
75
+ })
76
+ .partial(),
77
+ auth: z
78
+ .object({
79
+ origin: z.string(),
80
+ /** In minutes */
81
+ passkey_probation: z.number(),
82
+ rp_id: z.string(),
83
+ rp_name: z.string(),
84
+ secure_cookies: z.boolean(),
85
+ /** In minutes */
86
+ verification_timeout: z.number(),
87
+ /** Whether users can verify emails */
88
+ email_verification: z.boolean(),
89
+ })
90
+ .partial(),
91
+ db: z
92
+ .object({
93
+ host: z.string(),
94
+ port: z.number(),
95
+ password: z.string(),
96
+ user: z.string(),
97
+ database: z.string(),
98
+ })
99
+ .partial(),
100
+ debug: z.boolean(),
101
+ log: z
102
+ .object({
103
+ level: z.enum(levelText),
104
+ console: z.boolean(),
105
+ })
106
+ .partial(),
107
+ web: z
108
+ .object({
109
+ prefix: z.string(),
110
+ assets: z.string(),
111
+ secure: z.boolean(),
112
+ port: z.number().min(1).max(65535),
113
+ ssl_key: z.string(),
114
+ ssl_cert: z.string(),
115
+ })
116
+ .partial(),
100
117
  include: z.array(z.string()).optional(),
101
118
  plugins: z.array(z.string()).optional(),
102
119
  })
103
- .passthrough();
120
+ .partial();
121
+ export function addConfigDefaults(other, _target = config) {
122
+ for (const [key, value] of Object.entries(other)) {
123
+ if (!(key in _target) || _target[key] === null || _target[key] === undefined || Number.isNaN(_target[key])) {
124
+ _target[key] = value;
125
+ continue;
126
+ }
127
+ if (typeof value == 'object' && value != null && typeof _target[key] == 'object') {
128
+ addConfigDefaults(value, _target[key]);
129
+ }
130
+ }
131
+ }
104
132
  /**
105
133
  * Update the current config
106
134
  */
@@ -109,11 +137,14 @@ export function setConfig(other) {
109
137
  logger.detach(output);
110
138
  if (config.log.console)
111
139
  logger.attach(output, { output: config.log.level });
140
+ _setDebugOutput(config.debug);
112
141
  }
113
142
  /**
114
143
  * Load the config from the provided path
115
144
  */
116
145
  export async function loadConfig(path, options = {}) {
146
+ if (configFiles.has(path))
147
+ return;
117
148
  let json;
118
149
  try {
119
150
  json = JSON.parse(readFileSync(path, 'utf8'));
@@ -127,13 +158,14 @@ export async function loadConfig(path, options = {}) {
127
158
  const file = options.strict ? File.parse(json) : json;
128
159
  configFiles.set(path, file);
129
160
  setConfig(file);
161
+ output.debug('Loaded config: ' + path);
130
162
  for (const include of file.include ?? [])
131
163
  await loadConfig(join(dirname(path), include), { optional: true });
132
164
  for (const plugin of file.plugins ?? [])
133
165
  await loadPlugin(plugin.startsWith('.') ? resolve(dirname(path), plugin) : plugin);
134
166
  }
135
167
  export async function loadDefaultConfigs() {
136
- for (const path of [findConfigPath(true), findConfigPath(false)]) {
168
+ for (const path of findConfigPaths()) {
137
169
  if (!existsSync(path))
138
170
  writeFileSync(path, '{}');
139
171
  await loadConfig(path, { optional: true });
@@ -143,7 +175,7 @@ export async function loadDefaultConfigs() {
143
175
  * Update the current config and write the updated config to the appropriate file
144
176
  */
145
177
  export function saveConfig(changed, global = false) {
146
- saveConfigTo(process.env.AXIUM_CONFIG ?? findConfigPath(global), changed);
178
+ saveConfigTo(findConfigPaths().at(global ? 0 : -1), changed);
147
179
  }
148
180
  /**
149
181
  * Update the current config and write the updated config to the provided path
@@ -158,10 +190,11 @@ export function saveConfigTo(path, changed) {
158
190
  /**
159
191
  * Find the path to the config file
160
192
  */
161
- export function findConfigPath(global) {
193
+ export function findConfigPaths() {
194
+ const paths = dirs.map(dir => join(dir, 'config.json'));
162
195
  if (process.env.AXIUM_CONFIG)
163
- return process.env.AXIUM_CONFIG;
164
- return join(findDir(global), 'config.json');
196
+ paths.push(process.env.AXIUM_CONFIG);
197
+ return paths;
165
198
  }
166
199
  if (process.env.AXIUM_CONFIG)
167
200
  await loadConfig(process.env.AXIUM_CONFIG);
@@ -38,8 +38,7 @@ export interface Schema {
38
38
  transports: AuthenticatorTransportFuture[];
39
39
  };
40
40
  }
41
- export interface Database extends Kysely<Schema>, AsyncDisposable {
42
- }
41
+ export type Database = Kysely<Schema> & AsyncDisposable;
43
42
  export declare let database: Database;
44
43
  export declare function connect(): Database;
45
44
  export interface Stats {
@@ -47,7 +46,7 @@ export interface Stats {
47
46
  passkeys: number;
48
47
  sessions: number;
49
48
  }
50
- export declare function count(table: keyof Schema): Promise<number>;
49
+ export declare function count<const T extends keyof Schema>(table: T): Promise<number>;
51
50
  export declare function status(): Promise<Stats>;
52
51
  export declare function statusText(): Promise<string>;
53
52
  export interface OpOptions extends MaybeOutput {
@@ -65,6 +64,7 @@ export interface PluginShortcuts {
65
64
  }
66
65
  export declare function init(opt: InitOptions): Promise<void>;
67
66
  export declare function check(opt: OpOptions): Promise<void>;
67
+ export declare function clean(opt: Partial<OpOptions>): Promise<void>;
68
68
  /**
69
69
  * Completely remove Axium from the database.
70
70
  */
package/dist/database.js CHANGED
@@ -57,10 +57,13 @@ import pg from 'pg';
57
57
  import config from './config.js';
58
58
  import { _fixOutput, run, someWarnings } from './io.js';
59
59
  import { plugins } from './plugins.js';
60
+ const sym = Symbol.for('Axium:database');
60
61
  export let database;
61
62
  export function connect() {
62
63
  if (database)
63
64
  return database;
65
+ if (globalThis[sym])
66
+ return (database = globalThis[sym]);
64
67
  const _db = new Kysely({
65
68
  dialect: new PostgresDialect({ pool: new pg.Pool(config.db) }),
66
69
  });
@@ -69,11 +72,14 @@ export function connect() {
69
72
  await _db.destroy();
70
73
  },
71
74
  });
75
+ globalThis[sym] = database;
72
76
  return database;
73
77
  }
74
78
  export async function count(table) {
75
79
  const db = connect();
76
- return (await db.selectFrom(table).select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count;
80
+ return (await db.selectFrom(table)
81
+ .select(db.fn.countAll().as('count'))
82
+ .executeTakeFirstOrThrow()).count;
77
83
  }
78
84
  export async function status() {
79
85
  return {
@@ -286,35 +292,62 @@ export async function check(opt) {
286
292
  await result_2;
287
293
  }
288
294
  }
289
- /**
290
- * Completely remove Axium from the database.
291
- */
292
- export async function uninstall(opt) {
295
+ export async function clean(opt) {
293
296
  _fixOutput(opt);
297
+ const done = () => opt.output('done');
298
+ const now = new Date();
294
299
  const db = connect();
300
+ opt.output('start', 'Removing expired sessions');
301
+ await db.deleteFrom('sessions').where('sessions.expires', '<', now).execute().then(done);
302
+ opt.output('start', 'Removing expired verifications');
303
+ await db.deleteFrom('verifications').where('verifications.expires', '<', now).execute().then(done);
295
304
  for (const plugin of plugins) {
296
- if (!plugin.db_remove)
305
+ if (!plugin.db_clean)
297
306
  continue;
298
307
  opt.output('plugin', plugin.name);
299
- await plugin.db_remove(opt, db);
308
+ await plugin.db_clean(opt, db);
309
+ }
310
+ }
311
+ /**
312
+ * Completely remove Axium from the database.
313
+ */
314
+ export async function uninstall(opt) {
315
+ const env_3 = { stack: [], error: void 0, hasError: false };
316
+ try {
317
+ _fixOutput(opt);
318
+ const db = __addDisposableResource(env_3, connect(), true);
319
+ for (const plugin of plugins) {
320
+ if (!plugin.db_remove)
321
+ continue;
322
+ opt.output('plugin', plugin.name);
323
+ await plugin.db_remove(opt, db);
324
+ }
325
+ const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
326
+ await _sql('DROP DATABASE axium', 'Dropping database');
327
+ await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
328
+ await _sql('DROP USER axium', 'Dropping user');
329
+ await getHBA(opt)
330
+ .then(([content, writeBack]) => {
331
+ opt.output('start', 'Checking for Axium HBA configuration');
332
+ if (!content.includes(pgHba))
333
+ throw 'missing.';
334
+ opt.output('done');
335
+ opt.output('start', 'Removing Axium HBA configuration');
336
+ const newContent = content.replace(pgHba, '');
337
+ opt.output('done');
338
+ writeBack(newContent);
339
+ })
340
+ .catch(e => opt.output('warn', e));
341
+ }
342
+ catch (e_3) {
343
+ env_3.error = e_3;
344
+ env_3.hasError = true;
345
+ }
346
+ finally {
347
+ const result_3 = __disposeResources(env_3);
348
+ if (result_3)
349
+ await result_3;
300
350
  }
301
- await db.destroy();
302
- const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
303
- await _sql('DROP DATABASE axium', 'Dropping database');
304
- await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
305
- await _sql('DROP USER axium', 'Dropping user');
306
- await getHBA(opt)
307
- .then(([content, writeBack]) => {
308
- opt.output('start', 'Checking for Axium HBA configuration');
309
- if (!content.includes(pgHba))
310
- throw 'missing.';
311
- opt.output('done');
312
- opt.output('start', 'Removing Axium HBA configuration');
313
- const newContent = content.replace(pgHba, '');
314
- opt.output('done');
315
- writeBack(newContent);
316
- })
317
- .catch(e => opt.output('warn', e));
318
351
  }
319
352
  /**
320
353
  * Removes all data from tables.
package/dist/io.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { Logger } from 'logzen';
2
+ export declare const systemDir = "/etc/axium";
3
+ export declare const userDir: string;
4
+ export declare const dirs: string[];
5
+ export declare const logger: Logger;
2
6
  /**
3
- * Find the Axium directory.
4
- * This directory includes things like config files, secrets, etc.
7
+ * @internal
5
8
  */
6
- export declare function findDir(global: boolean): string;
7
- export declare const logger: Logger;
8
9
  export declare const output: {
9
10
  constructor: {
10
11
  name: string;
@@ -36,6 +37,7 @@ export interface MaybeOutput {
36
37
  export interface WithOutput {
37
38
  output: Output;
38
39
  }
40
+ export declare function _setDebugOutput(enabled: boolean): void;
39
41
  export declare function defaultOutput(tag: 'done'): void;
40
42
  export declare function defaultOutput(tag: Exclude<OutputTag, 'done'>, message: string): void;
41
43
  /**
package/dist/io.js CHANGED
@@ -2,29 +2,32 @@ import { Logger } from 'logzen';
2
2
  import { exec } from 'node:child_process';
3
3
  import * as fs from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { join } from 'node:path/posix';
5
+ import { dirname, join, resolve } from 'node:path/posix';
6
6
  import { styleText } from 'node:util';
7
- import config from './config.js';
8
- /**
9
- * Find the Axium directory.
10
- * This directory includes things like config files, secrets, etc.
11
- */
12
- export function findDir(global) {
13
- if (process.env.AXIUM_DIR)
14
- return process.env.AXIUM_DIR;
15
- if (global && process.getuid?.() === 0)
16
- return '/etc/axium';
17
- if (global)
18
- return join(homedir(), '.axium');
19
- return '.axium';
7
+ import { _unique } from './state.js';
8
+ export const systemDir = '/etc/axium';
9
+ export const userDir = join(homedir(), '.axium');
10
+ export const dirs = _unique('dirs', [systemDir, userDir]);
11
+ for (let dir = resolve(process.cwd()); dir !== '/'; dir = dirname(dir)) {
12
+ if (fs.existsSync(join(dir, '.axium')))
13
+ dirs.push(join(dir, '.axium'));
14
+ }
15
+ if (process.env.AXIUM_DIR)
16
+ dirs.push(process.env.AXIUM_DIR);
17
+ try {
18
+ fs.mkdirSync(systemDir, { recursive: true });
19
+ }
20
+ catch {
21
+ // Missing permissions
20
22
  }
21
- if (process.getuid?.() === 0)
22
- fs.mkdirSync('/etc/axium', { recursive: true });
23
- fs.mkdirSync(findDir(false), { recursive: true });
23
+ fs.mkdirSync(userDir, { recursive: true });
24
24
  export const logger = new Logger({
25
25
  hideWarningStack: true,
26
26
  noGlobalConsole: true,
27
27
  });
28
+ /**
29
+ * @internal
30
+ */
28
31
  export const output = {
29
32
  constructor: { name: 'Console' },
30
33
  error(message) {
@@ -40,7 +43,7 @@ export const output = {
40
43
  console.log(message);
41
44
  },
42
45
  debug(message) {
43
- console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
46
+ _debugOutput && console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
44
47
  },
45
48
  };
46
49
  logger.attach(output);
@@ -85,10 +88,14 @@ export function handleError(e) {
85
88
  else
86
89
  exit(e);
87
90
  }
91
+ let _debugOutput = false;
92
+ export function _setDebugOutput(enabled) {
93
+ _debugOutput = enabled;
94
+ }
88
95
  export function defaultOutput(tag, message = '') {
89
96
  switch (tag) {
90
97
  case 'debug':
91
- config.debug && output.debug(message);
98
+ _debugOutput && output.debug(message);
92
99
  break;
93
100
  case 'info':
94
101
  console.log(message);
package/dist/plugins.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import z from 'zod/v4';
2
2
  export declare const fn: z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>;
3
3
  export declare const Plugin: z.ZodObject<{
4
- id: z.ZodString;
5
4
  name: z.ZodString;
6
5
  version: z.ZodString;
7
6
  description: z.ZodOptional<z.ZodString>;
@@ -9,12 +8,16 @@ export declare const Plugin: z.ZodObject<{
9
8
  db_init: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
10
9
  db_remove: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
11
10
  db_wipe: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
11
+ db_clean: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
12
12
  }, z.core.$strip>;
13
+ declare const kSpecifier: unique symbol;
13
14
  export interface Plugin extends z.infer<typeof Plugin> {
15
+ [kSpecifier]: string;
14
16
  }
15
17
  export declare const plugins: Set<Plugin>;
16
18
  export declare function resolvePlugin(search: string): Plugin | undefined;
17
19
  export declare function pluginText(plugin: Plugin): string;
18
20
  export declare function loadPlugin(specifier: string): Promise<void>;
21
+ export declare function getSpecifier(plugin: Plugin): string;
19
22
  export declare function loadPlugins(dir: string): Promise<void>;
20
- export declare function loadDefaultPlugins(): Promise<void>;
23
+ export {};
package/dist/plugins.js CHANGED
@@ -1,12 +1,12 @@
1
+ import { zAsyncFunction } from '@axium/core/schemas';
1
2
  import * as fs from 'node:fs';
2
- import { join, resolve } from 'node:path/posix';
3
+ import { resolve } from 'node:path/posix';
3
4
  import { styleText } from 'node:util';
4
5
  import z from 'zod/v4';
5
- import { findDir, output } from './io.js';
6
- import { zAsyncFunction } from '@axium/core/schemas';
6
+ import { output } from './io.js';
7
+ import { _unique } from './state.js';
7
8
  export const fn = z.custom(data => typeof data === 'function');
8
9
  export const Plugin = z.object({
9
- id: z.string(),
10
10
  name: z.string(),
11
11
  version: z.string(),
12
12
  description: z.string().optional(),
@@ -14,18 +14,19 @@ export const Plugin = z.object({
14
14
  db_init: fn.optional(),
15
15
  db_remove: fn.optional(),
16
16
  db_wipe: fn.optional(),
17
+ db_clean: fn.optional(),
17
18
  });
18
- export const plugins = new Set();
19
+ const kSpecifier = Symbol('specifier');
20
+ export const plugins = _unique('plugins', new Set());
19
21
  export function resolvePlugin(search) {
20
22
  for (const plugin of plugins) {
21
- if (plugin.name.startsWith(search) || plugin.id.startsWith(search))
23
+ if (plugin.name.startsWith(search))
22
24
  return plugin;
23
25
  }
24
26
  }
25
27
  export function pluginText(plugin) {
26
28
  return [
27
29
  styleText('whiteBright', plugin.name),
28
- plugin.id,
29
30
  `Version: ${plugin.version}`,
30
31
  `Description: ${plugin.description ?? styleText('dim', '(none)')}`,
31
32
  `Database integration: ${[plugin.db_init, plugin.db_remove, plugin.db_wipe]
@@ -36,16 +37,21 @@ export function pluginText(plugin) {
36
37
  }
37
38
  export async function loadPlugin(specifier) {
38
39
  try {
39
- const plugin = await Plugin.parseAsync(await import(/* @vite-ignore */ specifier)).catch(e => {
40
+ const imported = await import(/* @vite-ignore */ specifier);
41
+ const maybePlugin = 'default' in imported ? imported.default : imported;
42
+ const plugin = Object.assign(await Plugin.parseAsync(maybePlugin).catch(e => {
40
43
  throw z.prettifyError(e);
41
- });
44
+ }), { [kSpecifier]: specifier });
42
45
  plugins.add(plugin);
43
- output.debug(`Loaded plugin: "${plugin.name}" (${plugin.id}) ${plugin.version}`);
46
+ output.debug(`Loaded plugin: ${plugin.name} ${plugin.version}`);
44
47
  }
45
48
  catch (e) {
46
49
  output.debug(`Failed to load plugin from ${specifier}: ${e.message || e}`);
47
50
  }
48
51
  }
52
+ export function getSpecifier(plugin) {
53
+ return plugin[kSpecifier];
54
+ }
49
55
  export async function loadPlugins(dir) {
50
56
  fs.mkdirSync(dir, { recursive: true });
51
57
  const files = fs.readdirSync(dir);
@@ -57,7 +63,3 @@ export async function loadPlugins(dir) {
57
63
  await loadPlugin(path);
58
64
  }
59
65
  }
60
- export async function loadDefaultPlugins() {
61
- await loadPlugins(join(findDir(true), 'plugins'));
62
- await loadPlugins(join(findDir(false), 'plugins'));
63
- }
@@ -0,0 +1,11 @@
1
+ import type { NewSessionResponse } from '@axium/core/api';
2
+ import { type User } from '@axium/core/user';
3
+ import { type HttpError, type RequestEvent } from '@sveltejs/kit';
4
+ import z from 'zod/v4';
5
+ import { type SessionAndUser, type UserInternal } from './auth.js';
6
+ export declare function parseBody<const Schema extends z.ZodType, const Result extends z.infer<Schema> = z.infer<Schema>>(event: RequestEvent, schema: Schema): Promise<Result>;
7
+ export declare function getToken(event: RequestEvent, sensitive?: boolean): string | undefined;
8
+ export declare function checkAuth(event: RequestEvent, userId: string, sensitive?: boolean): Promise<SessionAndUser>;
9
+ export declare function createSessionData(event: RequestEvent, userId: string, elevated?: boolean): Promise<NewSessionResponse>;
10
+ export declare function stripUser(user: UserInternal, includeProtected?: boolean): User;
11
+ export declare function withError(text: string, code?: number): (e: Error | HttpError) => never;
@@ -0,0 +1,58 @@
1
+ import { userProtectedFields, userPublicFields } from '@axium/core/user';
2
+ import { error } from '@sveltejs/kit';
3
+ import { pick } from 'utilium';
4
+ import z from 'zod/v4';
5
+ import { createSession, getSessionAndUser } from './auth.js';
6
+ import { config } from './config.js';
7
+ export async function parseBody(event, schema) {
8
+ const contentType = event.request.headers.get('content-type');
9
+ if (!contentType || !contentType.includes('application/json'))
10
+ error(415, { message: 'Invalid content type' });
11
+ const body = await event.request.json().catch(() => error(415, { message: 'Invalid JSON' }));
12
+ try {
13
+ return schema.parse(body);
14
+ }
15
+ catch (e) {
16
+ error(400, { message: z.prettifyError(e) });
17
+ }
18
+ }
19
+ export function getToken(event, sensitive = false) {
20
+ const header_token = event.request.headers.get('Authorization')?.replace('Bearer ', '');
21
+ if (header_token)
22
+ return header_token;
23
+ if (config.debug || config.api.cookie_auth) {
24
+ return event.cookies.get(sensitive ? 'elevated_token' : 'session_token');
25
+ }
26
+ }
27
+ export async function checkAuth(event, userId, sensitive = false) {
28
+ const token = getToken(event, sensitive);
29
+ if (!token)
30
+ throw error(401, { message: 'Missing token' });
31
+ const session = await getSessionAndUser(token).catch(() => error(401, { message: 'Invalid or expired session' }));
32
+ if (session.user?.id !== userId /* && !user.isAdmin */)
33
+ error(403, { message: 'User ID mismatch' });
34
+ if (!session.elevated && sensitive)
35
+ error(403, 'This token can not be used for sensitive actions');
36
+ return session;
37
+ }
38
+ export async function createSessionData(event, userId, elevated = false) {
39
+ const { token } = await createSession(userId, elevated);
40
+ if (elevated) {
41
+ event.cookies.set('elevated_token', token, { httpOnly: true, path: '/', expires: new Date(Date.now() + 10 * 60_000) });
42
+ return { userId, token: '[[redacted:elevated]]' };
43
+ }
44
+ else {
45
+ event.cookies.set('session_token', token, { httpOnly: config.auth.secure_cookies, path: '/' });
46
+ return { userId, token };
47
+ }
48
+ }
49
+ export function stripUser(user, includeProtected = false) {
50
+ return pick(user, ...userPublicFields, ...(includeProtected ? userProtectedFields : []));
51
+ }
52
+ export function withError(text, code = 500) {
53
+ return function (e) {
54
+ if ('body' in e)
55
+ throw e;
56
+ error(code, { message: text + (config.debug && e.message ? `: ${e.message}` : '') });
57
+ };
58
+ }