@axium/server 0.0.2 → 0.0.4

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/dist/auth.d.ts CHANGED
@@ -1,22 +1,28 @@
1
1
  import type { Adapter } from '@auth/core/adapters';
2
2
  import type { Provider } from '@auth/core/providers';
3
3
  import type { AuthConfig } from '@auth/core/types';
4
- import { Registration } from '@axium/core/api';
4
+ import { Registration } from '@axium/core/schemas';
5
5
  declare module '@auth/core/adapters' {
6
6
  interface AdapterUser {
7
- password: string;
8
- salt: string;
7
+ password: string | null;
8
+ salt: string | null;
9
9
  }
10
10
  }
11
11
  export declare let adapter: Adapter;
12
- export declare function createAdapter(): Adapter;
12
+ export declare function createAdapter(): void;
13
+ /**
14
+ * Login using credentials
15
+ */
13
16
  export declare function register(credentials: Registration): Promise<{
14
- user: import("@auth/core/adapters").AdapterUser | undefined;
15
- session: import("@auth/core/adapters").AdapterSession | undefined;
17
+ user: import("@auth/core/adapters").AdapterUser;
18
+ session: import("@auth/core/adapters").AdapterSession;
16
19
  }>;
17
- export declare function authorize(credentials: Partial<Record<string, unknown>>): Promise<Omit<import("@auth/core/adapters").AdapterUser, "password" | "salt"> | null>;
18
20
  /**
19
- * Get the providers current enabled in the Axium config.
21
+ * Authorize using credentials
20
22
  */
21
- export declare function getProviders(): Provider[];
22
- export declare function getConfig(): AuthConfig;
23
+ export declare function authorize(credentials: Partial<Record<string, unknown>>): Promise<Omit<import("@auth/core/adapters").AdapterUser, "password" | "salt"> | null>;
24
+ type Providers = Exclude<Provider, (...args: any[]) => any>[];
25
+ export declare function getConfig(): AuthConfig & {
26
+ providers: Providers;
27
+ };
28
+ export {};
package/dist/auth.js CHANGED
@@ -2,17 +2,19 @@ import { CredentialsSignin } from '@auth/core/errors';
2
2
  import Credentials from '@auth/core/providers/credentials';
3
3
  import Passkey from '@auth/core/providers/passkey';
4
4
  import { KyselyAdapter } from '@auth/kysely-adapter';
5
- import { Login, Registration } from '@axium/core/api';
5
+ import { Login, Registration } from '@axium/core/schemas';
6
6
  import { genSaltSync, hashSync } from 'bcryptjs';
7
7
  import { randomBytes } from 'node:crypto';
8
8
  import { omit } from 'utilium';
9
9
  import * as config from './config.js';
10
10
  import * as db from './database.js';
11
+ import { logger } from './io.js';
11
12
  export let adapter;
