@axium/server 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/assets/icons/brands.svg +1493 -0
  2. package/{web/api/index.ts → dist/api/index.d.ts} +0 -2
  3. package/dist/api/index.js +5 -0
  4. package/dist/api/metadata.d.ts +1 -0
  5. package/dist/api/metadata.js +28 -0
  6. package/dist/api/passkeys.d.ts +1 -0
  7. package/dist/api/passkeys.js +50 -0
  8. package/dist/api/register.d.ts +1 -0
  9. package/dist/api/register.js +70 -0
  10. package/dist/api/session.d.ts +1 -0
  11. package/dist/api/session.js +31 -0
  12. package/dist/api/users.d.ts +1 -0
  13. package/dist/api/users.js +244 -0
  14. package/dist/apps.d.ts +0 -5
  15. package/dist/apps.js +2 -9
  16. package/dist/auth.d.ts +14 -30
  17. package/dist/auth.js +12 -18
  18. package/dist/cli.js +289 -32
  19. package/dist/config.d.ts +21 -8
  20. package/dist/config.js +46 -17
  21. package/dist/database.d.ts +12 -12
  22. package/dist/database.js +83 -84
  23. package/dist/io.d.ts +19 -20
  24. package/dist/io.js +85 -56
  25. package/dist/linking.d.ts +10 -0
  26. package/dist/linking.js +76 -0
  27. package/dist/plugins.d.ts +28 -12
  28. package/dist/plugins.js +29 -25
  29. package/dist/requests.d.ts +14 -0
  30. package/dist/requests.js +67 -0
  31. package/dist/routes.d.ts +12 -13
  32. package/dist/routes.js +21 -22
  33. package/dist/serve.d.ts +7 -0
  34. package/dist/serve.js +11 -0
  35. package/dist/state.d.ts +4 -0
  36. package/dist/state.js +22 -0
  37. package/dist/sveltekit.d.ts +8 -0
  38. package/dist/sveltekit.js +94 -0
  39. package/package.json +17 -8
  40. package/{web/routes → routes}/account/+page.svelte +6 -5
  41. package/svelte.config.js +37 -0
  42. package/web/hooks.server.ts +8 -3
  43. package/web/lib/Dialog.svelte +0 -1
  44. package/web/lib/FormDialog.svelte +0 -1
  45. package/web/lib/Upload.svelte +58 -0
  46. package/web/lib/icons/Icon.svelte +2 -7
  47. package/web/lib/icons/index.ts +6 -3
  48. package/web/lib/icons/mime.json +2 -1
  49. package/web/template.html +18 -0
  50. package/web/tsconfig.json +2 -2
  51. package/web/api/metadata.ts +0 -35
  52. package/web/api/passkeys.ts +0 -56
  53. package/web/api/readme.md +0 -1
  54. package/web/api/register.ts +0 -83
  55. package/web/api/schemas.ts +0 -22
  56. package/web/api/session.ts +0 -33
  57. package/web/api/users.ts +0 -351
  58. package/web/api/utils.ts +0 -66
  59. package/web/app.html +0 -14
  60. package/web/auth.ts +0 -8
  61. package/web/index.server.ts +0 -1
  62. package/web/index.ts +0 -1
  63. package/web/lib/auth.ts +0 -12
  64. package/web/lib/index.ts +0 -5
  65. package/web/routes/+layout.svelte +0 -6
  66. package/web/routes/[...path]/+page.server.ts +0 -13
  67. package/web/routes/[appId]/[...page]/+page.server.ts +0 -14
  68. package/web/routes/api/[...path]/+server.ts +0 -49
  69. package/web/utils.ts +0 -26
  70. /package/{web/lib → assets}/icons/light.svg +0 -0
  71. /package/{web/lib → assets}/icons/regular.svg +0 -0
  72. /package/{web/lib → assets}/icons/solid.svg +0 -0
  73. /package/{web/lib → assets}/styles.css +0 -0
  74. /package/{web/routes → routes}/_axium/default/+page.svelte +0 -0
  75. /package/{web/routes → routes}/login/+page.svelte +0 -0
  76. /package/{web/routes → routes}/logout/+page.svelte +0 -0
  77. /package/{web/routes → routes}/register/+page.svelte +0 -0
