@axium/server 0.7.5 → 0.8.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 (58) hide show
  1. package/dist/apps.d.ts +15 -0
  2. package/dist/apps.js +20 -0
  3. package/dist/auth.d.ts +63 -30
  4. package/dist/auth.js +110 -129
  5. package/dist/cli.js +33 -6
  6. package/dist/config.d.ts +241 -61
  7. package/dist/config.js +26 -2
  8. package/dist/database.d.ts +28 -37
  9. package/dist/database.js +124 -50
  10. package/dist/io.js +6 -2
  11. package/dist/plugins.d.ts +7 -24
  12. package/dist/plugins.js +9 -14
  13. package/dist/routes.d.ts +55 -0
  14. package/dist/routes.js +54 -0
  15. package/package.json +8 -15
  16. package/web/api/index.ts +7 -0
  17. package/web/api/metadata.ts +35 -0
  18. package/web/api/passkeys.ts +56 -0
  19. package/web/api/readme.md +1 -0
  20. package/web/api/register.ts +83 -0
  21. package/web/api/schemas.ts +22 -0
  22. package/web/api/session.ts +33 -0
  23. package/web/api/users.ts +340 -0
  24. package/web/api/utils.ts +66 -0
  25. package/web/auth.ts +1 -5
  26. package/web/hooks.server.ts +6 -1
  27. package/web/index.server.ts +0 -1
  28. package/web/lib/Dialog.svelte +3 -6
  29. package/web/lib/FormDialog.svelte +53 -14
  30. package/web/lib/Toast.svelte +8 -1
  31. package/web/lib/UserCard.svelte +1 -1
  32. package/web/lib/auth.ts +12 -0
  33. package/web/lib/{Icon.svelte → icons/Icon.svelte} +8 -10
  34. package/web/lib/icons/index.ts +10 -0
  35. package/web/lib/icons/mime.json +28 -0
  36. package/web/lib/index.ts +1 -3
  37. package/web/lib/styles.css +12 -1
  38. package/web/routes/+layout.svelte +1 -1
  39. package/web/routes/[...path]/+page.server.ts +13 -0
  40. package/web/routes/[appId]/[...page]/+page.server.ts +14 -0
  41. package/web/routes/_axium/default/+page.svelte +11 -0
  42. package/web/routes/account/+page.svelte +224 -0
  43. package/web/routes/api/[...path]/+server.ts +49 -0
  44. package/web/routes/login/+page.svelte +25 -0
  45. package/web/routes/logout/+page.svelte +13 -0
  46. package/web/routes/register/+page.svelte +21 -0
  47. package/web/tsconfig.json +2 -1
  48. package/web/utils.ts +9 -15
  49. package/web/actions.ts +0 -58
  50. package/web/lib/Account.svelte +0 -76
  51. package/web/lib/SignUp.svelte +0 -20
  52. package/web/lib/account.css +0 -36
  53. package/web/routes/+page.server.ts +0 -16
  54. package/web/routes/+page.svelte +0 -10
  55. package/web/routes/name/+page.server.ts +0 -5
  56. package/web/routes/name/+page.svelte +0 -20
  57. package/web/routes/signup/+page.server.ts +0 -10
  58. package/web/routes/signup/+page.svelte +0 -15
