@axium/server 0.9.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 (70) hide show
  1. package/{web/api/index.ts → dist/api/index.d.ts} +0 -2
  2. package/dist/api/index.js +5 -0
  3. package/dist/api/metadata.d.ts +1 -0
  4. package/dist/api/metadata.js +28 -0
  5. package/dist/api/passkeys.d.ts +1 -0
  6. package/dist/api/passkeys.js +50 -0
  7. package/dist/api/register.d.ts +1 -0
  8. package/dist/api/register.js +70 -0
  9. package/dist/api/session.d.ts +1 -0
  10. package/dist/api/session.js +31 -0
  11. package/dist/api/users.d.ts +1 -0
  12. package/dist/api/users.js +244 -0
  13. package/dist/apps.d.ts +0 -5
  14. package/dist/apps.js +2 -9
  15. package/dist/auth.d.ts +9 -21
  16. package/dist/auth.js +12 -18
  17. package/dist/cli.js +65 -26
  18. package/dist/config.d.ts +15 -8
  19. package/dist/config.js +38 -15
  20. package/dist/database.js +4 -0
  21. package/dist/io.d.ts +6 -4
  22. package/dist/io.js +26 -19
  23. package/dist/plugins.d.ts +4 -2
  24. package/dist/plugins.js +15 -14
  25. package/dist/requests.d.ts +11 -0
  26. package/dist/requests.js +58 -0
  27. package/dist/routes.d.ts +12 -13
  28. package/dist/routes.js +21 -22
  29. package/dist/serve.d.ts +7 -0
  30. package/dist/serve.js +11 -0
  31. package/dist/state.d.ts +4 -0
  32. package/dist/state.js +22 -0
  33. package/dist/sveltekit.d.ts +8 -0
  34. package/dist/sveltekit.js +90 -0
  35. package/package.json +10 -5
  36. package/svelte.config.js +36 -0
  37. package/web/hooks.server.ts +8 -3
  38. package/web/lib/Dialog.svelte +0 -1
  39. package/web/lib/FormDialog.svelte +0 -1
  40. package/web/lib/icons/Icon.svelte +2 -7
  41. package/web/template.html +18 -0
  42. package/web/tsconfig.json +3 -2
  43. package/web/api/metadata.ts +0 -35
  44. package/web/api/passkeys.ts +0 -56
  45. package/web/api/readme.md +0 -1
  46. package/web/api/register.ts +0 -83
  47. package/web/api/schemas.ts +0 -22
  48. package/web/api/session.ts +0 -33
  49. package/web/api/users.ts +0 -351
  50. package/web/api/utils.ts +0 -66
  51. package/web/app.html +0 -14
  52. package/web/auth.ts +0 -8
  53. package/web/index.server.ts +0 -1
  54. package/web/index.ts +0 -1
  55. package/web/lib/auth.ts +0 -12
  56. package/web/lib/index.ts +0 -5
  57. package/web/routes/+layout.svelte +0 -6
  58. package/web/routes/[...path]/+page.server.ts +0 -13
  59. package/web/routes/[appId]/[...page]/+page.server.ts +0 -14
  60. package/web/routes/api/[...path]/+server.ts +0 -49
  61. package/web/utils.ts +0 -26
  62. /package/{web/lib → assets}/icons/light.svg +0 -0
  63. /package/{web/lib → assets}/icons/regular.svg +0 -0
  64. /package/{web/lib → assets}/icons/solid.svg +0 -0
  65. /package/{web/lib → assets}/styles.css +0 -0
  66. /package/{web/routes → routes}/_axium/default/+page.svelte +0 -0
  67. /package/{web/routes → routes}/account/+page.svelte +0 -0
  68. /package/{web/routes → routes}/login/+page.svelte +0 -0
  69. /package/{web/routes → routes}/logout/+page.svelte +0 -0
  70. /package/{web/routes → routes}/register/+page.svelte +0 -0
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 {};
package/dist/routes.js CHANGED
@@ -1,39 +1,47 @@
1
+ import { apps } from './apps.js';
2
+ import config from './config.js';
3
+ import { output } from './io.js';
4
+ import { _unique } from './state.js';
1
5
  /**
2
6
  * @internal
3
7
  */
