@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/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,94 @@
1
+ import { error, json } from '@sveltejs/kit';
2
+ import { readFileSync } from 'node:fs';
3
+ import { styleText } from 'node:util';
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
+ if ('location' in e)
38
+ return Response.redirect(e.location, e.status);
39
+ console.error(e);
40
+ return json({ message: 'Internal Error' + (config.debug ? ': ' + e.message : '') }, { status: 500 });
41
+ }
42
+ let template = null;
43
+ function fillTemplate({ head, body }, env = {}, nonce = '') {
44
+ template ||= readFileSync(config.web.template, 'utf-8');
45
+ return (template
46
+ .replace('%sveltekit.head%', head)
47
+ .replace('%sveltekit.body%', body)
48
+ .replace(/%sveltekit\.assets%/g, config.web.assets)
49
+ // Unused for now.
50
+ .replace(/%sveltekit\.nonce%/g, nonce)
51
+ .replace(/%sveltekit\.env\.([^%]+)%/g, (_match, key) => env[key] ?? ''));
52
+ }
53
+ /**
54
+ * @internal
55
+ */
56
+ export async function handle({ event, resolve, }) {
57
+ const route = resolveRoute(event);
58
+ if (!route && event.url.pathname === '/' && config.debug)
59
+ return new Response(null, { status: 303, headers: { Location: '/_axium/default' } });
60
+ if (config.debug)
61
+ console.log(styleText('blueBright', event.request.method.padEnd(7)), route ? route.path : event.url.pathname);
62
+ if (!route)
63
+ return await resolve(event).catch(handleError);
64
+ if (route.server == true) {
65
+ if (route.api)
66
+ return await handleAPIRequest(event, route).catch(handleError);
67
+ const run = route[event.request.method];
68
+ if (typeof run !== 'function') {
69
+ error(405, `Method ${event.request.method} not allowed for ${route.path}`);
70
+ }
71
+ try {
72
+ const result = await run(event);
73
+ if (result instanceof Response)
74
+ return result;
75
+ return json(result);
76
+ }
77
+ catch (e) {
78
+ return handleError(e);
79
+ }
80
+ }
81
+ const data = await route.load?.(event);
82
+ const body = fillTemplate(render(route.page));
83
+ return new Response(body, {
84
+ headers: config.web.disable_cache
85
+ ? {
86
+ 'Content-Type': 'text/html; charset=utf-8',
87
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
88
+ Pragma: 'no-cache',
89
+ Expires: '0',
90
+ }
91
+ : {},
92
+ status: 200,
93
+ });
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -20,12 +20,19 @@
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
22
  "./*": "./dist/*.js",
23
- "./web": "./web/index.js",
24
- "./web/*": "./web/*"
23
+ "./lib/*": "./web/lib/*",
24
+ "./$hooks": "./web/hooks.server.ts",
25
+ "./$routes": "./routes",
26
+ "./$template": "./web/template.html",
27
+ "./svelte.config.js": "./svelte.config.js"
25
28
  },
26
29
  "files": [
30
+ "assets",
31
+ "build",
27
32
  "dist",
28
- "web"
33
+ "routes",
34
+ "web",
35
+ "svelte.config.js"
29
36
  ],
30
37
  "bin": {
31
38
  "axium": "dist/cli.js"
@@ -34,24 +41,26 @@
34
41
  "build": "tsc"
35
42
  },
36
43
  "peerDependencies": {
37
- "@axium/core": ">=0.3.0",
38
- "@axium/client": ">=0.0.2",
44
+ "@axium/client": ">=0.1.0",
45
+ "@axium/core": ">=0.4.0",
39
46
  "utilium": "^2.3.8",
40
47
  "zod": "^3.25.61"
41
48
  },
42
49
  "dependencies": {
50
+ "@axium/server": "file:.",
43
51
  "@simplewebauthn/server": "^13.1.1",
44
52
  "@sveltejs/kit": "^2.20.2",
45
53
  "@types/pg": "^8.11.11",
46
54
  "commander": "^13.1.0",
55
+ "cookie": "^1.0.2",
47
56
  "kysely": "^0.28.0",
48
57
  "logzen": "^0.7.0",
49
58
  "mime": "^4.0.7",
50
- "pg": "^8.14.1"
59
+ "pg": "^8.14.1",
60
+ "svelte": "^5.25.3"
51
61
  },
52
62
  "devDependencies": {
53
63
  "@sveltejs/adapter-node": "^5.2.12",
54
- "svelte": "^5.25.3",
55
64
  "vite-plugin-mkcert": "^1.17.8"
56
65
  }
57
66
  }
@@ -191,11 +191,12 @@
191
191
  </div>
192
192
  <FormDialog
193
193
  bind:dialog={dialogs['logout#' + session.id]}
194
- submit={() =>
195
- logout(user.id, session.id).then(() => {
196
- if (session.id == currentSession.id) goto('/');
197
- else sessions.splice(sessions.indexOf(session), 1);
198
- })}
194
+ submit={async () => {
195
+ await logout(user.id, session.id);
196
+ dialogs['logout#' + session.id].remove();
197
+ sessions.splice(sessions.indexOf(session), 1);
198
+ if (session.id == currentSession.id) goto('/');
199
+ }}
199
200
  submitText="Logout"
