@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
@@ -1,6 +1,17 @@
1
- import { loadDefaultConfigs } from '@axium/server/config.js';
2
- import { _markDefaults } from '@axium/server/routes.js';
3
- import './api/index.js';
1
+ import '@axium/server/api/index';
2
+ import { loadDefaultConfigs } from '@axium/server/config';
3
+ import { clean, database } from '@axium/server/database';
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';
4
8
 
5
- _markDefaults();
9
+ logger.attach(createWriteStream(join(dirs.at(-1), 'server.log')), { output: allLogLevels });
6
10
  await loadDefaultConfigs();
11
+ await clean({});
12
+
13
+ process.on('beforeExit', async () => {
14
+ await database.destroy();
15
+ });
16
+
17
+ export { handle } from '@axium/server/sveltekit';
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import { fade } from 'svelte/transition';
3
+ import { wait } from 'utilium';
4
+ import Icon from './icons/Icon.svelte';
5
+
6
+ const { value, type = 'text/plain' }: { value: BlobPart; type?: string } = $props();
7
+
8
+ let success = $state(false);
9
+
10
+ async function onclick() {
11
+ const blob = new Blob([value], { type });
12
+ const item = new ClipboardItem({ [type]: blob });
13
+ await navigator.clipboard.write([item]);
14
+ success = true;
15
+ await wait(3000);
16
+ success = false;
17
+ }
18
+ </script>
19
+
20
+ <button {onclick}>
21
+ {#if success}
22
+ <span transition:fade><Icon i="check" /></span>
23
+ {:else}
24
+ <span transition:fade><Icon i="copy" /></span>
25
+ {/if}
26
+ </button>
27
+
28
+ <style>
29
+ button {
30
+ position: relative;
31
+ display: inline-block;
32
+ width: 1em;
33
+ height: 1em;
34
+ border: none;
35
+ background: transparent;
36
+ }
37
+
38
+ span {
39
+ position: absolute;
40
+ inset: 0;
41
+ }
42
+ </style>
@@ -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}>
@@ -1,7 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { goto } from '$app/navigation';
3
+ import { page } from '$app/state';
3
4
  import Dialog from './Dialog.svelte';
4
- import './styles.css';
5
+
6
+ function resolveRedirectAfter() {
7
+ const maybe = page.url.searchParams.get('after');
8
+ if (!maybe || maybe == page.url.pathname) return '/';
9
+ for (const prefix of ['/api/']) if (maybe.startsWith(prefix)) return '/';
10
+ return maybe || '/';
11
+ }
5
12
 
6
13
  let {
7
14
  children,
@@ -42,7 +49,7 @@
42
49
  const data = Object.fromEntries(new FormData(e.currentTarget));
43
50
  submit(data)
44
51
  .then(result => {
45
- if (pageMode) goto('/');
52
+ if (pageMode) goto(resolveRedirectAfter());
46
53
  else dialog.close();
47
54
  })
48
55
  .catch((e: unknown) => {
@@ -1,20 +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
- const [style, id] = i.includes('/') ? i.split('/') : ['solid', i];
9
- const url = urls[style];
3
+ const [style, id] = $derived(i.includes('/') ? i.split('/') : ['solid', i]);
4
+ const href = $derived(`/icons/${style}.svg#${id}`);
10
5
  </script>
11
6
 
12
- <svelte:head>
13
- <link rel="preload" href={url} />
14
- </svelte:head>
15
-
16
7
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em">
17
- <use href="{url}#{id}" />
8
+ <use {href} />
18
9
  </svg>
19
10
 
20
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.js';
4
- import { plugins } from '@axium/server/plugins.js';
5
- import { addRoute, routes } from '@axium/server/routes.js';
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.js';
4
- import { database as db } from '@axium/server/database.js';
5
- import { addRoute } from '@axium/server/routes.js';
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.js';
5
- import config from '@axium/server/config.js';
6
- import { database as db, type Schema } from '@axium/server/database.js';
7
- import { addRoute } from '@axium/server/routes.js';
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
- });
@@ -1,33 +0,0 @@
1
- import { authenticate } from '$lib/auth';
2
- import type { Result } from '@axium/core/api';
3
- import { connect, database as db } from '@axium/server/database.js';
4
- import { addRoute } from '@axium/server/routes.js';
5
- import { error } from '@sveltejs/kit';
6
- import { omit } from 'utilium';
7
- import { getToken, stripUser } from './utils';
8
-
9
- addRoute({
10
- path: '/api/session',
11
- async GET(event): Result<'GET', 'session'> {
12
- const result = await authenticate(event);
13
-
14
- if (!result) error(404, 'Session does not exist');
15
-
16
- return {
17
- ...omit(result, 'token'),
18
- user: stripUser(result.user, true),
19
- };
20
- },
21
- async DELETE(event): Result<'DELETE', 'session'> {
22
- const token = getToken(event);
23
- connect();
24
- const result = await db
25
- .deleteFrom('sessions')
26
- .where('sessions.token', '=', token)
27
- .returningAll()
28
- .executeTakeFirstOrThrow()
29
- .catch((e: Error) => (e.message == 'no result' ? error(404, 'Session does not exist') : error(400, 'Invalid session')));
30
-
31
- return omit(result, 'token');
32
- },
33
- });