4
- export const routes = new Map();
5
- const kBuiltin = Symbol('kBuiltin');
6
- export function addRoute(opt, _routeMap = routes) {
7
- const route = { ...opt, server: !('page' in opt), [kBuiltin]: false };
8
+ export const routes = _unique('routes', new Map());
9
+ export function addRoute(opt) {
10
+ const route = { ...opt, server: !('page' in opt) };
8
11
  if (!route.path.startsWith('/')) {
9
12
  throw new Error(`Route path must start with a slash: ${route.path}`);
10
13
  }
11
- if (route.path.startsWith('/api/') && !route.server) {
14
+ if (route.path.startsWith('/api/'))
15
+ route.api = true;
16
+ if (route.api && !route.server)
12
17
  throw new Error(`API routes cannot have a client page: ${route.path}`);
13
- }
14
- _routeMap.set(route.path, route);
18
+ routes.set(route.path, route);
19
+ output.debug('Added route: ' + route.path);
15
20
  }
16
21
  /**
17
22
  * Resolve a request URL into a route.
18
23
  * This handles parsing of parameters in the URL.
19
24
  */
20
- export function resolveRoute(event, _routeMap = routes) {
25
+ export function resolveRoute(event) {
21
26
  const { pathname } = event.url;
22
- if (_routeMap.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
23
- return _routeMap.get(pathname);
27
+ if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
28
+ return routes.get(pathname);
24
29
  // Otherwise we must have a parameterized route
25
- routes: for (const route of _routeMap.values()) {
30
+ _routes: for (const route of routes.values()) {
26
31
  const params = {};
27
32
  // Split the path and route into parts, zipped together
28
33
  const pathParts = pathname.split('/').filter(Boolean);
34
+ // Skips routes in disabled apps
35
+ if (apps.has(pathParts[0]) && config.apps.disabled.includes(pathParts[0]))
36
+ continue;
29
37
  for (const routePart of route.path.split('/').filter(Boolean)) {
30
38
  const pathPart = pathParts.shift();
31
39
  if (!pathPart)
32
- continue routes;
40
+ continue _routes;
33
41
  if (pathPart == routePart)
34
42
  continue;
35
43
  if (!routePart.startsWith(':'))
36
- continue routes;
44
+ continue _routes;
37
45
  params[routePart.slice(1)] = pathPart;
38
46
  }
39
47
  // we didn't find a match, since an exact match would have been found already
@@ -43,12 +51,3 @@ export function resolveRoute(event, _routeMap = routes) {
43
51
  return route;
44
52
  }
45
53
  }
46
- /**
47
- * This function marks all existing routes as built-in.
48
- * @internal
49
- */
50
- export function _markDefaults() {
51
- for (const route of routes.values()) {
52
- route[kBuiltin] = true;
53
- }
54
- }
@@ -0,0 +1,7 @@
1
+ import type { Server } from 'node:http';
2
+ export interface ServeOptions {
3
+ secure: boolean;
4
+ ssl_key: string;
5
+ ssl_cert: string;
6
+ }
7
+ export declare function serve(opt: Partial<ServeOptions>): Promise<Server>;
package/dist/serve.js ADDED
@@ -0,0 +1,11 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createServer } from 'node:http';
3
+ import { createServer as createSecureServer } from 'node:https';
4
+ import config from './config.js';
5
+ const _handlerPath = '../build/handler.js';
6
+ export async function serve(opt) {
7
+ const { handler } = await import(_handlerPath);
8
+ if (!opt.secure && !config.web.secure)
9
+ return createServer(handler);
10
+ return createSecureServer({ key: readFileSync(opt.ssl_key || config.web.ssl_key), cert: readFileSync(opt.ssl_cert || config.web.ssl_cert) }, handler);
11
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Prevent duplicate shared state.
3
+ */
4
+ export declare function _unique<T>(id: string, value: T): T;
package/dist/state.js ADDED
@@ -0,0 +1,22 @@
1
+ import { styleText } from 'node:util';
2
+ const sym = Symbol.for('Axium:state');
3
+ globalThis[sym] ||= Object.create({ _errored: false });
4
+ /**
5
+ * Prevent duplicate shared state.
6
+ */
7
+ export function _unique(id, value) {
8
+ const state = globalThis[sym];
9
+ const _err = new Error();
10
+ Error.captureStackTrace(_err, _unique);
11
+ const stack = _err.stack.slice(6);
12
+ if (!(id in state)) {
13
+ state[id] = { value, stack };
14
+ return value;
15
+ }
16
+ if (!state._errored) {
17
+ console.error(styleText('red', 'Duplicate Axium server state! You might have multiple instances of the same module loaded.'));
18
+ state._errored = true;
19
+ }
20
+ console.warn(styleText('yellow', `Mitigating duplicate state! (${id})\n${stack}\nFrom original\n${state[id].stack}`));
21
+ return state[id].value;
22
+ }
@@ -0,0 +1,8 @@
1
+ import type { RequestEvent, ResolveOptions } from '@sveltejs/kit';
2
+ /**
3
+ * @internal
4
+ */
5
+ export declare function handle({ event, resolve, }: {
6
+ event: RequestEvent;
7
+ resolve: (event: RequestEvent, opts?: ResolveOptions) => Promise<Response>;
8
+ }): Promise<Response>;
@@ -0,0 +1,90 @@
1
+ import { error, json, redirect } from '@sveltejs/kit';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path/posix';
4
+ import { render } from 'svelte/server';
5
+ import z from 'zod/v4';
6
+ import { config } from './config.js';
7
+ import { resolveRoute } from './routes.js';
8
+ async function handleAPIRequest(event, route) {
9
+ const method = event.request.method;
10
+ const _warnings = [];
11
+ if (route.api && !event.request.headers.get('Accept')?.includes('application/json')) {
12
+ _warnings.push('Only application/json is supported');
13
+ event.request.headers.set('Accept', 'application/json');
14
+ }
15
+ for (const [key, type] of Object.entries(route.params || {})) {
16
+ if (!type)
17
+ continue;
18
+ try {
19
+ event.params[key] = type.parse(event.params[key]);
20
+ }
21
+ catch (e) {
22
+ error(400, `Invalid parameter: ${z.prettifyError(e)}`);
23
+ }
24
+ }
25
+ if (typeof route[method] != 'function')
26
+ error(405, `Method ${method} not allowed for ${route.path}`);
27
+ const result = await route[method](event);
28
+ if (result instanceof Response)
29
+ return result;
30
+ result._warnings ||= [];
31
+ result._warnings.push(..._warnings);
32
+ return json(result);
33
+ }
34
+ function handleError(e) {
35
+ if ('body' in e)
36
+ return json(e.body, { status: e.status });
37
+ console.error(e);
38
+ return json({ message: 'Internal Error' + (config.debug ? ': ' + e.message : '') }, { status: 500 });
39
+ }
40
+ const templatePath = join(import.meta.dirname, '../web/template.html');
41
+ const template = readFileSync(templatePath, 'utf-8');
42
+ function fillTemplate({ head, body }, env = {}, nonce = '') {
43
+ return (template
44
+ .replace('%sveltekit.head%', head)
45
+ .replace('%sveltekit.body%', body)
46
+ .replace(/%sveltekit\.assets%/g, config.web.assets)
47
+ // Unused for now.
48
+ .replace(/%sveltekit\.nonce%/g, nonce)
49
+ .replace(/%sveltekit\.env\.([^%]+)%/g, (_match, capture) => env[capture] ?? ''));
50
+ }
51
+ /**
52
+ * @internal
53
+ */
54
+ export async function handle({ event, resolve, }) {
55
+ const route = resolveRoute(event);
56
+ if (!route && event.url.pathname === '/')
57
+ redirect(303, '/_axium/default');
58
+ if (config.debug)
59
+ console.log(event.request.method.padEnd(7), route ? route.path : event.url.pathname);
60
+ if (!route)
61
+ return await resolve(event).catch(handleError);
62
+ if (route.server == true) {
63
+ if (route.api)
64
+ return await handleAPIRequest(event, route).catch(handleError);
65
+ const run = route[event.request.method];
66
+ if (typeof run !== 'function') {
67
+ error(405, `Method ${event.request.method} not allowed for ${route.path}`);
68
+ }
69
+ try {
70
+ const result = await run(event);
71
+ if (result instanceof Response)
72
+ return result;
73
+ return json(result);
74
+ }
75
+ catch (e) {
76
+ return handleError(e);
77
+ }
78
+ }
79
+ const data = await route.load?.(event);
80
+ const body = fillTemplate(render(route.page));
81
+ return new Response(body, {
82
+ headers: {
83
+ 'Content-Type': 'text/html; charset=utf-8',
84
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
85
+ Pragma: 'no-cache',
86
+ Expires: '0',
87
+ },
88
+ status: 200,
89
+ });
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -20,12 +20,16 @@
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
22
  "./*": "./dist/*.js",
23
- "./web": "./web/index.js",
24
- "./web/*": "./web/*"
23
+ "./web/*": "./web/*",
24
+ "./$routes": "./routes",
25
+ "./svelte.config.js": "./svelte.config.js"
25
26
  },
26
27
  "files": [
28
+ "assets",
27
29
  "dist",
28
- "web"
30
+ "routes",
31
+ "web",
32
+ "svelte.config.js"
29
33
  ],
30
34
  "bin": {
31
35
  "axium": "dist/cli.js"
@@ -34,12 +38,13 @@
34
38
  "build": "tsc"
35
39
  },
36
40
  "peerDependencies": {
37
- "@axium/core": ">=0.3.0",
38
41
  "@axium/client": ">=0.0.2",
42
+ "@axium/core": ">=0.3.0",
39
43
  "utilium": "^2.3.8",
40
44
  "zod": "^3.25.61"
41
45
  },
42
46
  "dependencies": {
47
+ "@axium/server": "file:.",
43
48
  "@simplewebauthn/server": "^13.1.1",
44
49
  "@sveltejs/kit": "^2.20.2",
45
50
  "@types/pg": "^8.11.11",
@@ -0,0 +1,36 @@
1
+ import node from '@sveltejs/adapter-node';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+ import { join } from 'node:path/posix';
4
+
5
+ /**
6
+ * Paths relative to the directory of this file.
7
+ * This allows this file to be imported from other projects and still resolve to the correct paths.
8
+ */
9
+ const fixed = p => join(import.meta.dirname, p);
10
+
11
+ /** @type {import('@sveltejs/kit').Config} */
12
+ export default {
13
+ compilerOptions: {
14
+ runes: true,
15
+ },
16
+ preprocess: vitePreprocess({ script: true }),
17
+ vitePlugin: {
18
+ exclude: '@axium/server/**',
19
+ },
20
+ kit: {
21
+ adapter: node(),
22
+ alias: {
23
+ $stores: fixed('web/stores'),
24
+ $lib: fixed('web/lib'),
25
+ },
26
+ files: {
27
+ routes: 'routes',
28
+ lib: fixed('web/lib'),
29
+ assets: fixed('assets'),
30
+ appTemplate: fixed('web/template.html'),
31
+ hooks: {
32
+ server: fixed('web/hooks.server.ts'),
33
+ },
34
+ },
35
+ },
36
+ };
@@ -1,12 +1,17 @@
1
+ import '@axium/server/api/index';
1
2
  import { loadDefaultConfigs } from '@axium/server/config';
2
3
  import { clean, database } from '@axium/server/database';
3
- import { _markDefaults } from '@axium/server/routes';
4
- import './api/index.js';
4
+ import { dirs, logger } from '@axium/server/io';
5
+ import { allLogLevels } from 'logzen';
6
+ import { createWriteStream } from 'node:fs';
7
+ import { join } from 'node:path/posix';
5
8
 
6
- _markDefaults();
9
+ logger.attach(createWriteStream(join(dirs.at(-1), 'server.log')), { output: allLogLevels });
7
10
  await loadDefaultConfigs();
8
11
  await clean({});
9
12
 
10
13
  process.on('beforeExit', async () => {
11
14
  await database.destroy();
12
15
  });
16
+
17
+ export { handle } from '@axium/server/sveltekit';
@@ -1,6 +1,5 @@
1
1
  <script>
2
2
  let { children, dialog = $bindable(), ...rest } = $props();
3
- import './styles.css';
4
3
  </script>
5
4
 
6
5
  <dialog bind:this={dialog} {...rest}>
@@ -2,7 +2,6 @@
2
2
  import { goto } from '$app/navigation';
3
3
  import { page } from '$app/state';
4
4
  import Dialog from './Dialog.svelte';
5
- import './styles.css';
6
5
 
7
6
  function resolveRedirectAfter() {
8
7
  const maybe = page.url.searchParams.get('after');
@@ -1,16 +1,11 @@
1
1
  <script lang="ts">
2
- import light from './light.svg';
3
- import solid from './solid.svg';
4
- import regular from './regular.svg';
5
- const urls = { light, solid, regular };
6
2
  const { i } = $props();
7
-
8
3
  const [style, id] = $derived(i.includes('/') ? i.split('/') : ['solid', i]);
9
- const url = $derived(urls[style]);
4
+ const href = $derived(`/icons/${style}.svg#${id}`);
10
5
  </script>
11
6
 
12
7
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em">
13
- <use href="{url}#{id}" />
8
+ <use {href} />
14
9
  </svg>
15
10
 
16
11
  <style>
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="%sveltekit.assets%/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <meta name="color-scheme" content="dark light" />
8
+ <link rel="stylesheet" href="%sveltekit.assets%/styles.css" />
9
+ <link rel="preload" href="%sveltekit.assets%/icons/light.svg" as="image" type="image/svg+xml" />
10
+ <link rel="preload" href="%sveltekit.assets%/icons/regular.svg" as="image" type="image/svg+xml" />
11
+ <link rel="preload" href="%sveltekit.assets%/icons/solid.svg" as="image" type="image/svg+xml" />
12
+ %sveltekit.head%
13
+ </head>
14
+
15
+ <body>
16
+ <div style="display: contents">%sveltekit.body%</div>
17
+ </body>
18
+ </html>
package/web/tsconfig.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
2
3
  "extends": "../.svelte-kit/tsconfig.json",
3
4
  "compilerOptions": {
4
- "target": "ES2021",
5
+ "target": "ES2023",
5
6
  "lib": ["ESNext", "DOM", "DOM.Iterable"]
6
7
  },
7
- "include": ["**/*.ts", "**/*.svelte"]
8
+ "include": ["./**/*", "../lib"]
8
9
  }
@@ -1,35 +0,0 @@
1
- import type { Result } from '@axium/core/api';
2
- import { requestMethods } from '@axium/core/requests';
3
- import { config } from '@axium/server/config';
4
- import { plugins } from '@axium/server/plugins';
5
- import { addRoute, routes } from '@axium/server/routes';
6
- import { error } from '@sveltejs/kit';
7
- import pkg from '../../package.json' with { type: 'json' };
8
-
9
- addRoute({
10
- path: '/api/metadata',
11
- async GET(): Result<'GET', 'metadata'> {
12
- if (config.api.disable_metadata) {
13
- error(401, { message: 'API metadata is disabled' });
14
- }
15
-
16
- return {
17
- version: pkg.version,
18
- routes: Object.fromEntries(
19
- routes
20
- .entries()
21
- .filter(([path]) => path.startsWith('/api/'))
22
- .map(([path, route]) => [
23
- path,
24
- {
25
- params: Object.fromEntries(
26
- Object.entries(route.params || {}).map(([key, type]) => [key, type ? type.def.type : null])
27
- ),
28
- methods: requestMethods.filter(m => m in route),
29
- },
30
- ])
31
- ),
32
- plugins: Object.fromEntries(plugins.values().map(plugin => [plugin.id, plugin.version])),
33
- };
34
- },
35
- });
@@ -1,56 +0,0 @@
1
- import type { Result } from '@axium/core/api';
2
- import { PasskeyChangeable } from '@axium/core/schemas';
3
- import { getPasskey } from '@axium/server/auth';
4
- import { database as db } from '@axium/server/database';
5
- import { addRoute } from '@axium/server/routes';
6
- import { error } from '@sveltejs/kit';
7
- import { omit } from 'utilium';
8
- import z from 'zod/v4';
9
- import { checkAuth, parseBody, withError } from './utils';
10
-
11
- addRoute({
12
- path: '/api/passkeys/:id',
13
- params: {
14
- id: z.string(),
15
- },
16
- async GET(event): Result<'GET', 'passkeys/:id'> {
17
- const passkey = await getPasskey(event.params.id);
18
- await checkAuth(event, passkey.userId);
19
- return omit(passkey, 'counter', 'publicKey');
20
- },
21
- async PATCH(event): Result<'PATCH', 'passkeys/:id'> {
22
- const body = await parseBody(event, PasskeyChangeable);
23
- const passkey = await getPasskey(event.params.id);
24
- await checkAuth(event, passkey.userId);
25
- const result = await db
26
- .updateTable('passkeys')
27
- .set(body)
28
- .where('id', '=', passkey.id)
29
- .returningAll()
30
- .executeTakeFirstOrThrow()
31
- .catch(withError('Could not update passkey'));
32
-
33
- return omit(result, 'counter', 'publicKey');
34
- },
35
- async DELETE(event): Result<'DELETE', 'passkeys/:id'> {
36
- const passkey = await getPasskey(event.params.id);
37
- await checkAuth(event, passkey.userId);
38
-
39
- const { count } = await db
40
- .selectFrom('passkeys')
41
- .select(db.fn.countAll().as('count'))
42
- .where('userId', '=', passkey.userId)
43
- .executeTakeFirstOrThrow();
44
-
45
- if (Number(count) <= 1) error(409, 'At least one passkey is required');
46
-
47
- const result = await db
48
- .deleteFrom('passkeys')
49
- .where('id', '=', passkey.id)
50
- .returningAll()
51
- .executeTakeFirstOrThrow()
52
- .catch(withError('Could not delete passkey'));
53
-
54
- return omit(result, 'counter', 'publicKey');
55
- },
56
- });
package/web/api/readme.md DELETED
@@ -1 +0,0 @@
1
- This is the web-facing API, not a TypeScript API. In this directory you'll find the `addRoute` calls for the built-in `/api` routes along with the utilities and other helpers used.
@@ -1,83 +0,0 @@
1
- /** Register a new user. */
2
- import type { Result } from '@axium/core/api';
3
- import { APIUserRegistration } from '@axium/core/schemas';
4
- import { createPasskey, getUser, getUserByEmail } from '@axium/server/auth';
5
- import config from '@axium/server/config';
6
- import { database as db, type Schema } from '@axium/server/database';
7
- import { addRoute } from '@axium/server/routes';
8
- import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
9
- import { error, type RequestEvent } from '@sveltejs/kit';
10
- import { randomUUID } from 'node:crypto';
11
- import z from 'zod/v4';
12
- import { createSessionData, parseBody, withError } from './utils.js';
13
-
14
- // Map of user ID => challenge
15
- const registrations = new Map<string, string>();
16
-
17
- async function OPTIONS(event: RequestEvent): Result<'OPTIONS', 'register'> {
18
- const { name, email } = await parseBody(event, z.object({ name: z.string().optional(), email: z.email().optional() }));
19
-
20
- const userId = randomUUID();
21
- const user = await getUser(userId);
22
- if (user) error(409, { message: 'Generated UUID is already in use, please retry.' });
23
-
24
- const options = await generateRegistrationOptions({
25
- rpName: config.auth.rp_name,
26
- rpID: config.auth.rp_id,
27
- userName: email ?? userId,
28
- userDisplayName: name,
29
- attestationType: 'none',
30
- excludeCredentials: [],
31
- authenticatorSelection: {
32
- residentKey: 'preferred',
33
- userVerification: 'preferred',
34
- authenticatorAttachment: 'platform',
35
- },
36
- });
37
-
38
- registrations.set(userId, options.challenge);
39
-
40
- return { userId, options };
41
- }
42
-
43
- async function POST(event: RequestEvent): Result<'POST', 'register'> {
44
- const { userId, email, name, response } = await parseBody(event, APIUserRegistration);
45
-
46
- const existing = await getUserByEmail(email);
47
- if (existing) error(409, { message: 'Email already in use' });
48
-
49
- const expectedChallenge = registrations.get(userId);
50
- if (!expectedChallenge) error(404, { message: 'No registration challenge found for this user' });
51
- registrations.delete(userId);
52
-
53
- const { verified, registrationInfo } = await verifyRegistrationResponse({
54
- response,
55
- expectedChallenge,
56
- expectedOrigin: config.auth.origin,
57
- }).catch(() => error(400, { message: 'Verification failed' }));
58
-
59
- if (!verified || !registrationInfo) error(401, { message: 'Verification failed' });
60
-
61
- await db
62
- .insertInto('users')
63
- .values({ id: userId, name, email } as Schema['users'])
64
- .executeTakeFirstOrThrow()
65
- .catch(withError('Failed to create user'));
66
-
67
- await createPasskey({
68
- transports: [],
69
- ...registrationInfo.credential,
70
- userId,
71
- deviceType: registrationInfo.credentialDeviceType,
72
- backedUp: registrationInfo.credentialBackedUp,
73
- }).catch(e => error(500, { message: 'Failed to create passkey' + (config.debug ? `: ${e.message}` : '') }));
74
-
75
- return await createSessionData(event, userId);
76
- }
77
-
78
- addRoute({
79
- path: '/api/register',
80
- params: {},
81
- OPTIONS,
82
- POST,
83
- });
@@ -1,22 +0,0 @@
1
- import type { AuthenticatorTransportFuture } from '@simplewebauthn/server';
2
- import z from 'zod/v4';
3
-
4
- const transports = ['ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', 'usb'] satisfies AuthenticatorTransportFuture[];
5
-
6
- export const authenticatorAttachment = z.enum(['platform', 'cross-platform'] satisfies AuthenticatorAttachment[]).optional();
7
-
8
- export const PasskeyRegistration = z.object({
9
- id: z.string(),
10
- rawId: z.string(),
11
- response: z.object({
12
- clientDataJSON: z.string(),
13
- attestationObject: z.string(),
14
- authenticatorData: z.string().optional(),
15
- transports: z.array(z.enum(transports)).optional(),
16
- publicKeyAlgorithm: z.number().optional(),
17
- publicKey: z.string().optional(),
18
- }),
19
- authenticatorAttachment,
20
- clientExtensionResults: z.record(z.any(), z.any()),
21
- type: z.literal('public-key'),
22
- });