200
201
  >
201
202
  <p>Are you sure you want to log out this session?</p>
@@ -0,0 +1,37 @@
1
+ import node from '@sveltejs/adapter-node';
2
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3
+ import { join } from 'node:path/posix';
4
+ import config from '@axium/server/config';
5
+
6
+ /**
7
+ * Paths relative to the directory of this file.
8
+ * This allows this file to be imported from other projects and still resolve to the correct paths.
9
+ */
10
+ const fixed = p => join(import.meta.dirname, p);
11
+
12
+ /** @type {import('@sveltejs/kit').Config} */
13
+ export default {
14
+ compilerOptions: {
15
+ runes: true,
16
+ },
17
+ preprocess: vitePreprocess({ script: true }),
18
+ vitePlugin: {
19
+ exclude: '@axium/server/**',
20
+ },
21
+ kit: {
22
+ adapter: node(),
23
+ alias: {
24
+ $stores: fixed('web/stores'),
25
+ $lib: fixed('web/lib'),
26
+ },
27
+ files: {
28
+ routes: config.web.routes,
29
+ lib: fixed('web/lib'),
30
+ assets: fixed('assets'),
31
+ appTemplate: fixed('web/template.html'),
32
+ hooks: {
33
+ server: fixed('web/hooks.server.ts'),
34
+ },
35
+ },
36
+ },
37
+ };
@@ -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');
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import Icon from './icons/Icon.svelte';
3
+ import { iconForMime, iconForPath } from './icons';
4
+
5
+ let { files = $bindable(), name = 'files', ...rest }: { files?: FileList; name?: string; multiple?: any; required?: any } = $props();
6
+
7
+ let input = $state<HTMLInputElement>();
8
+
9
+ const id = 'input:' + Math.random().toString(36).slice(2);
10
+ </script>
11
+
12
+ <div>
13
+ {#if files?.length}
14
+ <label for={id} class="file">
15
+ {#each files as file}
16
+ <Icon i={iconForMime(file.type) || iconForPath(file.name) || 'file'} />
17
+ <span>{file.name}</span>
18
+ <button
19
+ onclick={e => {
20
+ e.preventDefault();
21
+ const dt = new DataTransfer();
22
+ for (let f of files) if (file !== f) dt.items.add(f);
23
+ input.files = files = dt.files;
24
+ }}
25
+ style:display="contents"
26
+ >
27
+ <Icon i="trash" />
28
+ </button>
29
+ {/each}
30
+ </label>
31
+ {:else}
32
+ <label for={id}><Icon i="upload" />Upload</label>
33
+ {/if}
34
+
35
+ <input bind:this={input} {name} {id} type="file" bind:files {...rest} />
36
+ </div>
37
+
38
+ <style>
39
+ input {
40
+ display: none;
41
+ }
42
+
43
+ label {
44
+ padding: 0.5em 1em;
45
+ border: 1px solid #cccc;
46
+ cursor: pointer;
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 0.5em;
50
+ border-radius: 0.5em;
51
+ width: 20em;
52
+ }
53
+
54
+ label.file {
55
+ display: grid;
56
+ grid-template-columns: 2em 1fr 2em;
57
+ }
58
+ </style>
@@ -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>
@@ -3,8 +3,11 @@ import mimeIcons from './mime.json' with { type: 'json' };
3
3
 
4
4
  export { default as Icon } from './Icon.svelte';
5
5
 
6
- export function iconFor(path: string): string {
7
- type K = keyof typeof mimeIcons;
6
+ export function iconForMime(mimeType: string): string {
7
+ return mimeIcons[mimeType as keyof typeof mimeIcons] || 'file';
8
+ }
9
+
10
+ export function iconForPath(path: string): string {
8
11
  const type = mime.getType(path) || 'application/octet-stream';
9
- return mimeIcons[type as K] || mimeIcons[type.split('/')[0] as K] || 'file';
12
+ return iconForMime(type);
10
13
  }
@@ -21,7 +21,8 @@
21
21
  "text/css": "css",
22
22
  "text/csv": "file-csv",
23
23
  "text/html": "code",
24
- "text/javascript": "square-js",
24
+ "text/javascript": "brands/square-js",
25
+ "application/x-javascript": "brands/square-js",
25
26
  "text/plain": "file-lines",
26
27
  "text/xml": "code",
27
28
  "video": "clapperboard-play"
@@ -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,8 @@
1
1
  {
2
2
  "extends": "../.svelte-kit/tsconfig.json",
3
3
  "compilerOptions": {
4
- "target": "ES2021",
4
+ "target": "ES2023",
5
5
  "lib": ["ESNext", "DOM", "DOM.Iterable"]
6
6
  },
7
- "include": ["**/*.ts", "**/*.svelte"]
7
+ "include": ["./**/*", "../lib"]
8
8
  }
@@ -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.