package/dist/apps.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { LoadEvent, RequestEvent } from '@sveltejs/kit';
2
+ import type { WebRoute, WebRouteOptions } from './routes.js';
3
+ export interface CreateAppOptions {
4
+ id: string;
5
+ name?: string;
6
+ }
7
+ export declare const apps: Map<string, App>;
8
+ export declare class App {
9
+ readonly id: string;
10
+ name: string;
11
+ protected readonly routes: Map<string, WebRoute>;
12
+ constructor(opt: CreateAppOptions);
13
+ addRoute(route: WebRouteOptions): void;
14
+ resolveRoute(event: RequestEvent | LoadEvent): WebRoute | undefined;
15
+ }
package/dist/apps.js ADDED
@@ -0,0 +1,20 @@
1
+ import { pick } from 'utilium';
2
+ import { addRoute, resolveRoute } from './routes.js';
3
+ export const apps = new Map();
4
+ export class App {
5
+ id;
6
+ name;
7
+ routes = new Map();
8
+ constructor(opt) {
9
+ if (apps.has(opt.id))
10
+ throw new ReferenceError(`App with ID "${opt.id}" already exists.`);
11
+ Object.assign(this, pick(opt, 'id', 'name'));
12
+ apps.set(this.id, this);
13
+ }
14
+ addRoute(route) {
15
+ addRoute(route, this.routes);
16
+ }
17
+ resolveRoute(event) {
18
+ return resolveRoute(event, this.routes);
19
+ }
20
+ }
package/dist/auth.d.ts CHANGED
@@ -1,35 +1,68 @@
1
- import type { Adapter } from '@auth/core/adapters';
2
- import type { Provider } from '@auth/core/providers';
3
- import type { AuthConfig } from '@auth/core/types';
4
- import { Registration } from '@axium/core/schemas';
5
- /**
6
- * User preferences.
7
- * Modify with `declare module ...`
8
- */
9
- export interface Preferences {
1
+ import type { Passkey, Session, Verification } from '@axium/core/api';
2
+ import type { User } from '@axium/core/user';
3
+ export interface UserInternal extends User {
4
+ password?: string | null;
5
+ salt?: string | null;
10
6
  }
11
- declare module '@auth/core/adapters' {
12
- interface AdapterUser {
13
- password: string | null;
14
- salt: string | null;
15
- preferences: Preferences;
16
- }
7
+ export declare function getUser(id: string): Promise<{
8
+ name: string;
9
+ id: string;
10
+ image: string | null | undefined;
11
+ email: string;
12
+ emailVerified: Date | null | undefined;
13
+ preferences: import("@axium/core/user").Preferences | undefined;
14
+ } | null>;
15
+ export declare function getUserByEmail(email: string): Promise<{
16
+ name: string;
17
+ id: string;
18
+ image: string | null | undefined;
19
+ email: string;
20
+ emailVerified: Date | null | undefined;
21
+ preferences: import("@axium/core/user").Preferences | undefined;
22
+ } | null>;
23
+ export declare function updateUser({ id, ...user }: UserInternal): Promise<{
24
+ name: string;
25
+ id: string;
26
+ image: string | null | undefined;
27
+ email: string;
28
+ emailVerified: Date | null | undefined;
29
+ preferences: import("@axium/core/user").Preferences | undefined;
30
+ }>;
31
+ export interface SessionInternal extends Session {
32
+ token: string;
33
+ elevated: boolean;
17
34
  }
18
- export declare let adapter: Adapter;
19
- export declare function createAdapter(): void;
20
- /**
21
- * Login using credentials
22
- */
23
- export declare function register(credentials: Registration): Promise<{
24
- user: import("@auth/core/adapters").AdapterUser;
25
- session: import("@auth/core/adapters").AdapterSession;
35
+ export declare function createSession(userId: string, elevated?: boolean): Promise<SessionInternal>;
36
+ export declare function checkExpiration(session: SessionInternal): Promise<void>;
37
+ export declare function getSessionAndUser(token: string): Promise<SessionInternal & {
38
+ user: UserInternal | null;
39
+ }>;
40
+ export declare function getSession(sessionId: string): Promise<SessionInternal>;
41
+ export declare function getSessions(userId: string): Promise<SessionInternal[]>;
42
+ export declare function updateSession(session: SessionInternal): Promise<{
43
+ id: string;
44
+ expires: Date;
45
+ token: string;
46
+ userId: string;
47
+ created: Date;
48
+ elevated: boolean;
26
49
  }>;
50
+ export type VerificationRole = 'verify_email' | 'login';
51
+ export interface VerificationInternal extends Verification {
52
+ token: string;
53
+ role: VerificationRole;
54
+ }
27
55
  /**
28
- * Authorize using credentials
56
+ * Create a verification
57
+ * @param expires How long the token should be valid for in seconds
29
58
  */
30
- export declare function authorize(credentials: Partial<Record<string, unknown>>): Promise<Omit<import("@auth/core/adapters").AdapterUser, "password" | "salt"> | null>;
31
- type Providers = Exclude<Provider, (...args: any[]) => any>[];
32
- export declare function getConfig(): AuthConfig & {
33
- providers: Providers;
34
- };
35
- export {};
59
+ export declare function createVerification(role: VerificationRole, userId: string, expires: number): Promise<VerificationInternal>;
60
+ export declare function useVerification(role: VerificationRole, userId: string, token: string): Promise<VerificationInternal | undefined>;
61
+ export interface PasskeyInternal extends Passkey {
62
+ publicKey: Uint8Array;
63
+ counter: number;
64
+ }
65
+ export declare function getPasskey(id: string): Promise<PasskeyInternal | null>;
66
+ export declare function createPasskey(passkey: Omit<PasskeyInternal, 'createdAt'>): Promise<PasskeyInternal>;
67
+ export declare function getPasskeysByUserId(userId: string): Promise<PasskeyInternal[]>;
68
+ export declare function updatePasskeyCounter(id: PasskeyInternal['id'], newCounter: PasskeyInternal['counter']): Promise<PasskeyInternal>;
package/dist/auth.js CHANGED
@@ -1,137 +1,118 @@
1
- import { CredentialsSignin } from '@auth/core/errors';
2
- import Credentials from '@auth/core/providers/credentials';
3
- import Passkey from '@auth/core/providers/passkey';
4
- import { KyselyAdapter } from '@auth/kysely-adapter';
5
- import { Login, Registration } from '@axium/core/schemas';
6
- import { genSaltSync, hashSync } from 'bcryptjs';
7
- import { randomBytes } from 'node:crypto';
8
- import { omit } from 'utilium';
9
- import config from './config.js';
10
- import * as db from './database.js';
11
- import { logger } from './io.js';
12
- export let adapter;
13
- export function createAdapter() {
14
- if (adapter)
1
+ import { jsonObjectFrom } from 'kysely/helpers/postgres';
2
+ import { randomBytes, randomUUID } from 'node:crypto';
3
+ import { connect, database as db } from './database.js';
4
+ export async function getUser(id) {
5
+ connect();
6
+ const result = await db.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirst();
7
+ if (!result)
8
+ return null;
9
+ return result;
10
+ }
11
+ export async function getUserByEmail(email) {
12
+ connect();
13
+ const result = await db.selectFrom('users').selectAll().where('email', '=', email).executeTakeFirst();
14
+ if (!result)
15
+ return null;
16
+ return result;
17
+ }
18
+ export async function updateUser({ id, ...user }) {
19
+ connect();
20
+ const query = db.updateTable('users').set(user).where('id', '=', id);
21
+ return await query.returningAll().executeTakeFirstOrThrow();
22
+ }
23
+ const in30days = () => new Date(Date.now() + 2592000000);
24
+ const in10minutes = () => new Date(Date.now() + 600000);
25
+ export async function createSession(userId, elevated = false) {
26
+ connect();
27
+ const session = {
28
+ id: randomUUID(),
29
+ userId,
30
+ token: randomBytes(64).toString('base64'),
31
+ expires: elevated ? in10minutes() : in30days(),
32
+ elevated,
33
+ created: new Date(),
34
+ };
35
+ await db.insertInto('sessions').values(session).execute();
36
+ return session;
37
+ }
38
+ export async function checkExpiration(session) {
39
+ if (session.expires.getTime() > Date.now())
15
40
  return;
16
- const conn = db.connect();
17
- adapter = Object.assign(KyselyAdapter(conn), {
18
- async getAccount(providerAccountId, provider) {
19
- const result = await conn.selectFrom('Account').selectAll().where('providerAccountId', '=', providerAccountId).where('provider', '=', provider).executeTakeFirst();
20
- return result ?? null;
21
- },
22
- async getAuthenticator(credentialID) {
23
- const result = await conn.selectFrom('Authenticator').selectAll().where('credentialID', '=', credentialID).executeTakeFirst();
24
- return result ?? null;
25
- },
26
- async createAuthenticator(authenticator) {
27
- await conn.insertInto('Authenticator').values(authenticator).executeTakeFirstOrThrow();
28
- return authenticator;
29
- },
30
- async listAuthenticatorsByUserId(userId) {
31
- const result = await conn.selectFrom('Authenticator').selectAll().where('userId', '=', userId).execute();
32
- return result;
33
- },
34
- async updateAuthenticatorCounter(credentialID, newCounter) {
35
- await conn.updateTable('Authenticator').set({ counter: newCounter }).where('credentialID', '=', credentialID).executeTakeFirstOrThrow();
36
- const authenticator = await adapter.getAuthenticator?.(credentialID);
37
- if (!authenticator)
38
- throw new Error('Authenticator not found');
39
- return authenticator;
40
- },
41
- });
41
+ await db.deleteFrom('sessions').where('sessions.id', '=', session.id).executeTakeFirstOrThrow();
42
+ throw new Error('Session expired');
42
43
  }
43
- /**
44
- * Login using credentials
45
- */
46
- export async function register(credentials) {
47
- const { email, password, name } = Registration.parse(credentials);
48
- const existing = await adapter.getUserByEmail?.(email);
49
- if (existing)
50
- throw 'User already exists';
51
- let id = crypto.randomUUID();
52
- while (await adapter.getUser?.(id))
53
- id = crypto.randomUUID();
54
- const salt = genSaltSync(10);
55
- const user = await adapter.createUser({
56
- id,
57
- name,
58
- email,
59
- emailVerified: null,
60
- salt: password ? salt : null,
61
- password: password ? hashSync(password, salt) : null,
62
- preferences: {},
63
- });
64
- const expires = new Date();
65
- expires.setMonth(expires.getMonth() + 1);
66
- const session = await adapter.createSession({
67
- sessionToken: randomBytes(64).toString('base64'),
68
- userId: id,
69
- expires,
70
- });
71
- return { user, session };
44
+ export async function getSessionAndUser(token) {
45
+ connect();
46
+ const result = await db
47
+ .selectFrom('sessions')
48
+ .selectAll()
49
+ .select(eb => jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('users.id', '=', 'sessions.userId')).as('user'))
50
+ .where('sessions.token', '=', token)
51
+ .executeTakeFirst();
52
+ if (!result)
53
+ throw new Error('Session not found');
54
+ await checkExpiration(result);
55
+ return result;
56
+ }
57
+ export async function getSession(sessionId) {
58
+ connect();
59
+ const session = await db.selectFrom('sessions').selectAll().where('id', '=', sessionId).executeTakeFirstOrThrow();
60
+ await checkExpiration(session);
61
+ return session;
62
+ }
63
+ export async function getSessions(userId) {
64
+ connect();
65
+ return await db.selectFrom('sessions').selectAll().where('userId', '=', userId).execute();
66
+ }
67
+ export async function updateSession(session) {
68
+ connect();
69
+ const query = db.updateTable('sessions').set(session).where('sessions.token', '=', session.token);
70
+ return await query.returningAll().executeTakeFirstOrThrow();
72
71
  }
73
72
  /**
74
- * Authorize using credentials
73
+ * Create a verification
74
+ * @param expires How long the token should be valid for in seconds
75
75
  */
76
- export async function authorize(credentials) {
77
- const { success, error, data } = Login.safeParse(credentials);
78
- if (!success)
79
- throw new CredentialsSignin(error);
80
- const user = await adapter.getUserByEmail?.(data.email);
81
- if (!user || !data.password || !user.salt)
82
- return null;
83
- if (user.password !== hashSync(data.password, user.salt))
76
+ export async function createVerification(role, userId, expires) {
77
+ const token = randomBytes(64).toString('base64url');
78
+ const verification = { userId, token, expires: new Date(Date.now() + expires * 1000), role };
79
+ connect();
80
+ await db.insertInto('verifications').values(verification).executeTakeFirstOrThrow();
81
+ setTimeout(() => {
82
+ void db.deleteFrom('verifications').where('verifications.token', '=', verification.token).execute();
83
+ }, expires * 1000);
84
+ return verification;
85
+ }
86
+ export async function useVerification(role, userId, token) {
87
+ connect();
88
+ const query = db
89
+ .deleteFrom('verifications')
90
+ .where('verifications.token', '=', token)
91
+ .where('verifications.userId', '=', userId)
92
+ .where('verifications.role', '=', role);
93
+ return await query.returningAll().executeTakeFirst();
94
+ }
95
+ export async function getPasskey(id) {
96
+ connect();
97
+ const result = await db.selectFrom('passkeys').selectAll().where('id', '=', id).executeTakeFirst();
98
+ if (!result)
84
99
  return null;
85
- return omit(user, 'password', 'salt');
100
+ return result;
86
101
  }
87
- export function getConfig() {
88
- createAdapter();
89
- const providers = [Passkey({})];
90
- if (config.auth.credentials) {
91
- providers.push(Credentials({
92
- credentials: {
93
- email: { label: 'Email', type: 'email' },
94
- password: { label: 'Password', type: 'password' },
95
- },
96
- authorize,
97
- }));
98
- }
99
- const debug = config.auth.debug ?? config.debug;
100
- return {
101
- adapter,
102
- providers,
103
- debug,
104
- experimental: { enableWebAuthn: true },
105
- secret: config.auth.secret,
106
- useSecureCookies: config.auth.secure_cookies,
107
- session: { strategy: 'database' },
108
- logger: {
109
- error(error) {
110
- logger.error('[auth] ' + error.message);
111
- },
112
- warn(code) {
113
- switch (code) {
114
- case 'experimental-webauthn':
115
- case 'debug-enabled':
116
- return;
117
- case 'csrf-disabled':
118
- logger.warn('CSRF protection is disabled.');
119
- break;
120
- case 'env-url-basepath-redundant':
121
- case 'env-url-basepath-mismatch':
122
- default:
123
- logger.warn('[auth] ' + code);
124
- }
125
- },
126
- debug(message, metadata) {
127
- debug && logger.debug('[auth]', message, metadata ? JSON.stringify(metadata, (k, v) => (k && JSON.stringify(v).length > 100 ? '...' : v)) : '');
128
- },
129
- },
130
- callbacks: {
131
- signIn({ user }) {
132
- logger.info('[auth] signin', user.id ?? '', user.email ? `(${user.email})` : '');
133
- return true;
134
- },
135
- },
136
- };
102
+ export async function createPasskey(passkey) {
103
+ connect();
104
+ const result = await db.insertInto('passkeys').values(passkey).returningAll().executeTakeFirstOrThrow();
105
+ return result;
106
+ }
107
+ export async function getPasskeysByUserId(userId) {
108
+ connect();
109
+ return await db.selectFrom('passkeys').selectAll().where('userId', '=', userId).execute();
110
+ }
111
+ export async function updatePasskeyCounter(id, newCounter) {
112
+ connect();
113
+ await db.updateTable('passkeys').set({ counter: newCounter }).where('id', '=', id).executeTakeFirstOrThrow();
114
+ const passkey = await getPasskey(id);
115
+ if (!passkey)
116
+ throw new Error('Passkey not found');
117
+ return passkey;
137
118
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { Argument, Option, program } from 'commander';
3
- import { randomBytes } from 'node:crypto';
4
3
  import { styleText } from 'node:util';
5
4
  import { getByString, isJSON, setByString } from 'utilium';
6
5
  import $pkg from '../package.json' with { type: 'json' };
@@ -8,6 +7,7 @@ import config from './config.js';
8
7
  import * as db from './database.js';
9
8
  import { _portActions, _portMethods, exit, handleError, output, restrictedPorts } from './io.js';
10
9
  import { loadDefaultPlugins, plugins, pluginText, resolvePlugin } from './plugins.js';
10
+ import { apps } from './apps.js';
11
11
  program
12
12
  .version($pkg.version)
13
13
  .name('axium')
@@ -25,10 +25,10 @@ program.hook('preAction', async function (_, action) {
25
25
  opt.force && output.warn('--force: Protections disabled.');
26
26
  if (opt.debug === false)
27
27
  config.set({ debug: false });
28
- if (!config.auth.secret) {
28
+ /* if (!config.auth.secret) {
29
29
  config.save({ auth: { secret: process.env.AUTH_SECRET || randomBytes(32).toString('base64') } }, true);
30
30
  output.debug('Auto-generated a new auth secret');
31
- }
31
+ } */
32
32
  });
33
33
  // Options shared by multiple (sub)commands
34
34
  const opts = {
@@ -78,7 +78,7 @@ axiumDB
78
78
  .action(async (opt) => {
79
79
  const stats = await db.status().catch(exit);
80
80
  if (!opt.force)
81
- for (const key of ['users', 'accounts', 'sessions']) {
81
+ for (const key of ['users', 'passkeys', 'sessions']) {
82
82
  if (stats[key] == 0)
83
83
  continue;
84
84
  output.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
@@ -94,7 +94,7 @@ axiumDB
94
94
  .action(async (opt) => {
95
95
  const stats = await db.status().catch(exit);
96
96
  if (!opt.force)
97
- for (const key of ['users', 'accounts', 'sessions']) {
97
+ for (const key of ['users', 'passkeys', 'sessions']) {
98
98
  if (stats[key] == 0)
99
99
  continue;
100
100
  output.warn(`Database has existing ${key}. Use --force if you really want to wipe the database.`);
@@ -103,6 +103,12 @@ axiumDB
103
103
  await db.wipe(opt).catch(exit);
104
104
  await db.database.destroy();
105
105
  });
106
+ axiumDB
107
+ .command('check')
108
+ .description('Check the structure of the database')
109
+ .action(async (opt) => {
110
+ await db.check(opt).catch(exit);
111
+ });
106
112
  const axiumConfig = program
107
113
  .command('config')
108
114
  .description('Manage the configuration')
@@ -191,6 +197,27 @@ axiumPlugin
191
197
  exit(`Can't find a plugin matching "${search}"`);
192
198
  console.log(pluginText(plugin));
193
199
  });
200
+ const axiumApps = program.command('apps').description('Manage Axium apps').addOption(opts.global);
201
+ axiumApps
202
+ .command('list')
203
+ .alias('ls')
204
+ .description('List apps added by plugins')
205
+ .option('-l, --long', 'use the long listing format')
206
+ .option('-b, --builtin', 'include built-in apps')
207
+ .action((opt) => {
208
+ if (!apps.size) {
209
+ console.log('No apps.');
210
+ return;
211
+ }
212
+ if (!opt.long) {
213
+ console.log(Array.from(apps.values().map(app => app.name)).join(', '));
214
+ return;
215
+ }
216
+ console.log(styleText('whiteBright', apps.size + ' app(s) loaded:'));
217
+ for (const app of apps.values()) {
218
+ console.log(app.name, styleText('dim', `(${app.id})`));
219
+ }
220
+ });
194
221
  program
195
222
  .command('status')
196
223
  .alias('stats')
@@ -234,7 +261,7 @@ program
234
261
  .addOption(opts.force)
235
262
  .addOption(opts.host)
236
263
  .action(async (opt) => {
237
- config.save({ auth: { secret: randomBytes(32).toString('base64') } }, true);
264
+ /* config.save({ auth: { secret: randomBytes(32).toString('base64') } }, true); */
238
265
  await db.init({ ...opt, skip: opt.dbSkip }).catch(handleError);
239
266
  await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(handleError);
240
267
  });