package/dist/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';
20
- }
21
- if (process.getuid?.() === 0)
22
- fs.mkdirSync('/etc/axium', { recursive: true });
23
- fs.mkdirSync(findDir(false), { recursive: true });
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
22
+ }
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,20 +43,24 @@ 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);
50
+ let timeout = 1000;
51
+ export function setCommandTimeout(value) {
52
+ timeout = value;
53
+ }
47
54
  /**
48
55
  * Run a system command with the fancy "Example... done."
49
56
  * @internal
50
57
  */
51
- export async function run(opts, message, command) {
58
+ export async function run(message, command) {
52
59
  let stderr;
53
60
  try {
54
- opts.output('start', message);
61
+ start(message);
55
62
  const { promise, resolve, reject } = Promise.withResolvers();
56
- exec(command, opts, (err, stdout, _stderr) => {
63
+ exec(command, { timeout }, (err, stdout, _stderr) => {
57
64
  stderr = _stderr.startsWith('ERROR:') ? _stderr.slice(6).trim() : _stderr;
58
65
  if (err)
59
66
  reject('[command]');
@@ -61,7 +68,7 @@ export async function run(opts, message, command) {
61
68
  resolve(stdout);
62
69
  });
63
70
  const value = await promise;
64
- opts.output('done');
71
+ done();
65
72
  return value;
66
73
  }
67
74
  catch (error) {
@@ -85,10 +92,17 @@ export function handleError(e) {
85
92
  else
86
93
  exit(e);
87
94
  }
88
- export function defaultOutput(tag, message = '') {
95
+ let _debugOutput = false;
96
+ /**
97
+ * Enable or disable debug output.
98
+ */
99
+ export function _setDebugOutput(enabled) {
100
+ _debugOutput = enabled;
101
+ }
102
+ function defaultOutput(tag, message = '') {
89
103
  switch (tag) {
90
104
  case 'debug':
91
- config.debug && output.debug(message);
105
+ _debugOutput && output.debug(message);
92
106
  break;
93
107
  case 'info':
94
108
  console.log(message);
@@ -109,15 +123,31 @@ export function defaultOutput(tag, message = '') {
109
123
  console.log(styleText('whiteBright', 'Running plugin: ' + message));
110
124
  }
111
125
  }
112
- /**
113
- * TS can't tell when we do this inline
114
- * @internal
115
- */
116
- export function _fixOutput(opt) {
117
- if (opt.output === false)
118
- opt.output = () => { };
119
- else
120
- opt.output ??= defaultOutput;
126
+ let _taggedOutput = defaultOutput;
127
+ export function useTaggedOutput(output) {
128
+ _taggedOutput = output;
129
+ }
130
+ // Shortcuts for tagged output
131
+ export function done() {
132
+ _taggedOutput?.('done');
133
+ }
134
+ export function start(message) {
135
+ _taggedOutput?.('start', message);
136
+ }
137
+ export function plugin(name) {
138
+ _taggedOutput?.('plugin', name);
139
+ }
140
+ export function debug(message) {
141
+ _taggedOutput?.('debug', message);
142
+ }
143
+ export function info(message) {
144
+ _taggedOutput?.('info', message);
145
+ }
146
+ export function warn(message) {
147
+ _taggedOutput?.('warn', message);
148
+ }
149
+ export function error(message) {
150
+ _taggedOutput?.('error', message);
121
151
  }
122
152
  /** @internal */
123
153
  export const _portMethods = ['node-cap'];
@@ -129,46 +159,45 @@ export const _portActions = ['enable', 'disable'];
129
159
  * If the origin has a port, passkeys do not work correctly with some password managers.
130
160
  */
131
161
  export async function restrictedPorts(opt) {
132
- _fixOutput(opt);
133
- opt.output('start', 'Checking for root privileges');
162
+ start('Checking for root privileges');
134
163
  if (process.getuid?.() != 0)
135
164
  throw 'root privileges are needed to change restricted ports.';
136
- opt.output('done');
137
- opt.output('start', 'Checking ports method');
165
+ done();
166
+ start('Checking ports method');
138
167
  if (!_portMethods.includes(opt.method))
139
168
  throw 'invalid';
140
- opt.output('done');
141
- opt.output('start', 'Checking ports action');
169
+ done();
170
+ start('Checking ports action');
142
171
  if (!_portActions.includes(opt.action))
143
172
  throw 'invalid';
144
- opt.output('done');
173
+ done();
145
174
  switch (opt.method) {
146
175
  case 'node-cap': {
147
- const setcap = await run(opt, 'Finding setcap', 'command -v setcap')
176
+ const setcap = await run('Finding setcap', 'command -v setcap')
148
177
  .then(e => e.trim())
149
178
  .catch(() => {
150
- opt.output('warn', 'not in path.');
151
- opt.output('start', 'Checking for /usr/sbin/setcap');
179
+ warn('not in path.');
180
+ start('Checking for /usr/sbin/setcap');
152
181
  fs.accessSync('/usr/sbin/setcap', fs.constants.X_OK);
153
- opt.output('done');
182
+ done();
154
183
  return '/usr/sbin/setcap';
155
184
  });
156
- opt.output('debug', 'Using setcap at ' + setcap);
185
+ debug('Using setcap at ' + setcap);
157
186
  let { node } = opt;
158
- node ||= await run(opt, 'Finding node', 'command -v node')
187
+ node ||= await run('Finding node', 'command -v node')
159
188
  .then(e => e.trim())
160
189
  .catch(() => {
161
- opt.output('warn', 'not in path.');
162
- opt.output('start', 'Checking for /usr/bin/node');
190
+ warn('not in path.');
191
+ start('Checking for /usr/bin/node');
163
192
  fs.accessSync('/usr/bin/node', fs.constants.X_OK);
164
- opt.output('done');
193
+ done();
165
194
  return '/usr/bin/node';
166
195
  });
167
- opt.output('start', 'Resolving real path for node');
196
+ start('Resolving real path for node');
168
197
  node = fs.realpathSync(node);
169
- opt.output('done');
170
- opt.output('debug', 'Using node at ' + node);
171
- await run(opt, 'Setting ports capability', `${setcap} cap_net_bind_service=${opt.action == 'enable' ? '+' : '-'}ep ${node}`);
198
+ done();
199
+ debug('Using node at ' + node);
200
+ await run('Setting ports capability', `${setcap} cap_net_bind_service=${opt.action == 'enable' ? '+' : '-'}ep ${node}`);
172
201
  break;
173
202
  }
174
203
  }
@@ -178,13 +207,13 @@ export async function restrictedPorts(opt) {
178
207
  * The handler will allow the parent scope to continue if a relation already exists,
179
208
  * rather than fatally exiting.
180
209
  */
181
- export function someWarnings(output, ...allowList) {
210
+ export function someWarnings(...allowList) {
182
211
  return (error) => {
183
212
  error = typeof error == 'object' && 'message' in error ? error.message : error;
184
213
  for (const [pattern, message = error] of allowList) {
185
214
  if (!pattern.test(error))
186
215
  continue;
187
- output('warn', message);
216
+ warn(message);
188
217
  return;
189
218
  }
190
219
  throw error;
@@ -0,0 +1,10 @@
1
+ export declare function listRouteLinks(): Generator<{
2
+ id: string;
3
+ from: string;
4
+ to: string;
5
+ }>;
6
+ /**
7
+ * Symlinks .svelte page routes for plugins and the server into a project's routes directory.
8
+ */
9
+ export declare function linkRoutes(): void;
10
+ export declare function unlinkRoutes(): void;
@@ -0,0 +1,76 @@
1
+ import { existsSync, symlinkSync, unlinkSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path/posix';
3
+ import config from './config.js';
4
+ import * as io from './io.js';
5
+ import { getSpecifier, plugins } from './plugins.js';
6
+ const textFor = {
7
+ builtin: 'built-in routes',
8
+ };
9
+ function info(id) {
10
+ const text = id.startsWith('#') ? textFor[id.slice(1)] : `routes for plugin: ${id}`;
11
+ const link = join(config.web.routes, `(${id.startsWith('#') ? id.slice(1) : id.replaceAll('/', '__')}`);
12
+ return [text, link];
13
+ }
14
+ export function* listRouteLinks() {
15
+ const [, link] = info('#builtin');
16
+ yield { id: '#builtin', from: resolve(import.meta.dirname, '../routes'), to: link };
17
+ for (const plugin of plugins) {
18
+ if (!plugin.routes)
19
+ continue;
20
+ const [, link] = info(plugin.name);
21
+ yield { id: plugin.name, from: join(import.meta.resolve(getSpecifier(plugin)), plugin.routes), to: link };
22
+ }
23
+ }
24
+ function createLink(id, routes) {
25
+ const [text, link] = info(id);
26
+ if (existsSync(link)) {
27
+ io.warn('already exists.');
28
+ io.start('Unlinking ' + text);
29
+ try {
30
+ unlinkSync(link);
31
+ }
32
+ catch (e) {
33
+ io.exit(e && e instanceof Error ? e.message : e.toString());
34
+ }
35
+ io.done();
36
+ io.start('Re-linking ' + text);
37
+ }
38
+ try {
39
+ symlinkSync(routes, link, 'dir');
40
+ }
41
+ catch (e) {
42
+ io.exit(e && e instanceof Error ? e.message : e.toString());
43
+ }
44
+ }
45
+ /**
46
+ * Symlinks .svelte page routes for plugins and the server into a project's routes directory.
47
+ */
48
+ export function linkRoutes() {
49
+ io.start('Linking built-in routes');
50
+ createLink('#builtin', resolve(import.meta.dirname, '../routes'));
51
+ for (const plugin of plugins) {
52
+ if (!plugin.routes)
53
+ continue;
54
+ io.start(`Linking routes for plugin: ${plugin.name}`);
55
+ createLink(plugin.name, join(import.meta.resolve(getSpecifier(plugin)), plugin.routes));
56
+ }
57
+ }
58
+ function removeLink(id) {
59
+ const [text, link] = info(id);
60
+ if (!existsSync(link))
61
+ return;
62
+ io.start('Unlinking ' + text);
63
+ try {
64
+ unlinkSync(link);
65
+ }
66
+ catch (e) {
67
+ io.exit(e && e instanceof Error ? e.message : e.toString());
68
+ }
69
+ io.done();
70
+ }
71
+ export function unlinkRoutes() {
72
+ removeLink('#builtin');
73
+ for (const plugin of plugins) {
74
+ removeLink(plugin.name);
75
+ }
76
+ }
package/dist/plugins.d.ts CHANGED
@@ -1,21 +1,37 @@
1
1
  import z from 'zod/v4';
2
- export declare const fn: z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>;
2
+ import type { Database, InitOptions, OpOptions } from './database.js';
3
+ export declare const PluginMetadata: z.ZodObject<{
4
+ name: z.ZodString;
5
+ version: z.ZodString;
6
+ description: z.ZodOptional<z.ZodString>;
7
+ routes: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$loose>;
3
9
  export declare const Plugin: z.ZodObject<{
4
- id: z.ZodString;
5
10
  name: z.ZodString;
6
11
  version: z.ZodString;
7
12
  description: z.ZodOptional<z.ZodString>;
13
+ routes: z.ZodOptional<z.ZodString>;
8
14
  statusText: z.ZodCustom<z.core.$InferInnerFunctionTypeAsync<z.core.$ZodTuple<[], null>, z.ZodString>, z.core.$InferInnerFunctionTypeAsync<z.core.$ZodTuple<[], null>, z.ZodString>>;
9
- db_init: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
10
- db_remove: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
11
- db_wipe: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
12
- db_clean: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
13
- }, z.core.$strip>;
14
- export interface Plugin extends z.infer<typeof Plugin> {
15
+ hooks: z.ZodOptional<z.ZodRecord<z.ZodUnion<[z.ZodLiteral<"remove" | "db_init" | "db_wipe" | "clean">, z.ZodNever]>, z.ZodCustom<(...args: any[]) => any, (...args: any[]) => any>>>;
16
+ }, z.core.$loose>;
17
+ declare const kSpecifier: unique symbol;
18
+ export type Plugin = z.infer<typeof Plugin>;
19
+ interface PluginInternal extends Plugin {
20
+ [kSpecifier]: string;
21
+ hooks: Hooks;
22
+ }
23
+ export interface Hooks {
24
+ db_init?: (opt: InitOptions, db: Database) => void | Promise<void>;
25
+ remove?: (opt: {
26
+ force?: boolean;
27
+ }, db: Database) => void | Promise<void>;
28
+ db_wipe?: (opt: OpOptions, db: Database) => void | Promise<void>;
29
+ clean?: (opt: Partial<OpOptions>, db: Database) => void | Promise<void>;
15
30
  }
16
- export declare const plugins: Set<Plugin>;
17
- export declare function resolvePlugin(search: string): Plugin | undefined;
18
- export declare function pluginText(plugin: Plugin): string;
31
+ export declare const plugins: Set<PluginInternal>;
32
+ export declare function resolvePlugin(search: string): PluginInternal | undefined;
33
+ export declare function pluginText(plugin: PluginInternal): string;
19
34
  export declare function loadPlugin(specifier: string): Promise<void>;
35
+ export declare function getSpecifier(plugin: PluginInternal): string;
20
36
  export declare function loadPlugins(dir: string): Promise<void>;
21
- export declare function loadDefaultPlugins(): Promise<void>;
37
+ export {};
package/dist/plugins.js CHANGED
@@ -1,52 +1,60 @@
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';
7
- export const fn = z.custom(data => typeof data === 'function');
8
- export const Plugin = z.object({
9
- id: z.string(),
6
+ import { output } from './io.js';
7
+ import { _unique } from './state.js';
8
+ export const PluginMetadata = z.looseObject({
10
9
  name: z.string(),
11
10
  version: z.string(),
12
11
  description: z.string().optional(),
12
+ routes: z.string().optional(),
13
+ });
14
+ const hookNames = ['db_init', 'remove', 'db_wipe', 'clean'];
15
+ const fn = z.custom(data => typeof data === 'function');
16
+ export const Plugin = PluginMetadata.extend({
13
17
  statusText: zAsyncFunction(z.function({ input: [], output: z.string() })),
14
- db_init: fn.optional(),
15
- db_remove: fn.optional(),
16
- db_wipe: fn.optional(),
17
- db_clean: fn.optional(),
18
+ hooks: z.partialRecord(z.literal(hookNames), fn).optional(),
18
19
  });
19
- export const plugins = new Set();
20
+ const kSpecifier = Symbol('specifier');
21
+ export const plugins = _unique('plugins', new Set());
20
22
  export function resolvePlugin(search) {
21
23
  for (const plugin of plugins) {
22
- if (plugin.name.startsWith(search) || plugin.id.startsWith(search))
24
+ if (plugin.name === search)
23
25
  return plugin;
24
26
  }
25
27
  }
26
28
  export function pluginText(plugin) {
27
29
  return [
28
30
  styleText('whiteBright', plugin.name),
29
- plugin.id,
30
31
  `Version: ${plugin.version}`,
31
32
  `Description: ${plugin.description ?? styleText('dim', '(none)')}`,
32
- `Database integration: ${[plugin.db_init, plugin.db_remove, plugin.db_wipe]
33
- .filter(Boolean)
34
- .map(fn => fn?.name.slice(3))
35
- .join(', ') || styleText('dim', '(none)')}`,
33
+ `Hooks: ${Object.keys(plugin.hooks).join(', ') || styleText('dim', '(none)')}`,
34
+ // @todo list the routes when debug output is enabled
35
+ `Routes: ${plugin.routes || styleText('dim', '(none)')}`,
36
36
  ].join('\n');
37
37
  }
38
38
  export async function loadPlugin(specifier) {
39
39
  try {
40
- 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({ hooks: {}, [kSpecifier]: specifier }, await Plugin.parseAsync(maybePlugin).catch(e => {
41
43
  throw z.prettifyError(e);
42
- });
44
+ }));
45
+ if (plugin.name.startsWith('#') || plugin.name.includes(' ')) {
46
+ throw 'Invalid plugin name. Plugin names can not start with a hash or contain spaces.';
47
+ }
43
48
  plugins.add(plugin);
44
- output.debug(`Loaded plugin: "${plugin.name}" (${plugin.id}) ${plugin.version}`);
49
+ output.debug(`Loaded plugin: ${plugin.name} ${plugin.version}`);
45
50
  }
46
51
  catch (e) {
47
- output.debug(`Failed to load plugin from ${specifier}: ${e.message || e}`);
52
+ output.debug(`Failed to load plugin from ${specifier}: ${e ? (e instanceof Error ? e.message : e.toString()) : e}`);
48
53
  }
49
54
  }
55
+ export function getSpecifier(plugin) {
56
+ return plugin[kSpecifier];
57
+ }
50
58
  export async function loadPlugins(dir) {
51
59
  fs.mkdirSync(dir, { recursive: true });
52
60
  const files = fs.readdirSync(dir);
@@ -58,7 +66,3 @@ export async function loadPlugins(dir) {
58
66
  await loadPlugin(path);
59
67
  }
60
68
  }
61
- export async function loadDefaultPlugins() {
62
- await loadPlugins(join(findDir(true), 'plugins'));
63
- await loadPlugins(join(findDir(false), 'plugins'));
64
- }
@@ -0,0 +1,14 @@
1
+ import { type User } from '@axium/core/user';
2
+ import { type HttpError, type RequestEvent } from '@sveltejs/kit';
3
+ import z from 'zod/v4';
4
+ import { type SessionAndUser, type UserInternal } from './auth.js';
5
+ export declare function parseBody<const Schema extends z.ZodType, const Result extends z.infer<Schema> = z.infer<Schema>>(event: RequestEvent, schema: Schema): Promise<Result>;
6
+ export declare function getToken(event: RequestEvent, sensitive?: boolean): string | undefined;
7
+ export interface AuthResult extends SessionAndUser {
8
+ /** The user authenticating the request. */
9
+ accessor: UserInternal;
10
+ }
11
+ export declare function checkAuth(event: RequestEvent, userId: string, sensitive?: boolean): Promise<AuthResult>;
12
+ export declare function createSessionData(userId: string, elevated?: boolean): Promise<Response>;
13
+ export declare function stripUser(user: UserInternal, includeProtected?: boolean): User;
14
+ export declare function withError(text: string, code?: number): (e: Error | HttpError) => never;
@@ -0,0 +1,67 @@
1
+ import { userProtectedFields, userPublicFields } from '@axium/core/user';
2
+ import { error, json } from '@sveltejs/kit';
3
+ import { serialize as serializeCookie } from 'cookie';
4
+ import { pick } from 'utilium';
5
+ import z from 'zod/v4';
6
+ import { createSession, getSessionAndUser, getUser } from './auth.js';
7
+ import { config } from './config.js';
8
+ export async function parseBody(event, schema) {
9
+ const contentType = event.request.headers.get('content-type');
10
+ if (!contentType || !contentType.includes('application/json'))
11
+ error(415, { message: 'Invalid content type' });
12
+ const body = await event.request.json().catch(() => error(415, { message: 'Invalid JSON' }));
13
+ try {
14
+ return schema.parse(body);
15
+ }
16
+ catch (e) {
17
+ error(400, { message: z.prettifyError(e) });
18
+ }
19
+ }
20
+ export function getToken(event, sensitive = false) {
21
+ const header_token = event.request.headers.get('Authorization')?.replace('Bearer ', '');
22
+ if (header_token)
23
+ return header_token;
24
+ if (config.debug || config.api.cookie_auth) {
25
+ return event.cookies.get(sensitive ? 'elevated_token' : 'session_token');
26
+ }
27
+ }
28
+ export async function checkAuth(event, userId, sensitive = false) {
29
+ const token = getToken(event, sensitive);
30
+ if (!token)
31
+ throw error(401, { message: 'Missing token' });
32
+ const session = await getSessionAndUser(token).catch(() => error(401, { message: 'Invalid or expired session' }));
33
+ if (session.userId !== userId) {
34
+ if (!session.user?.isAdmin)
35
+ error(403, { message: 'User ID mismatch' });
36
+ // Admins are allowed to manage other users.
37
+ const accessor = session.user;
38
+ session.user = await getUser(userId).catch(() => error(404, { message: 'Target user not found' }));
39
+ return Object.assign(session, { accessor });
40
+ }
41
+ if (!session.elevated && sensitive)
42
+ error(403, 'This token can not be used for sensitive actions');
43
+ return Object.assign(session, { accessor: session.user });
44
+ }
45
+ export async function createSessionData(userId, elevated = false) {
46
+ const { token, expires } = await createSession(userId, elevated);
47
+ const response = json({ userId, token: elevated ? '[[redacted:elevated]]' : token }, { status: 201 });
48
+ const cookies = serializeCookie(elevated ? 'elevated_token' : 'session_token', token, {
49
+ httpOnly: true,
50
+ path: '/',
51
+ expires,
52
+ secure: config.auth.secure_cookies,
53
+ sameSite: 'lax',
54
+ });
55
+ response.headers.set('Set-Cookie', cookies);
56
+ return response;
57
+ }
58
+ export function stripUser(user, includeProtected = false) {
59
+ return pick(user, ...userPublicFields, ...(includeProtected ? userProtectedFields : []));
60
+ }
61
+ export function withError(text, code = 500) {
62
+ return function (e) {
63
+ if ('body' in e)
64
+ throw e;
65
+ error(code, { message: text + (config.debug && e.message ? `: ${e.message}` : '') });
66
+ };
67
+ }
package/dist/routes.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { RequestMethod } from '@axium/core/requests';
2
- import type { LoadEvent, RequestEvent } from '@sveltejs/kit';
2
+ import type { RequestEvent } from '@sveltejs/kit';
3
3
  import type { Component } from 'svelte';
4
4
  import type z from 'zod/v4';
5
5
  type _Params = Partial<Record<string, string>>;
6
- export type EndpointHandlers<Params extends _Params = _Params> = Partial<Record<RequestMethod, (event: RequestEvent<Params>) => object | Promise<object>>>;
6
+ type MaybePromise<T> = T | Promise<T>;
7
+ export type EndpointHandlers<Params extends _Params = _Params> = Partial<Record<RequestMethod, (event: RequestEvent<Params>) => MaybePromise<object | Response>>>;
7
8
  export type RouteParamOptions = z.ZodType;
8
9
  export interface CommonRouteOptions<Params extends _Params = _Params> {
9
10
  path: string;
@@ -15,6 +16,7 @@ export interface CommonRouteOptions<Params extends _Params = _Params> {
15
16
  * A route with server-side handlers for different HTTP methods.
16
17
  */
17
18
  export interface ServerRouteOptions<Params extends _Params = _Params> extends CommonRouteOptions<Params>, EndpointHandlers<Params> {
19
+ api?: boolean;
18
20
  }
19
21
  export interface WebRouteOptions extends CommonRouteOptions {
20
22
  load?(event: RequestEvent): object | Promise<object>;
@@ -25,31 +27,28 @@ export type RouteOptions = ServerRouteOptions | WebRouteOptions;
25
27
  export interface RouteCommon {
26
28
  path: string;
27
29
  params?: Record<string, RouteParamOptions>;
28
- [kBuiltin]: boolean;
29
30
  }
30
31
  export interface ServerRoute extends RouteCommon, EndpointHandlers {
32
+ api: boolean;
31
33
  server: true;
32
34
  }
33
35
  export interface WebRoute extends RouteCommon {
34
36
  server: false;
35
- load?(event: LoadEvent): object | Promise<object>;
36
- page?: Component;
37
+ load?(event: RequestEvent): object | Promise<object>;
38
+ page: Component;
37
39
  }
38
40
  export type Route = ServerRoute | WebRoute;
39
41
  /**
40
42
  * @internal
41
43
  */
42
44
  export declare const routes: Map<string, Route>;
43
- declare const kBuiltin: unique symbol;
44
- export declare function addRoute(opt: RouteOptions, _routeMap?: Map<string, Route>): void;
45
+ export declare function addRoute(opt: RouteOptions): void;
45
46
  /**
46
47
  * Resolve a request URL into a route.
47
48
  * This handles parsing of parameters in the URL.
48
49
  */
49
- export declare function resolveRoute<T extends Route>(event: RequestEvent | LoadEvent, _routeMap?: Map<string, T>): T | undefined;
50
- /**
51
- * This function marks all existing routes as built-in.
52
- * @internal
53
- */
54
- export declare function _markDefaults(): void;
50
+ export declare function resolveRoute(event: {
51
+ url: URL;
52
+ params?: object;
53
+ }): Route | undefined;
55
54
  export {};