@axium/server 0.8.0 → 0.9.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.
@@ -38,8 +38,7 @@ export interface Schema {
38
38
  transports: AuthenticatorTransportFuture[];
39
39
  };
40
40
  }
41
- export interface Database extends Kysely<Schema>, AsyncDisposable {
42
- }
41
+ export type Database = Kysely<Schema> & AsyncDisposable;
43
42
  export declare let database: Database;
44
43
  export declare function connect(): Database;
45
44
  export interface Stats {
@@ -47,7 +46,7 @@ export interface Stats {
47
46
  passkeys: number;
48
47
  sessions: number;
49
48
  }
50
- export declare function count(table: keyof Schema): Promise<number>;
49
+ export declare function count<const T extends keyof Schema>(table: T): Promise<number>;
51
50
  export declare function status(): Promise<Stats>;
52
51
  export declare function statusText(): Promise<string>;
53
52
  export interface OpOptions extends MaybeOutput {
@@ -65,6 +64,7 @@ export interface PluginShortcuts {
65
64
  }
66
65
  export declare function init(opt: InitOptions): Promise<void>;
67
66
  export declare function check(opt: OpOptions): Promise<void>;
67
+ export declare function clean(opt: Partial<OpOptions>): Promise<void>;
68
68
  /**
69
69
  * Completely remove Axium from the database.
70
70
  */
package/dist/database.js CHANGED
@@ -73,7 +73,9 @@ export function connect() {
73
73
  }
74
74
  export async function count(table) {
75
75
  const db = connect();
76
- return (await db.selectFrom(table).select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count;
76
+ return (await db.selectFrom(table)
77
+ .select(db.fn.countAll().as('count'))
78
+ .executeTakeFirstOrThrow()).count;
77
79
  }
78
80
  export async function status() {
79
81
  return {
@@ -286,35 +288,62 @@ export async function check(opt) {
286
288
  await result_2;
287
289
  }
288
290
  }
289
- /**
290
- * Completely remove Axium from the database.
291
- */
292
- export async function uninstall(opt) {
291
+ export async function clean(opt) {
293
292
  _fixOutput(opt);
293
+ const done = () => opt.output('done');
294
+ const now = new Date();
294
295
  const db = connect();
296
+ opt.output('start', 'Removing expired sessions');
297
+ await db.deleteFrom('sessions').where('sessions.expires', '<', now).execute().then(done);
298
+ opt.output('start', 'Removing expired verifications');
299
+ await db.deleteFrom('verifications').where('verifications.expires', '<', now).execute().then(done);
295
300
  for (const plugin of plugins) {
296
- if (!plugin.db_remove)
301
+ if (!plugin.db_clean)
297
302
  continue;
298
303
  opt.output('plugin', plugin.name);
299
- await plugin.db_remove(opt, db);
304
+ await plugin.db_clean(opt, db);
305
+ }
306
+ }
307
+ /**
308
+ * Completely remove Axium from the database.
309
+ */
310
+ export async function uninstall(opt) {
311
+ const env_3 = { stack: [], error: void 0, hasError: false };
312
+ try {
313
+ _fixOutput(opt);
314
+ const db = __addDisposableResource(env_3, connect(), true);
315
+ for (const plugin of plugins) {
316
+ if (!plugin.db_remove)
317
+ continue;
318
+ opt.output('plugin', plugin.name);
319
+ await plugin.db_remove(opt, db);
320
+ }
321
+ const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
322
+ await _sql('DROP DATABASE axium', 'Dropping database');
323
+ await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
324
+ await _sql('DROP USER axium', 'Dropping user');
325
+ await getHBA(opt)
326
+ .then(([content, writeBack]) => {
327
+ opt.output('start', 'Checking for Axium HBA configuration');
328
+ if (!content.includes(pgHba))
329
+ throw 'missing.';
330
+ opt.output('done');
331
+ opt.output('start', 'Removing Axium HBA configuration');
332
+ const newContent = content.replace(pgHba, '');
333
+ opt.output('done');
334
+ writeBack(newContent);
335
+ })
336
+ .catch(e => opt.output('warn', e));
337
+ }
338
+ catch (e_3) {
339
+ env_3.error = e_3;
340
+ env_3.hasError = true;
341
+ }
342
+ finally {
343
+ const result_3 = __disposeResources(env_3);
344
+ if (result_3)
345
+ await result_3;
300
346
  }
301
- await db.destroy();
302
- const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
303
- await _sql('DROP DATABASE axium', 'Dropping database');
304
- await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
305
- await _sql('DROP USER axium', 'Dropping user');
306
- await getHBA(opt)
307
- .then(([content, writeBack]) => {
308
- opt.output('start', 'Checking for Axium HBA configuration');
309
- if (!content.includes(pgHba))
310
- throw 'missing.';
311
- opt.output('done');
312
- opt.output('start', 'Removing Axium HBA configuration');
313
- const newContent = content.replace(pgHba, '');
314
- opt.output('done');
315
- writeBack(newContent);
316
- })
317
- .catch(e => opt.output('warn', e));
318
347
  }
319
348
  /**
320
349
  * Removes all data from tables.
package/dist/plugins.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare const Plugin: z.ZodObject<{
9
9
  db_init: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
10
10
  db_remove: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
11
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>>;
12
13
  }, z.core.$strip>;
13
14
  export interface Plugin extends z.infer<typeof Plugin> {
14
15
  }
package/dist/plugins.js CHANGED
@@ -14,6 +14,7 @@ export const Plugin = z.object({
14
14
  db_init: fn.optional(),
15
15
  db_remove: fn.optional(),
16
16
  db_wipe: fn.optional(),
17
+ db_clean: fn.optional(),
17
18
  });
18
19
  export const plugins = new Set();
19
20
  export function resolvePlugin(search) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -19,7 +19,7 @@
19
19
  "types": "dist/index.d.ts",
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
- "./*": "./dist/*",
22
+ "./*": "./dist/*.js",
23
23
  "./web": "./web/index.js",
24
24
  "./web/*": "./web/*"
25
25
  },
@@ -33,18 +33,21 @@
33
33
  "scripts": {
34
34
  "build": "tsc"
35
35
  },
36
+ "peerDependencies": {
37
+ "@axium/core": ">=0.3.0",
38
+ "@axium/client": ">=0.0.2",
39
+ "utilium": "^2.3.8",
40
+ "zod": "^3.25.61"
41
+ },
36
42
  "dependencies": {
37
43
  "@simplewebauthn/server": "^13.1.1",
38
44
  "@sveltejs/kit": "^2.20.2",
39
45
  "@types/pg": "^8.11.11",
40
46
  "commander": "^13.1.0",
41
- "kysely": "^0.27.5",
47
+ "kysely": "^0.28.0",
42
48
  "logzen": "^0.7.0",
43
49
  "mime": "^4.0.7",
44
- "pg": "^8.14.1",
45
- "utilium": "^2.3.8",
46
- "zod": "^3.25.61",
47
- "@axium/core": ">=0.2.0"
50
+ "pg": "^8.14.1"
48
51
  },
49
52
  "devDependencies": {
50
53
  "@sveltejs/adapter-node": "^5.2.12",
@@ -1,8 +1,8 @@
1
1
  import type { Result } from '@axium/core/api';
2
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';
3
+ import { config } from '@axium/server/config';
4
+ import { plugins } from '@axium/server/plugins';
5
+ import { addRoute, routes } from '@axium/server/routes';
6
6
  import { error } from '@sveltejs/kit';
7
7
  import pkg from '../../package.json' with { type: 'json' };
8
8
 
@@ -1,8 +1,8 @@
1
1
  import type { Result } from '@axium/core/api';
2
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';
3
+ import { getPasskey } from '@axium/server/auth';
4
+ import { database as db } from '@axium/server/database';
5
+ import { addRoute } from '@axium/server/routes';
6
6
  import { error } from '@sveltejs/kit';
7
7
  import { omit } from 'utilium';
8
8
  import z from 'zod/v4';
@@ -1,10 +1,10 @@
1
1
  /** Register a new user. */
2
2
  import type { Result } from '@axium/core/api';
3
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';
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
8
  import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
9
9
  import { error, type RequestEvent } from '@sveltejs/kit';
10
10
  import { randomUUID } from 'node:crypto';
@@ -1,7 +1,7 @@
1
1
  import { authenticate } from '$lib/auth';
2
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';
3
+ import { connect, database as db } from '@axium/server/database';
4
+ import { addRoute } from '@axium/server/routes';
5
5
  import { error } from '@sveltejs/kit';
6
6
  import { omit } from 'utilium';
7
7
  import { getToken, stripUser } from './utils';
package/web/api/users.ts CHANGED
@@ -1,20 +1,19 @@
1
1
  /** Register a new passkey for a new or existing user. */
2
2
  import type { Result } from '@axium/core/api';
3
- import { PasskeyAuthenticationResponse, UserAuthOptions } from '@axium/core/schemas';
3
+ import { LogoutSessions, PasskeyAuthenticationResponse, UserAuthOptions } from '@axium/core/schemas';
4
4
  import { UserChangeable, type User } from '@axium/core/user';
5
5
  import {
6
6
  createPasskey,
7
7
  createVerification,
8
8
  getPasskey,
9
9
  getPasskeysByUserId,
10
- getSession,
11
10
  getSessions,
12
11
  getUser,
13
12
  useVerification,
14
- } from '@axium/server/auth.js';
15
- import { config } from '@axium/server/config.js';
16
- import { connect, database as db } from '@axium/server/database.js';
17
- import { addRoute } from '@axium/server/routes.js';
13
+ } from '@axium/server/auth';
14
+ import { config } from '@axium/server/config';
15
+ import { connect, database as db } from '@axium/server/database';
16
+ import { addRoute } from '@axium/server/routes';
18
17
  import {
19
18
  generateAuthenticationOptions,
20
19
  generateRegistrationOptions,
@@ -282,32 +281,44 @@ addRoute({
282
281
  await checkAuth(event, userId);
283
282
 
284
283
  return (await getSessions(userId).catch(e => error(503, 'Failed to get sessions' + (config.debug ? ': ' + e : '')))).map(s =>
285
- pick(s, 'id', 'expires')
284
+ omit(s, 'token')
286
285
  );
287
286
  },
288
287
  async DELETE(event: RequestEvent): Result<'DELETE', 'users/:id/sessions'> {
289
288
  const { id: userId } = event.params;
290
- const { id: sessionId } = await parseBody(event, z.object({ id: z.uuid() }));
289
+ const body = await parseBody(event, LogoutSessions);
291
290
 
292
- await checkAuth(event, userId);
293
-
294
- const session = await getSession(sessionId).catch(withError('Session does not exist', 404));
291
+ await checkAuth(event, userId, body.confirm_all);
295
292
 
296
- if (session.userId !== userId) error(403, { message: 'Session does not belong to the user' });
293
+ const query = body.confirm_all ? db.deleteFrom('sessions') : db.deleteFrom('sessions').where('sessions.id', 'in', body.id);
297
294
 
298
- await db
299
- .deleteFrom('sessions')
300
- .where('sessions.id', '=', session.id)
301
- .executeTakeFirstOrThrow()
302
- .catch(withError('Failed to delete session'));
295
+ const result = await query
296
+ .where('sessions.userId', '=', userId)
297
+ .returningAll()
298
+ .execute()
299
+ .catch(withError('Failed to delete one or more sessions'));
303
300
 
304
- return;
301
+ return result.map(s => omit(s, 'token'));
305
302
  },
306
303
  });
307
304
 
308
305
  addRoute({
309
306
  path: '/api/users/:id/verify_email',
310
307
  params,
308
+ async OPTIONS(event): Result<'OPTIONS', 'users/:id/verify_email'> {
309
+ const { id: userId } = event.params;
310
+
311
+ if (!config.auth.email_verification) return { enabled: false };
312
+
313
+ await checkAuth(event, userId);
314
+
315
+ const user = await getUser(userId);
316
+ if (!user) error(404, { message: 'User does not exist' });
317
+
318
+ if (!config.auth.email_verification) return { enabled: false };
319
+
320
+ return { enabled: true };
321
+ },
311
322
  async GET(event): Result<'GET', 'users/:id/verify_email'> {
312
323
  const { id: userId } = event.params;
313
324
 
package/web/api/utils.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { userProtectedFields, userPublicFields, type User } from '@axium/core/user';
2
2
  import type { NewSessionResponse } from '@axium/core/api';
3
- import { createSession, getSessionAndUser, type UserInternal } from '@axium/server/auth.js';
4
- import { config } from '@axium/server/config.js';
3
+ import { createSession, getSessionAndUser, type UserInternal } from '@axium/server/auth';
4
+ import { config } from '@axium/server/config';
5
5
  import { error, type RequestEvent } from '@sveltejs/kit';
6
6
  import { pick } from 'utilium';
7
7
  import z from 'zod/v4';
@@ -1,6 +1,12 @@
1
- import { loadDefaultConfigs } from '@axium/server/config.js';
2
- import { _markDefaults } from '@axium/server/routes.js';
1
+ import { loadDefaultConfigs } from '@axium/server/config';
2
+ import { clean, database } from '@axium/server/database';
3
+ import { _markDefaults } from '@axium/server/routes';
3
4
  import './api/index.js';
4
5
 
5
6
  _markDefaults();
6
7
  await loadDefaultConfigs();
8
+ await clean({});
9
+
10
+ process.on('beforeExit', async () => {
11
+ await database.destroy();
12
+ });
@@ -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,8 +1,16 @@
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
  import './styles.css';
5
6
 
7
+ function resolveRedirectAfter() {
8
+ const maybe = page.url.searchParams.get('after');
9
+ if (!maybe || maybe == page.url.pathname) return '/';
10
+ for (const prefix of ['/api/']) if (maybe.startsWith(prefix)) return '/';
11
+ return maybe || '/';
12
+ }
13
+
6
14
  let {
7
15
  children,
8
16
  dialog = $bindable(),
@@ -42,7 +50,7 @@
42
50
  const data = Object.fromEntries(new FormData(e.currentTarget));
43
51
  submit(data)
44
52
  .then(result => {
45
- if (pageMode) goto('/');
53
+ if (pageMode) goto(resolveRedirectAfter());
46
54
  else dialog.close();
47
55
  })
48
56
  .catch((e: unknown) => {
package/web/lib/auth.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { SessionInternal, UserInternal } from '@axium/server/auth.js';
2
- import { getSessionAndUser } from '@axium/server/auth.js';
1
+ import type { SessionInternal, UserInternal } from '@axium/server/auth';
2
+ import { getSessionAndUser } from '@axium/server/auth';
3
3
  import type { RequestEvent } from '@sveltejs/kit';
4
4
 
5
5
  export async function authenticate(event: RequestEvent): Promise<(SessionInternal & { user: UserInternal | null }) | null> {
@@ -5,14 +5,10 @@
5
5
  const urls = { light, solid, regular };
6
6
  const { i } = $props();
7
7
 
8
- const [style, id] = i.includes('/') ? i.split('/') : ['solid', i];
9
- const url = urls[style];
8
+ const [style, id] = $derived(i.includes('/') ? i.split('/') : ['solid', i]);
9
+ const url = $derived(urls[style]);
10
10
  </script>
11
11
 
12
- <svelte:head>
13
- <link rel="preload" href={url} />
14
- </svelte:head>
15
-
16
12
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em">
17
13
  <use href="{url}#{id}" />
18
14
  </svg>
@@ -4,6 +4,7 @@ body {
4
4
  background-color: #222;
5
5
  color: #bbb;
6
6
  accent-color: #bbb;
7
+ overflow-y: scroll;
7
8
  }
8
9
 
9
10
  .main {
@@ -15,7 +16,7 @@ body {
15
16
  gap: 1em;
16
17
  }
17
18
 
18
- div:not(.main):has(form.main) {
19
+ .main-container:has(form.main) {
19
20
  position: absolute;
20
21
  inset: 0;
21
22
  display: flex;
@@ -94,3 +95,8 @@ button:hover {
94
95
  .danger:hover {
95
96
  background-color: #633;
96
97
  }
98
+
99
+ :disabled,
100
+ .disabled {
101
+ cursor: not-allowed;
102
+ }
@@ -1,5 +1,5 @@
1
1
  import { error, redirect, type LoadEvent } from '@sveltejs/kit';
2
- import { resolveRoute } from '@axium/server/routes.js';
2
+ import { resolveRoute } from '@axium/server/routes';
3
3
 
4
4
  export async function load(event: LoadEvent) {
5
5
  const route = resolveRoute(event);
@@ -1,5 +1,5 @@
1
1
  import { error, type LoadEvent } from '@sveltejs/kit';
2
- import { apps } from '@axium/server/apps.js';
2
+ import { apps } from '@axium/server/apps';
3
3
 
4
4
  export async function load(event: LoadEvent) {
5
5
  const app = apps.get(event.params.appId);