12
13
  export function createAdapter() {
14
+ if (adapter)
15
+ return;
13
16
  const conn = db.connect();
14
17
  adapter = Object.assign(KyselyAdapter(conn), {
15
- _axium: { config },
16
18
  async getAccount(providerAccountId, provider) {
17
19
  const result = await conn.selectFrom('Account').selectAll().where('providerAccountId', '=', providerAccountId).where('provider', '=', provider).executeTakeFirst();
18
20
  return result ?? null;
@@ -37,8 +39,10 @@ export function createAdapter() {
37
39
  return authenticator;
38
40
  },
39
41
  });
40
- return adapter;
41
42
  }
43
+ /**
44
+ * Login using credentials
45
+ */
42
46
  export async function register(credentials) {
43
47
  const { email, password, name } = Registration.parse(credentials);
44
48
  const existing = await adapter.getUserByEmail?.(email);
@@ -48,41 +52,40 @@ export async function register(credentials) {
48
52
  while (await adapter.getUser?.(id))
49
53
  id = crypto.randomUUID();
50
54
  const salt = genSaltSync(10);
51
- const user = await adapter.createUser?.({
55
+ const user = await adapter.createUser({
52
56
  id,
53
57
  name,
54
58
  email,
55
59
  emailVerified: null,
56
- salt,
57
- password: hashSync(password, salt),
60
+ salt: password ? salt : null,
61
+ password: password ? hashSync(password, salt) : null,
58
62
  });
59
63
  const expires = new Date();
60
64
  expires.setMonth(expires.getMonth() + 1);
61
- const session = await adapter.createSession?.({
65
+ const session = await adapter.createSession({
62
66
  sessionToken: randomBytes(64).toString('base64'),
63
67
  userId: id,
64
68
  expires,
65
69
  });
66
70
  return { user, session };
67
71
  }
72
+ /**
73
+ * Authorize using credentials
74
+ */
68
75
  export async function authorize(credentials) {
69
76
  const { success, error, data } = Login.safeParse(credentials);
70
77
  if (!success)
71
78
  throw new CredentialsSignin(error);
72
79
  const user = await adapter.getUserByEmail?.(data.email);
73
- if (!user)
80
+ if (!user || !data.password || !user.salt)
74
81
  return null;
75
82
  if (user.password !== hashSync(data.password, user.salt))
76
83
  return null;
77
84
  return omit(user, 'password', 'salt');
78
85
  }
79
- /**
80
- * Get the providers current enabled in the Axium config.
81
- */
82
- export function getProviders() {
83
- const providers = [];
84
- if (config.auth.passkeys)
85
- providers.push(Passkey);
86
+ export function getConfig() {
87
+ createAdapter();
88
+ const providers = [Passkey({})];
86
89
  if (config.auth.credentials) {
87
90
  providers.push(Credentials({
88
91
  credentials: {
@@ -92,19 +95,42 @@ export function getProviders() {
92
95
  authorize,
93
96
  }));
94
97
  }
95
- return providers;
96
- }
97
- export function getConfig() {
98
- createAdapter();
98
+ const debug = config.auth.debug ?? config.debug;
99
99
  return {
100
100
  adapter,
101
- providers: getProviders(),
102
- debug: config.auth.debug ?? config.debug,
101
+ providers,
102
+ debug,
103
103
  experimental: { enableWebAuthn: true },
104
104
  secret: config.auth.secret,
105
105
  useSecureCookies: config.auth.secure_cookies,
106
- session: {
107
- strategy: 'database',
106
+ session: { strategy: 'database' },
107
+ logger: {
108
+ error(error) {
109
+ logger.error('[auth] ' + error.message);
110
+ },
111
+ warn(code) {
112
+ switch (code) {
113
+ case 'experimental-webauthn':
114
+ case 'debug-enabled':
115
+ return;
116
+ case 'csrf-disabled':
117
+ logger.warn('CSRF protection is disabled.');
118
+ break;
119
+ case 'env-url-basepath-redundant':
120
+ case 'env-url-basepath-mismatch':
121
+ default:
122
+ logger.warn('[auth] ' + code);
123
+ }
124
+ },
125
+ debug(message, metadata) {
126
+ debug && logger.debug('[auth]', message, metadata ? JSON.stringify(metadata, (k, v) => (k && JSON.stringify(v).length > 100 ? '...' : v)) : '');
127
+ },
128
+ },
129
+ callbacks: {
130
+ signIn({ user }) {
131
+ logger.info('[auth] signin', user.id ?? '', user.email ? `(${user.email})` : '');
132
+ return true;
133
+ },
108
134
  },
109
135
  };
110
136
  }
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import chalk from 'chalk';
3
2
  import { Option, program } from 'commander';
3
+ import { styleText } from 'node:util';
4
4
  import { getByString, isJSON, pick, setByString } from 'utilium';
5
5
  import $pkg from '../package.json' with { type: 'json' };
6
6
  import * as config from './config.js';
7
7
  import * as db from './database.js';
8
- import { err, exit } from './io.js';
8
+ import { exit, output } from './io.js';
9
9
  program
10
10
  .version($pkg.version)
11
11
  .name('axium')
@@ -18,7 +18,7 @@ program.on('option:config', () => config.load(program.opts().config));
18
18
  program.hook('preAction', function (_, action) {
19
19
  config.loadDefaults();
20
20
  const opt = action.optsWithGlobals();
21
- opt.force && console.log(chalk.yellow('--force: Protections disabled.'));
21
+ opt.force && output.warn('--force: Protections disabled.');
22
22
  });
23
23
  // Options shared by multiple (sub)commands
24
24
  const opts = {
@@ -36,17 +36,17 @@ const axiumDB = program
36
36
  .description('manage the database')
37
37
  .option('-t, --timeout <ms>', 'how long to wait for commands to complete.', '1000')
38
38
  .addOption(opts.host);
39
- function db_output(state, message) {
39
+ function db_output(state, message = '') {
40
40
  switch (state) {
41
41
  case 'start':
42
42
  process.stdout.write(message + '... ');
43
43
  break;
44
44
  case 'log':
45
45
  case 'warn':
46
- process.stdout.write(chalk.yellow(message));
46
+ process.stdout.write(styleText('yellow', message));
47
47
  break;
48
48
  case 'error':
49
- process.stdout.write(chalk.red(message));
49
+ process.stdout.write(styleText('red', message));
50
50
  break;
51
51
  case 'done':
52
52
  console.log('done.');
@@ -70,10 +70,18 @@ axiumDB
70
70
  .command('status')
71
71
  .alias('stats')
72
72
  .description('check the status of the database')
73
- .action(async () => db
74
- .statusText()
75
- .then(console.log)
76
- .catch(() => exit('Unavailable')));
73
+ .action(async () => {
74
+ try {
75
+ console.log(await db.statusText());
76
+ }
77
+ catch {
78
+ output.error('Unavailable');
79
+ process.exitCode = 1;
80
+ }
81
+ finally {
82
+ await db.database.destroy();
83
+ }
84
+ });
77
85
  axiumDB
78
86
  .command('drop')
79
87
  .description('drop the database')
@@ -84,10 +92,27 @@ axiumDB
84
92
  for (const key of ['users', 'accounts', 'sessions']) {
85
93
  if (stats[key] == 0)
86
94
  continue;
87
- console.warn(chalk.yellow(`Database has existing ${key}. Use --force if you really want to drop the database.`));
95
+ output.warn(`Database has existing ${key}. Use --force if you really want to drop the database.`);
96
+ process.exit(2);
97
+ }
98
+ await db.uninstall({ ...opt, output: db_output }).catch(exit);
99
+ await db.database.destroy();
100
+ });
101
+ axiumDB
102
+ .command('wipe')
103
+ .description('wipe the database')
104
+ .addOption(opts.force)
105
+ .action(async (opt) => {
106
+ const stats = await db.status().catch(exit);
107
+ if (!opt.force)
108
+ for (const key of ['users', 'accounts', 'sessions']) {
109
+ if (stats[key] == 0)
110
+ continue;
111
+ output.warn(`Database has existing ${key}. Use --force if you really want to wipe the database.`);
88
112
  process.exit(2);
89
113
  }
90
- await db.remove({ ...opt, output: db_output }).catch(exit);
114
+ await db.wipe({ ...opt, output: db_output }).catch(exit);
115
+ await db.database.destroy();
91
116
  });
92
117
  const axiumConfig = program
93
118
  .command('config')
@@ -139,14 +164,17 @@ program
139
164
  .addOption(opts.host)
140
165
  .action(async () => {
141
166
  console.log('Axium Server v' + program.version());
142
- console.log('Debug mode:', config.debug ? chalk.yellow('enabled') : 'disabled');
167
+ console.log('Debug mode:', config.debug ? styleText('yellow', 'enabled') : 'disabled');
143
168
  console.log('Loaded config files:', config.files.keys().toArray().join(', '));
144
169
  process.stdout.write('Database: ');
145
- await db
146
- .statusText()
147
- .then(console.log)
148
- .catch(() => err('Unavailable'));
149
- console.log('Enabled auth providers:', config.authProviders.filter(provider => config.auth[provider]).join(', '));
170
+ try {
171
+ console.log(await db.statusText());
172
+ }
173
+ catch {
174
+ output.error('Unavailable');
175
+ }
176
+ await db.database.destroy();
177
+ console.log('Credentials authentication:', config.auth.credentials ? styleText('yellow', 'enabled') : 'disabled');
150
178
  });
151
179
  program
152
180
  .command('init')
package/dist/config.d.ts CHANGED
@@ -24,43 +24,47 @@ export declare const db: Database;
24
24
  export declare const Auth: z.ZodObject<{
25
25
  credentials: z.ZodBoolean;
26
26
  debug: z.ZodOptional<z.ZodBoolean>;
27
- passkeys: z.ZodBoolean;
28
27
  secret: z.ZodString;
29
28
  secure_cookies: z.ZodBoolean;
30
29
  }, "strip", z.ZodTypeAny, {
31
30
  credentials: boolean;
32
- passkeys: boolean;
33
31
  secret: string;
34
32
  secure_cookies: boolean;
35
33
  debug?: boolean | undefined;
36
34
  }, {
37
35
  credentials: boolean;
38
- passkeys: boolean;
39
36
  secret: string;
40
37
  secure_cookies: boolean;
41
38
  debug?: boolean | undefined;
42
39
  }>;
43
40
  export type Auth = z.infer<typeof Auth>;
44
41
  export declare const auth: Auth;
45
- export declare const authProviders: readonly ["credentials", "passkeys"];
46
- export type AuthProvider = (typeof authProviders)[number];
42
+ export declare const Log: z.ZodObject<{
43
+ level: z.ZodEnum<["error", "warn", "notice", "info", "debug"]>;
44
+ console: z.ZodBoolean;
45
+ }, "strip", z.ZodTypeAny, {
46
+ level: "debug" | "error" | "warn" | "notice" | "info";
47
+ console: boolean;
48
+ }, {
49
+ level: "debug" | "error" | "warn" | "notice" | "info";
50
+ console: boolean;
51
+ }>;
52
+ export type Log = z.infer<typeof Log>;
53
+ export declare const log: Log;
47
54
  export declare const Config: z.ZodObject<{
48
55
  auth: z.ZodObject<{
49
56
  credentials: z.ZodOptional<z.ZodBoolean>;
50
57
  debug: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
51
- passkeys: z.ZodOptional<z.ZodBoolean>;
52
58
  secret: z.ZodOptional<z.ZodString>;
53
59
  secure_cookies: z.ZodOptional<z.ZodBoolean>;
54
60
  }, "strip", z.ZodTypeAny, {
55
61
  credentials?: boolean | undefined;
56
62
  debug?: boolean | undefined;
57
- passkeys?: boolean | undefined;
58
63
  secret?: string | undefined;
59
64
  secure_cookies?: boolean | undefined;
60
65
  }, {
61
66
  credentials?: boolean | undefined;
62
67
  debug?: boolean | undefined;
63
- passkeys?: boolean | undefined;
64
68
  secret?: string | undefined;
65
69
  secure_cookies?: boolean | undefined;
66
70
  }>;
@@ -84,12 +88,21 @@ export declare const Config: z.ZodObject<{
84
88
  user?: string | undefined;
85
89
  database?: string | undefined;
86
90
  }>;
91
+ log: z.ZodObject<{
92
+ level: z.ZodOptional<z.ZodEnum<["error", "warn", "notice", "info", "debug"]>>;
93
+ console: z.ZodOptional<z.ZodBoolean>;
94
+ }, "strip", z.ZodTypeAny, {
95
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
96
+ console?: boolean | undefined;
97
+ }, {
98
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
99
+ console?: boolean | undefined;
100
+ }>;
87
101
  }, "strip", z.ZodTypeAny, {
88
102
  debug: boolean;
89
103
  auth: {
90
104
  credentials?: boolean | undefined;
91
105
  debug?: boolean | undefined;
92
- passkeys?: boolean | undefined;
93
106
  secret?: string | undefined;
94
107
  secure_cookies?: boolean | undefined;
95
108
  };
@@ -100,12 +113,15 @@ export declare const Config: z.ZodObject<{
100
113
  user?: string | undefined;
101
114
  database?: string | undefined;
102
115
  };
116
+ log: {
117
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
118
+ console?: boolean | undefined;
119
+ };
103
120
  }, {
104
121
  debug: boolean;
105
122
  auth: {
106
123
  credentials?: boolean | undefined;
107
124
  debug?: boolean | undefined;
108
- passkeys?: boolean | undefined;
109
125
  secret?: string | undefined;
110
126
  secure_cookies?: boolean | undefined;
111
127
  };
@@ -116,24 +132,25 @@ export declare const Config: z.ZodObject<{
116
132
  user?: string | undefined;
117
133
  database?: string | undefined;
118
134
  };
135
+ log: {
136
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
137
+ console?: boolean | undefined;
138
+ };
119
139
  }>;
120
140
  export declare const File: z.ZodObject<z.objectUtil.extendShape<{
121
141
  auth: z.ZodOptional<z.ZodObject<{
122
142
  credentials: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
123
143
  debug: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodBoolean>>>;
124
- passkeys: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
125
144
  secret: z.ZodOptional<z.ZodOptional<z.ZodString>>;
126
145
  secure_cookies: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
127
146
  }, "strip", z.ZodTypeAny, {
128
147
  credentials?: boolean | undefined;
129
148
  debug?: boolean | undefined;
130
- passkeys?: boolean | undefined;
131
149
  secret?: string | undefined;
132
150
  secure_cookies?: boolean | undefined;
133
151
  }, {
134
152
  credentials?: boolean | undefined;
135
153
  debug?: boolean | undefined;
136
- passkeys?: boolean | undefined;
137
154
  secret?: string | undefined;
138
155
  secure_cookies?: boolean | undefined;
139
156
  }>>;
@@ -157,6 +174,16 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
157
174
  user?: string | undefined;
158
175
  database?: string | undefined;
159
176
  }>>;
177
+ log: z.ZodOptional<z.ZodObject<{
178
+ level: z.ZodOptional<z.ZodOptional<z.ZodEnum<["error", "warn", "notice", "info", "debug"]>>>;
179
+ console: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
180
+ }, "strip", z.ZodTypeAny, {
181
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
182
+ console?: boolean | undefined;
183
+ }, {
184
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
185
+ console?: boolean | undefined;
186
+ }>>;
160
187
  }, {
161
188
  include: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
162
189
  }>, "strip", z.ZodTypeAny, {
@@ -164,7 +191,6 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
164
191
  auth?: {
165
192
  credentials?: boolean | undefined;
166
193
  debug?: boolean | undefined;
167
- passkeys?: boolean | undefined;
168
194
  secret?: string | undefined;
169
195
  secure_cookies?: boolean | undefined;
170
196
  } | undefined;
@@ -175,13 +201,16 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
175
201
  user?: string | undefined;
176
202
  database?: string | undefined;
177
203
  } | undefined;
204
+ log?: {
205
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
206
+ console?: boolean | undefined;
207
+ } | undefined;
178
208
  include?: string[] | undefined;
179
209
  }, {
180
210
  debug?: boolean | undefined;
181
211
  auth?: {
182
212
  credentials?: boolean | undefined;
183
213
  debug?: boolean | undefined;
184
- passkeys?: boolean | undefined;
185
214
  secret?: string | undefined;
186
215
  secure_cookies?: boolean | undefined;
187
216
  } | undefined;
@@ -192,6 +221,10 @@ export declare const File: z.ZodObject<z.objectUtil.extendShape<{
192
221
  user?: string | undefined;
193
222
  database?: string | undefined;
194
223
  } | undefined;
224
+ log?: {
225
+ level?: "debug" | "error" | "warn" | "notice" | "info" | undefined;
226
+ console?: boolean | undefined;
227
+ } | undefined;
195
228
  include?: string[] | undefined;
196
229
  }>;
197
230
  export type File = z.infer<typeof File>;
package/dist/config.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
- import chalk from 'chalk';
2
+ import { levelText } from 'logzen';
3
3
  import { readFileSync, writeFileSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path/posix';
5
5
  import { assignWithDefaults } from 'utilium';
6
6
  import * as z from 'zod';
7
- import { findDir } from './io.js';
7
+ import { findDir, logger, output } from './io.js';
8
8
  export const Database = z.object({
9
9
  host: z.string(),
10
10
  port: z.number(),
@@ -22,22 +22,29 @@ export const db = {
22
22
  export const Auth = z.object({
23
23
  credentials: z.boolean(),
24
24
  debug: z.boolean().optional(),
25
- passkeys: z.boolean(),
26
25
  secret: z.string(),
27
26
  secure_cookies: z.boolean(),
28
27
  });
29
28
  export const auth = {
30
- credentials: true,
31
- passkeys: true,
29
+ credentials: false,
32
30
  secret: '',
33
31
  secure_cookies: false,
34
32
  };
35
- export const authProviders = ['credentials', 'passkeys'];
33
+ export const Log = z.object({
34
+ level: z.enum(levelText),
35
+ console: z.boolean(),
36
+ });
37
+ export const log = {
38
+ level: 'info',
39
+ console: true,
40
+ };
36
41
  export const Config = z.object({
37
42
  auth: Auth.partial(),
38
43
  debug: z.boolean(),
39
44
  db: Database.partial(),
45
+ log: Log.partial(),
40
46
  });
47
+ // config from file
41
48
  export const File = Config.deepPartial().extend({
42
49
  include: z.array(z.string()).optional(),
43
50
  });
@@ -47,6 +54,7 @@ export function get() {
47
54
  auth,
48
55
  db,
49
56
  debug,
57
+ log,
50
58
  };
51
59
  }
52
60
  /**
@@ -56,6 +64,10 @@ export function set(config) {
56
64
  assignWithDefaults(auth, config.auth ?? {});
57
65
  debug = config.debug ?? debug;
58
66
  assignWithDefaults(db, config.db ?? {});
67
+ assignWithDefaults(log, config.log ?? {});
68
+ logger.detach(output);
69
+ if (log.console)
70
+ logger.attach(output, { output: log.level });
59
71
  }
60
72
  export const files = new Map();
61
73
  /**
@@ -69,7 +81,7 @@ export function load(path, options = {}) {
69
81
  catch (e) {
70
82
  if (!options.optional)
71
83
  throw e;
72
- debug && console.debug(chalk.gray(`Skipping config at ${path} (${e.message})`));
84
+ logger.debug(`Skipping config at ${path} (${e.message})`);
73
85
  return;
74
86
  }
75
87
  const config = options.strict ? File.parse(json) : json;
@@ -95,7 +107,7 @@ export function saveTo(path, changed) {
95
107
  set(changed);
96
108
  const config = files.get(path) ?? {};
97
109
  Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
98
- debug && console.debug(chalk.gray(`Wrote config to ${path}`));
110
+ logger.debug(`Wrote config to ${path}`);
99
111
  writeFileSync(path, JSON.stringify(config));
100
112
  }
101
113
  /**
@@ -73,4 +73,8 @@ export declare function init(opt: InitOptions): Promise<config.Database>;
73
73
  /**
74
74
  * Completely remove Axium from the database.
75
75
  */
76
- export declare function remove(opt: OpOptions): Promise<void>;
76
+ export declare function uninstall(opt: OpOptions): Promise<void>;
77
+ /**
78
+ * Removes all data from tables.
79
+ */
80
+ export declare function wipe(opt: OpOptions): Promise<void>;
package/dist/database.js CHANGED
@@ -55,7 +55,7 @@ import { exec } from 'node:child_process';
55
55
  import { randomBytes } from 'node:crypto';
56
56
  import pg from 'pg';
57
57
  import * as config from './config.js';
58
- import { verbose } from './io.js';
58
+ import { logger } from './io.js';
59
59
  export let database;
60
60
  export function connect() {
61
61
  if (database)
@@ -71,24 +71,12 @@ export function connect() {
71
71
  return database;
72
72
  }
73
73
  export async function status() {
74
- const env_1 = { stack: [], error: void 0, hasError: false };
75
- try {
76
- const db = __addDisposableResource(env_1, connect(), true);
77
- return {
78
- users: (await db.selectFrom('User').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
79
- accounts: (await db.selectFrom('Account').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
80
- sessions: (await db.selectFrom('Session').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
81
- };
82
- }
83
- catch (e_1) {
84
- env_1.error = e_1;
85
- env_1.hasError = true;
86
- }
87
- finally {
88
- const result_1 = __disposeResources(env_1);
89
- if (result_1)
90
- await result_1;
91
- }
74
+ const db = connect();
75
+ return {
76
+ users: (await db.selectFrom('User').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
77
+ accounts: (await db.selectFrom('Account').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
78
+ sessions: (await db.selectFrom('Session').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
79
+ };
92
80
  }
93
81
  export async function statusText() {
94
82
  try {
@@ -141,12 +129,12 @@ function shouldRecreate(opt) {
141
129
  throw 2;
142
130
  }
143
131
  export async function init(opt) {
144
- const env_2 = { stack: [], error: void 0, hasError: false };
132
+ const env_1 = { stack: [], error: void 0, hasError: false };
145
133
  try {
146
134
  _fixOutput(opt);
147
135
  if (!config.db.password) {
148
136
  config.save({ db: { password: randomBytes(32).toString('base64') } }, true);
149
- verbose('Generated password and wrote to global config');
137
+ logger.debug('Generated password and wrote to global config');
150
138
  }
151
139
  const _sql = (command, message) => execSQL(opt, command, message);
152
140
  await _sql('CREATE DATABASE axium', 'Creating database').catch(async (error) => {
@@ -171,7 +159,7 @@ export async function init(opt) {
171
159
  await _sql('GRANT ALL PRIVILEGES ON SCHEMA public TO axium', 'Granting schema privileges');
172
160
  await _sql('ALTER DATABASE axium OWNER TO axium', 'Setting database owner');
173
161
  await _sql('SELECT pg_reload_conf()', 'Reloading configuration');
174
- const db = __addDisposableResource(env_2, connect(), true);
162
+ const db = __addDisposableResource(env_1, connect(), true);
175
163
  const relationExists = (table) => (error) => {
176
164
  error = typeof error == 'object' && 'message' in error ? error.message : error;
177
165
  if (error == `relation "${table}" already exists`)
@@ -254,22 +242,34 @@ export async function init(opt) {
254
242
  opt.output('done');
255
243
  return config.db;
256
244
  }
257
- catch (e_2) {
258
- env_2.error = e_2;
259
- env_2.hasError = true;
245
+ catch (e_1) {
246
+ env_1.error = e_1;
247
+ env_1.hasError = true;
260
248
  }
261
249
  finally {
262
- const result_2 = __disposeResources(env_2);
263
- if (result_2)
264
- await result_2;
250
+ const result_1 = __disposeResources(env_1);
251
+ if (result_1)
252
+ await result_1;
265
253
  }
266
254
  }
267
255
  /**
268
256
  * Completely remove Axium from the database.
269
257
  */
270
- export async function remove(opt) {
258
+ export async function uninstall(opt) {
271
259
  _fixOutput(opt);
272
260
  await execSQL(opt, 'DROP DATABASE axium', 'Dropping database');
273
261
  await execSQL(opt, 'REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
274
262
  await execSQL(opt, 'DROP USER axium', 'Dropping user');
275
263
  }
264
+ /**
265
+ * Removes all data from tables.
266
+ */
267
+ export async function wipe(opt) {
268
+ _fixOutput(opt);
269
+ const db = connect();
270
+ for (const table of ['User', 'Account', 'Session', 'VerificationToken', 'Authenticator']) {
271
+ opt.output('start', `Removing data from ${table}`);
272
+ await db.deleteFrom(table).execute();
273
+ opt.output('done');
274
+ }
275
+ }
package/dist/io.d.ts CHANGED
@@ -1,15 +1,21 @@
1
- /** Convenience function for `example... [Done. / error]` */
2
- export declare function report<T>(promise: Promise<T>, message: string, success?: string): Promise<T>;
3
- export declare function err(message: string | Error): void;
4
- /** Yet another convenience function */
5
- export declare function exit(message: string | Error, code?: number): never;
6
- export declare function verbose(...message: any[]): void;
1
+ import { Logger } from 'logzen';
7
2
  /**
8
3
  * Find the Axium directory.
9
4
  * This directory includes things like config files, secrets, etc.
10
5
  */
11
6
  export declare function findDir(global: boolean): string;
12
- /**
13
- *
14
- */
15
- export declare function checkDir(path: string): void;
7
+ export declare const logger: Logger;
8
+ export declare const output: {
9
+ constructor: {
10
+ name: string;
11
+ };
12
+ error(message: string): void;
13
+ warn(message: string): void;
14
+ info(message: string): void;
15
+ log(message: string): void;
16
+ debug(message: string): void;
17
+ };
18
+ /** Yet another convenience function */
19
+ export declare function exit(message: string | Error, code?: number): never;
20
+ /** Convenience function for `example... [done. / error]` */
21
+ export declare function report<T>(promise: Promise<T>, message: string, success?: string): Promise<T>;
package/dist/io.js CHANGED
@@ -1,35 +1,8 @@
1
- import chalk from 'chalk';
2
- import { existsSync, mkdirSync } from 'node:fs';
1
+ import { Logger } from 'logzen';
2
+ import { mkdirSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path/posix';
5
- import { debug } from './config.js';
6
- /** Convenience function for `example... [Done. / error]` */
7
- export async function report(promise, message, success = 'done.') {
8
- process.stdout.write(message + '... ');
9
- try {
10
- const result = await promise;
11
- console.log(success);
12
- return result;
13
- }
14
- catch (error) {
15
- throw typeof error == 'object' && 'message' in error ? error.message : error;
16
- }
17
- }
18
- export function err(message) {
19
- if (message instanceof Error)
20
- message = message.message;
21
- console.error(message.startsWith('\x1b') ? message : chalk.red(message));
22
- }
23
- /** Yet another convenience function */
24
- export function exit(message, code = 1) {
25
- err(message);
26
- process.exit(code);
27
- }
28
- export function verbose(...message) {
29
- if (!debug)
30
- return;
31
- console.debug(chalk.gray(message));
32
- }
5
+ import { styleText } from 'node:util';
33
6
  /**
34
7
  * Find the Axium directory.
35
8
  * This directory includes things like config files, secrets, etc.
@@ -37,19 +10,54 @@ export function verbose(...message) {
37
10
  export function findDir(global) {
38
11
  if (process.env.AXIUM_DIR)
39
12
  return process.env.AXIUM_DIR;
40
- if (process.getuid?.() === 0)
13
+ if (global && process.getuid?.() === 0)
41
14
  return '/etc/axium';
42
15
  if (global)
43
16
  return join(homedir(), '.axium');
44
17
  return '.axium';
45
18
  }
46
- /**
47
- *
48
- */
49
- export function checkDir(path) {
50
- if (existsSync(path))
51
- return;
52
- mkdirSync(path, { recursive: true });
53
- }
54
19
  if (process.getuid?.() === 0)
55
- checkDir('/etc/axium');
20
+ mkdirSync('/etc/axium', { recursive: true });
21
+ mkdirSync(findDir(false), { recursive: true });
22
+ export const logger = new Logger({
23
+ hideWarningStack: true,
24
+ noGlobalConsole: true,
25
+ });
26
+ export const output = {
27
+ constructor: { name: 'Console' },
28
+ error(message) {
29
+ console.error(message.startsWith('\x1b') ? message : styleText('red', message));
30
+ },
31
+ warn(message) {
32
+ console.warn(message.startsWith('\x1b') ? message : styleText('yellow', message));
33
+ },
34
+ info(message) {
35
+ console.info(message.startsWith('\x1b') ? message : styleText('blue', message));
36
+ },
37
+ log(message) {
38
+ console.log(message);
39
+ },
40
+ debug(message) {
41
+ console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
42
+ },
43
+ };
44
+ logger.attach(output);
45
+ /** Yet another convenience function */
46
+ export function exit(message, code = 1) {
47
+ if (message instanceof Error)
48
+ message = message.message;
49
+ output.error(message);
50
+ process.exit(code);
51
+ }
52
+ /** Convenience function for `example... [done. / error]` */
53
+ export async function report(promise, message, success = 'done.') {
54
+ process.stdout.write(message + '... ');
55
+ try {
56
+ const result = await promise;
57
+ console.log(success);
58
+ return result;
59
+ }
60
+ catch (error) {
61
+ throw typeof error == 'object' && 'message' in error ? error.message : error;
62
+ }
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -17,8 +17,15 @@
17
17
  "type": "module",
18
18
  "main": "dist/index.js",
19
19
  "types": "dist/index.d.ts",
20
+ "exports": {
21
+ ".": "./dist/index.js",
22
+ "./*": "./dist/*",
23
+ "./web": "./web/index.js",
24
+ "./web/*": "./web/*"
25
+ },
20
26
  "files": [
21
- "dist"
27
+ "dist",
28
+ "web"
22
29
  ],
23
30
  "bin": {
24
31
  "axium": "dist/cli.js"
@@ -34,9 +41,9 @@
34
41
  "@auth/kysely-adapter": "^1.8.0",
35
42
  "@types/pg": "^8.11.11",
36
43
  "bcryptjs": "^3.0.2",
37
- "chalk": "^5.4.1",
38
44
  "commander": "^13.1.0",
39
45
  "kysely": "^0.27.5",
46
+ "logzen": "^0.6.2",
40
47
  "pg": "^8.14.1",
41
48
  "utilium": "^2.2.3"
42
49
  },
package/web/app.html ADDED
@@ -0,0 +1,15 @@
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="/axium.css" />
9
+ %sveltekit.head%
10
+ </head>
11
+
12
+ <body>
13
+ <div style="display: contents">%sveltekit.body%</div>
14
+ </body>
15
+ </html>
package/web/auth.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { SvelteKitAuth } from '@auth/sveltekit';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { join } from 'node:path/posix';
4
+ import { getConfig } from '../src/auth.js';
5
+ import { loadDefaults as loadDefaultConfigs } from '../src/config.js';
6
+ import { findDir, logger } from '../src/io.js';
7
+ import { allLogLevels } from 'logzen';
8
+
9
+ logger.attach(createWriteStream(join(findDir(false), 'server.log')), { output: allLogLevels });
10
+ loadDefaultConfigs();
11
+
12
+ export const { handle, signIn, signOut } = SvelteKitAuth(getConfig());
@@ -0,0 +1 @@
1
+ export { handle } from './auth.js';
package/web/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './auth.js';
2
+ export { default as Icon } from './lib/Icon.svelte';
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+
4
+ const { id, style = 'solid' } = $props();
5
+
6
+ const href = `https://site-assets.fontawesome.com/releases/v6.7.2/svgs/${style}/${id}.svg`;
7
+
8
+ let content = $state('');
9
+
10
+ // Fetch and inline the SVG content on component mount
11
+ onMount(async () => {
12
+ const res = await fetch(href);
13
+
14
+ if (!res.ok) {
15
+ console.error('Failed to fetch icon:', res.statusText);
16
+ return;
17
+ }
18
+
19
+ const text = await res.text();
20
+
21
+ const doc = new DOMParser().parseFromString(text, 'image/svg+xml');
22
+ const errorNode = doc.querySelector('parsererror');
23
+
24
+ if (errorNode || doc.documentElement?.nodeName != 'svg') {
25
+ console.error('Invalid SVG');
26
+ return;
27
+ }
28
+
29
+ content = text;
30
+ });
31
+ </script>
32
+
33
+ <span>{@html content}</span>
34
+
35
+ <style>
36
+ span {
37
+ width: 1em;
38
+ height: 1em;
39
+ display: inline-block;
40
+ fill: #bbb;
41
+ }
42
+ </style>
@@ -0,0 +1,14 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ import { adapter } from '../../src/auth.js';
3
+ import type { PageServerLoadEvent } from './$types';
4
+
5
+ export async function load(event: PageServerLoadEvent) {
6
+ const session = await event.locals.auth();
7
+
8
+ if (!session) redirect(307, '/auth/signin');
9
+ if (!session.user.name) redirect(307, '/edit/name');
10
+
11
+ const user = await adapter.getUserByEmail(session.user.email);
12
+
13
+ return { session, user };
14
+ }
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import Icon from '$lib/Icon.svelte';
4
+ import { getUserImage } from '@axium/core';
5
+
6
+ const { user } = page.data;
7
+
8
+ const image = getUserImage(user);
9
+ </script>
10
+
11
+ <svelte:head>
12
+ <title>Axium Account</title>
13
+ </svelte:head>
14
+
15
+ <div id="content">
16
+ <img id="pfp" src={image} alt="User profile" />
17
+ <p id="greeting">Welcome, {user.name}</p>
18
+ <div class="main">
19
+ <div>
20
+ <p>Name</p>
21
+ <p>{user.name}</p>
22
+ <a href="/edit/name"><Icon id="chevron-right" /></a>
23
+ </div>
24
+ <div>
25
+ <p>Email</p>
26
+ <p>{user.email}</p>
27
+ <a href="/edit/email"><Icon id="chevron-right" /></a>
28
+ </div>
29
+ <div>
30
+ <p>User ID <dfn title="This is your UUID."><Icon id="circle-info" style="regular" /></dfn></p>
31
+ <p>{user.id}</p>
32
+ </div>
33
+ <a id="signout" href="/auth/signout"><button>Sign out</button></a>
34
+ </div>
35
+ </div>
36
+
37
+ <style>
38
+ #content {
39
+ display: flex;
40
+ align-items: center;
41
+ flex-direction: column;
42
+ }
43
+
44
+ #pfp {
45
+ width: 100px;
46
+ height: 100px;
47
+ border-radius: 50%;
48
+ border: 1px solid #8888;
49
+ margin: 3em auto 2em;
50
+ }
51
+
52
+ #greeting {
53
+ font-size: 2em;
54
+ margin-bottom: 1em;
55
+ }
56
+
57
+ .main {
58
+ width: 50%;
59
+ padding-top: 4em;
60
+
61
+ > div:has(+ div) {
62
+ border-bottom: 1px solid #8888;
63
+ }
64
+ }
65
+
66
+ .main > div {
67
+ display: grid;
68
+ grid-template-columns: 10em 1fr 2em;
69
+ align-items: center;
70
+ width: 100%;
71
+ gap: 1em;
72
+ text-wrap: nowrap;
73
+ padding-bottom: 1em;
74
+
75
+ > :first-child {
76
+ margin: 0 5em 0 1em;
77
+ color: #bbbb;
78
+ font-size: 0.9em;
79
+ grid-column: 1;
80
+ }
81
+
82
+ > :nth-child(2) {
83
+ margin: 0;
84
+ grid-column: 2;
85
+ }
86
+
87
+ > a:last-child {
88
+ margin: 0;
89
+ display: inline;
90
+ grid-column: 3;
91
+ font-size: 0.75em;
92
+ cursor: pointer;
93
+ }
94
+ }
95
+
96
+ #signout {
97
+ margin-top: 2em;
98
+ }
99
+ </style>
@@ -0,0 +1,35 @@
1
+ import { fail, redirect, type Actions } from '@sveltejs/kit';
2
+ import { adapter } from '../../../../src/auth';
3
+ import * as z from 'zod';
4
+ import type { PageServerLoadEvent } from './$types';
5
+
6
+ export async function load(event: PageServerLoadEvent) {
7
+ const session = await event.locals.auth();
8
+ if (!session) redirect(307, '/auth/signin');
9
+ return { session };
10
+ }
11
+
12
+ export const actions = {
13
+ async default(event) {
14
+ const session = await event.locals.auth();
15
+
16
+ const rawEmail = (await event.request.formData()).get('email');
17
+ const { data: email, success, error } = z.string().email().safeParse(rawEmail);
18
+
19
+ if (!success)
20
+ return fail(400, {
21
+ email,
22
+ error: error.flatten().formErrors[0] || Object.values(error.flatten().fieldErrors).flat()[0],
23
+ });
24
+
25
+ const user = await adapter.getUserByEmail(session.user.email);
26
+ if (!user) return fail(500, { email, error: 'User does not exist' });
27
+
28
+ try {
29
+ await adapter.updateUser({ id: user.id, email, image: user.image });
30
+ } catch (error: any) {
31
+ return fail(400, { email, error: typeof error === 'string' ? error : error.message });
32
+ }
33
+ redirect(303, '/');
34
+ },
35
+ } satisfies Actions;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { enhance } from '$app/forms';
3
+ import { page } from '$app/state';
4
+ const { user } = page.data.session;
5
+ let { form } = $props();
6
+ </script>
7
+
8
+ <svelte:head>
9
+ <title>Edit Email</title>
10
+ </svelte:head>
11
+
12
+ <div>
13
+ <form method="POST" class="main" use:enhance>
14
+ {#if form?.error}
15
+ <div class="error">
16
+ {typeof form.error === 'string' ? form.error : JSON.stringify(form.error)}
17
+ </div>
18
+ {/if}
19
+ <div>
20
+ <label for="email">Email Address</label>
21
+ <input name="email" type="email" value={form?.email || user.email || ''} required />
22
+ </div>
23
+ <button type="submit">Continue</button>
24
+ </form>
25
+ </div>
@@ -0,0 +1,35 @@
1
+ import { Name } from '@axium/core/schemas';
2
+ import { fail, redirect, type Actions } from '@sveltejs/kit';
3
+ import { adapter } from '../../../../src/auth';
4
+ import type { PageServerLoadEvent } from './$types';
5
+
6
+ export async function load(event: PageServerLoadEvent) {
7
+ const session = await event.locals.auth();
8
+ if (!session) redirect(307, '/auth/signin');
9
+ return { session };
10
+ }
11
+
12
+ export const actions = {
13
+ async default(event) {
14
+ const session = await event.locals.auth();
15
+
16
+ const rawName = (await event.request.formData()).get('name');
17
+ const { data: name, success, error } = Name.safeParse(rawName);
18
+
19
+ if (!success)
20
+ return fail(400, {
21
+ name,
22
+ error: error.flatten().formErrors[0] || Object.values(error.flatten().fieldErrors).flat()[0],
23
+ });
24
+
25
+ const user = await adapter.getUserByEmail(session.user.email);
26
+ if (!user) return fail(500, { name, error: 'User does not exist' });
27
+
28
+ try {
29
+ await adapter.updateUser({ id: user.id, name, image: user.image });
30
+ } catch (error: any) {
31
+ return fail(400, { name, error: typeof error === 'string' ? error : error.message });
32
+ }
33
+ redirect(303, '/');
34
+ },
35
+ } satisfies Actions;
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { enhance } from '$app/forms';
3
+ import { page } from '$app/state';
4
+ const { user } = page.data.session;
5
+ let { form } = $props();
6
+ </script>
7
+
8
+ <svelte:head>
9
+ <title>Edit Name</title>
10
+ </svelte:head>
11
+
12
+ <div>
13
+ <form method="POST" class="main" use:enhance>
14
+ {#if form?.error}
15
+ <div class="error">
16
+ {typeof form.error === 'string' ? form.error : JSON.stringify(form.error)}
17
+ </div>
18
+ {/if}
19
+ <div>
20
+ <label for="name">What do you want to be called?</label>
21
+ <input name="name" type="text" value={form?.name || user.name || ''} required />
22
+ </div>
23
+ <button type="submit">Continue</button>
24
+ </form>
25
+ </div>
@@ -0,0 +1,33 @@
1
+ import { Registration } from '@axium/core/schemas';
2
+ import { fail, redirect } from '@sveltejs/kit';
3
+ import * as auth from '../../../src/auth.js';
4
+ import * as config from '../../../src/config.js';
5
+ import type { Actions } from './$types';
6
+
7
+ export function load() {
8
+ if (!config.auth.credentials) return redirect(307, '/auth/signin');
9
+ }
10
+
11
+ export const actions = {
12
+ async default(event) {
13
+ const { data, success, error } = Registration.safeParse(Object.fromEntries(await event.request.formData()));
14
+
15
+ if (!success)
16
+ return fail(400, {
17
+ ...data,
18
+ error: error.flatten().formErrors[0] || Object.values(error.flatten().fieldErrors).flat()[0],
19
+ });
20
+
21
+ try {
22
+ const { session } = await auth.register(data);
23
+ event.cookies.set('session', session.sessionToken, {
24
+ path: '/',
25
+ expires: session.expires,
26
+ httpOnly: true,
27
+ });
28
+ return { ...data, success: true, data: session.sessionToken };
29
+ } catch (error: any) {
30
+ return fail(400, { ...data, error: typeof error === 'string' ? error : error.message });
31
+ }
32
+ },
33
+ } satisfies Actions;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { enhance } from '$app/forms';
3
+ let { form } = $props();
4
+ </script>
5
+
6
+ <div>
7
+ <form method="POST" class="main" use:enhance>
8
+ {#if form?.error}
9
+ <div class="error">
10
+ {typeof form.error === 'string' ? form.error : JSON.stringify(form.error)}
11
+ </div>
12
+ {/if}
13
+ <div>
14
+ <label for="name">Display Name</label>
15
+ <input name="name" type="text" value={form?.name || ''} required />
16
+ </div>
17
+ <div>
18
+ <label for="email">Email</label>
19
+ <input name="email" type="email" value={form?.email || ''} required />
20
+ </div>
21
+ <div>
22
+ <label for="password">Password</label>
23
+ <input name="password" type="password" />
24
+ </div>
25
+ <button type="submit">Register</button>
26
+ </form>
27
+ </div>
@@ -0,0 +1,67 @@
1
+ body {
2
+ position: fixed;
3
+ inset: 0;
4
+ font-family: sans-serif;
5
+ font-size: 16px;
6
+ background-color: #222;
7
+ color: #bbb;
8
+ accent-color: #bbb;
9
+ }
10
+
11
+ .main {
12
+ padding: 2em;
13
+ border-radius: 1em;
14
+ background-color: #111;
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: 1em;
18
+ }
19
+
20
+ div:has(form.main) {
21
+ position: absolute;
22
+ inset: 0;
23
+ display: flex;
24
+ justify-content: center;
25
+ align-items: center;
26
+ }
27
+
28
+ form.main {
29
+ width: max-content;
30
+ height: max-content;
31
+ }
32
+
33
+ form {
34
+ div {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 0;
38
+ }
39
+ }
40
+
41
+ input,
42
+ button {
43
+ border-radius: 0.5em;
44
+ border: 1px solid #aaa;
45
+ background-color: #222;
46
+ }
47
+
48
+ input {
49
+ padding: 0.5em 1em;
50
+ outline: none;
51
+ }
52
+
53
+ button {
54
+ padding: 0.5em 1em;
55
+ cursor: pointer;
56
+ }
57
+
58
+ button:hover {
59
+ background-color: #334;
60
+ }
61
+
62
+ .error {
63
+ padding: 1em;
64
+ border-radius: 0.5em;
65
+ background-color: #733;
66
+ color: #ccc;
67
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../.svelte-kit/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2021"
5
+ },
6
+ "include": ["**/*.ts", "**/*.svelte"]
7
+ }