@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.
- package/{web/lib → assets}/styles.css +7 -1
- package/{web/api/index.ts → dist/api/index.d.ts} +0 -2
- package/dist/api/index.js +5 -0
- package/dist/api/metadata.d.ts +1 -0
- package/dist/api/metadata.js +28 -0
- package/dist/api/passkeys.d.ts +1 -0
- package/dist/api/passkeys.js +50 -0
- package/dist/api/register.d.ts +1 -0
- package/dist/api/register.js +70 -0
- package/dist/api/session.d.ts +1 -0
- package/dist/api/session.js +31 -0
- package/dist/api/users.d.ts +1 -0
- package/dist/api/users.js +244 -0
- package/dist/apps.d.ts +0 -5
- package/dist/apps.js +2 -9
- package/dist/auth.d.ts +9 -31
- package/dist/auth.js +20 -34
- package/dist/cli.js +200 -54
- package/dist/config.d.ts +52 -480
- package/dist/config.js +89 -56
- package/dist/database.d.ts +3 -3
- package/dist/database.js +57 -24
- package/dist/io.d.ts +6 -4
- package/dist/io.js +26 -19
- package/dist/plugins.d.ts +5 -2
- package/dist/plugins.js +16 -14
- package/dist/requests.d.ts +11 -0
- package/dist/requests.js +58 -0
- package/dist/routes.d.ts +12 -13
- package/dist/routes.js +21 -22
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +11 -0
- package/dist/state.d.ts +4 -0
- package/dist/state.js +22 -0
- package/dist/sveltekit.d.ts +8 -0
- package/dist/sveltekit.js +90 -0
- package/package.json +18 -10
- package/{web/routes → routes}/account/+page.svelte +115 -48
- package/svelte.config.js +36 -0
- package/web/hooks.server.ts +15 -4
- package/web/lib/ClipboardCopy.svelte +42 -0
- package/web/lib/Dialog.svelte +0 -1
- package/web/lib/FormDialog.svelte +9 -2
- package/web/lib/icons/Icon.svelte +3 -12
- package/web/template.html +18 -0
- package/web/tsconfig.json +3 -2
- package/web/api/metadata.ts +0 -35
- package/web/api/passkeys.ts +0 -56
- package/web/api/readme.md +0 -1
- package/web/api/register.ts +0 -83
- package/web/api/schemas.ts +0 -22
- package/web/api/session.ts +0 -33
- package/web/api/users.ts +0 -340
- package/web/api/utils.ts +0 -66
- package/web/app.html +0 -14
- package/web/auth.ts +0 -8
- package/web/index.server.ts +0 -1
- package/web/index.ts +0 -1
- package/web/lib/auth.ts +0 -12
- package/web/lib/index.ts +0 -5
- package/web/routes/+layout.svelte +0 -6
- package/web/routes/[...path]/+page.server.ts +0 -13
- package/web/routes/[appId]/[...page]/+page.server.ts +0 -14
- package/web/routes/api/[...path]/+server.ts +0 -49
- package/web/utils.ts +0 -26
- /package/{web/lib → assets}/icons/light.svg +0 -0
- /package/{web/lib → assets}/icons/regular.svg +0 -0
- /package/{web/lib → assets}/icons/solid.svg +0 -0
- /package/{web/routes → routes}/_axium/default/+page.svelte +0 -0
- /package/{web/routes → routes}/login/+page.svelte +0 -0
- /package/{web/routes → routes}/logout/+page.svelte +0 -0
- /package/{web/routes → routes}/register/+page.svelte +0 -0
package/web/hooks.server.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import '
|
|
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
|
-
|
|
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>
|
package/web/lib/Dialog.svelte
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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": "
|
|
5
|
+
"target": "ES2023",
|
|
5
6
|
"lib": ["ESNext", "DOM", "DOM.Iterable"]
|
|
6
7
|
},
|
|
7
|
-
"include": ["
|
|
8
|
+
"include": ["./**/*", "../lib"]
|
|
8
9
|
}
|
package/web/api/metadata.ts
DELETED
|
@@ -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
|
-
});
|
package/web/api/passkeys.ts
DELETED
|
@@ -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.
|
package/web/api/register.ts
DELETED
|
@@ -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
|
-
});
|
package/web/api/schemas.ts
DELETED
|
@@ -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
|
-
});
|
package/web/api/session.ts
DELETED
|
@@ -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
|
-
});
|