@axium/server 0.0.1 → 0.0.2
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/README.md +1 -0
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +110 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +120 -229
- package/dist/config.d.ts +237 -0
- package/dist/config.js +110 -0
- package/dist/database.d.ts +76 -0
- package/dist/database.js +184 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/io.d.ts +15 -0
- package/dist/io.js +55 -0
- package/package.json +19 -5
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Axium Server
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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/api';
|
|
5
|
+
declare module '@auth/core/adapters' {
|
|
6
|
+
interface AdapterUser {
|
|
7
|
+
password: string;
|
|
8
|
+
salt: string;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export declare let adapter: Adapter;
|
|
12
|
+
export declare function createAdapter(): Adapter;
|
|
13
|
+
export declare function register(credentials: Registration): Promise<{
|
|
14
|
+
user: import("@auth/core/adapters").AdapterUser | undefined;
|
|
15
|
+
session: import("@auth/core/adapters").AdapterSession | undefined;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function authorize(credentials: Partial<Record<string, unknown>>): Promise<Omit<import("@auth/core/adapters").AdapterUser, "password" | "salt"> | null>;
|
|
18
|
+
/**
|
|
19
|
+
* Get the providers current enabled in the Axium config.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getProviders(): Provider[];
|
|
22
|
+
export declare function getConfig(): AuthConfig;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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/api';
|
|
6
|
+
import { genSaltSync, hashSync } from 'bcryptjs';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { omit } from 'utilium';
|
|
9
|
+
import * as config from './config.js';
|
|
10
|
+
import * as db from './database.js';
|
|
11
|
+
export let adapter;
|
|
12
|
+
export function createAdapter() {
|
|
13
|
+
const conn = db.connect();
|
|
14
|
+
adapter = Object.assign(KyselyAdapter(conn), {
|
|
15
|
+
_axium: { config },
|
|
16
|
+
async getAccount(providerAccountId, provider) {
|
|
17
|
+
const result = await conn.selectFrom('Account').selectAll().where('providerAccountId', '=', providerAccountId).where('provider', '=', provider).executeTakeFirst();
|
|
18
|
+
return result ?? null;
|
|
19
|
+
},
|
|
20
|
+
async getAuthenticator(credentialID) {
|
|
21
|
+
const result = await conn.selectFrom('Authenticator').selectAll().where('credentialID', '=', credentialID).executeTakeFirst();
|
|
22
|
+
return result ?? null;
|
|
23
|
+
},
|
|
24
|
+
async createAuthenticator(authenticator) {
|
|
25
|
+
await conn.insertInto('Authenticator').values(authenticator).executeTakeFirstOrThrow();
|
|
26
|
+
return authenticator;
|
|
27
|
+
},
|
|
28
|
+
async listAuthenticatorsByUserId(userId) {
|
|
29
|
+
const result = await conn.selectFrom('Authenticator').selectAll().where('userId', '=', userId).execute();
|
|
30
|
+
return result;
|
|
31
|
+
},
|
|
32
|
+
async updateAuthenticatorCounter(credentialID, newCounter) {
|
|
33
|
+
await conn.updateTable('Authenticator').set({ counter: newCounter }).where('credentialID', '=', credentialID).executeTakeFirstOrThrow();
|
|
34
|
+
const authenticator = await adapter.getAuthenticator?.(credentialID);
|
|
35
|
+
if (!authenticator)
|
|
36
|
+
throw new Error('Authenticator not found');
|
|
37
|
+
return authenticator;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return adapter;
|
|
41
|
+
}
|
|
42
|
+
export async function register(credentials) {
|
|
43
|
+
const { email, password, name } = Registration.parse(credentials);
|
|
44
|
+
const existing = await adapter.getUserByEmail?.(email);
|
|
45
|
+
if (existing)
|
|
46
|
+
throw 'User already exists';
|
|
47
|
+
let id = crypto.randomUUID();
|
|
48
|
+
while (await adapter.getUser?.(id))
|
|
49
|
+
id = crypto.randomUUID();
|
|
50
|
+
const salt = genSaltSync(10);
|
|
51
|
+
const user = await adapter.createUser?.({
|
|
52
|
+
id,
|
|
53
|
+
name,
|
|
54
|
+
email,
|
|
55
|
+
emailVerified: null,
|
|
56
|
+
salt,
|
|
57
|
+
password: hashSync(password, salt),
|
|
58
|
+
});
|
|
59
|
+
const expires = new Date();
|
|
60
|
+
expires.setMonth(expires.getMonth() + 1);
|
|
61
|
+
const session = await adapter.createSession?.({
|
|
62
|
+
sessionToken: randomBytes(64).toString('base64'),
|
|
63
|
+
userId: id,
|
|
64
|
+
expires,
|
|
65
|
+
});
|
|
66
|
+
return { user, session };
|
|
67
|
+
}
|
|
68
|
+
export async function authorize(credentials) {
|
|
69
|
+
const { success, error, data } = Login.safeParse(credentials);
|
|
70
|
+
if (!success)
|
|
71
|
+
throw new CredentialsSignin(error);
|
|
72
|
+
const user = await adapter.getUserByEmail?.(data.email);
|
|
73
|
+
if (!user)
|
|
74
|
+
return null;
|
|
75
|
+
if (user.password !== hashSync(data.password, user.salt))
|
|
76
|
+
return null;
|
|
77
|
+
return omit(user, 'password', 'salt');
|
|
78
|
+
}
|
|
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
|
+
if (config.auth.credentials) {
|
|
87
|
+
providers.push(Credentials({
|
|
88
|
+
credentials: {
|
|
89
|
+
email: { label: 'Email', type: 'email' },
|
|
90
|
+
password: { label: 'Password', type: 'password' },
|
|
91
|
+
},
|
|
92
|
+
authorize,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
return providers;
|
|
96
|
+
}
|
|
97
|
+
export function getConfig() {
|
|
98
|
+
createAdapter();
|
|
99
|
+
return {
|
|
100
|
+
adapter,
|
|
101
|
+
providers: getProviders(),
|
|
102
|
+
debug: config.auth.debug ?? config.debug,
|
|
103
|
+
experimental: { enableWebAuthn: true },
|
|
104
|
+
secret: config.auth.secret,
|
|
105
|
+
useSecureCookies: config.auth.secure_cookies,
|
|
106
|
+
session: {
|
|
107
|
+
strategy: 'database',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
CHANGED
|
@@ -1,251 +1,85 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
3
|
-
if (value !== null && value !== void 0) {
|
|
4
|
-
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
5
|
-
var dispose, inner;
|
|
6
|
-
if (async) {
|
|
7
|
-
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
8
|
-
dispose = value[Symbol.asyncDispose];
|
|
9
|
-
}
|
|
10
|
-
if (dispose === void 0) {
|
|
11
|
-
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
12
|
-
dispose = value[Symbol.dispose];
|
|
13
|
-
if (async) inner = dispose;
|
|
14
|
-
}
|
|
15
|
-
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
16
|
-
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
17
|
-
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
18
|
-
}
|
|
19
|
-
else if (async) {
|
|
20
|
-
env.stack.push({ async: true });
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
};
|
|
24
|
-
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
25
|
-
return function (env) {
|
|
26
|
-
function fail(e) {
|
|
27
|
-
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
28
|
-
env.hasError = true;
|
|
29
|
-
}
|
|
30
|
-
var r, s = 0;
|
|
31
|
-
function next() {
|
|
32
|
-
while (r = env.stack.pop()) {
|
|
33
|
-
try {
|
|
34
|
-
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
35
|
-
if (r.dispose) {
|
|
36
|
-
var result = r.dispose.call(r.value);
|
|
37
|
-
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
38
|
-
}
|
|
39
|
-
else s |= 1;
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
fail(e);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
46
|
-
if (env.hasError) throw env.error;
|
|
47
|
-
}
|
|
48
|
-
return next();
|
|
49
|
-
};
|
|
50
|
-
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
51
|
-
var e = new Error(message);
|
|
52
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
53
|
-
});
|
|
54
2
|
import chalk from 'chalk';
|
|
55
3
|
import { Option, program } from 'commander';
|
|
56
|
-
import {
|
|
57
|
-
import
|
|
58
|
-
import
|
|
59
|
-
import * as
|
|
60
|
-
|
|
61
|
-
|
|
4
|
+
import { getByString, isJSON, pick, setByString } from 'utilium';
|
|
5
|
+
import $pkg from '../package.json' with { type: 'json' };
|
|
6
|
+
import * as config from './config.js';
|
|
7
|
+
import * as db from './database.js';
|
|
8
|
+
import { err, exit } from './io.js';
|
|
9
|
+
program
|
|
10
|
+
.version($pkg.version)
|
|
11
|
+
.name('axium')
|
|
12
|
+
.description('Axium server CLI')
|
|
13
|
+
.configureHelp({ showGlobalOptions: true })
|
|
14
|
+
.option('-D, --debug', 'override debug mode', false)
|
|
15
|
+
.option('-c, --config <path>', 'path to the config file');
|
|
16
|
+
program.on('option:debug', () => config.set(pick(program.opts(), 'debug')));
|
|
17
|
+
program.on('option:config', () => config.load(program.opts().config));
|
|
18
|
+
program.hook('preAction', function (_, action) {
|
|
19
|
+
config.loadDefaults();
|
|
20
|
+
const opt = action.optsWithGlobals();
|
|
21
|
+
opt.force && console.log(chalk.yellow('--force: Protections disabled.'));
|
|
22
|
+
});
|
|
23
|
+
// Options shared by multiple (sub)commands
|
|
62
24
|
const opts = {
|
|
63
|
-
|
|
64
|
-
|
|
25
|
+
// database specific
|
|
26
|
+
host: new Option('-H, --host <host>', 'the host of the database.').argParser(value => {
|
|
27
|
+
const [hostname, port] = value?.split(':') ?? [];
|
|
28
|
+
config.db.host = hostname || config.db.host;
|
|
29
|
+
config.db.port = port && Number.isSafeInteger(parseInt(port)) ? parseInt(port) : config.db.port;
|
|
30
|
+
}),
|
|
65
31
|
force: new Option('-f, --force', 'force the operation').default(false),
|
|
66
|
-
verbose: new Option('-v, --verbose', 'verbose output').default(false),
|
|
67
32
|
};
|
|
68
|
-
const axiumDB = program
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (err)
|
|
90
|
-
reject('[command]');
|
|
91
|
-
else
|
|
92
|
-
resolve();
|
|
93
|
-
});
|
|
94
|
-
await promise;
|
|
95
|
-
console.log('done.');
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
throw error == '[command]' ? stderr?.slice(0, 100) || 'failed.' : typeof error == 'object' && 'message' in error ? error.message : error;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function err(message) {
|
|
102
|
-
if (message instanceof Error)
|
|
103
|
-
message = message.message;
|
|
104
|
-
console.error(message.startsWith('\x1b') ? message : chalk.red(message));
|
|
105
|
-
}
|
|
106
|
-
/** Yet another convenience function */
|
|
107
|
-
function exit(message, code = 1) {
|
|
108
|
-
err(message);
|
|
109
|
-
process.exit(code);
|
|
110
|
-
}
|
|
111
|
-
function shouldRecreate(opt) {
|
|
112
|
-
if (opt.skip) {
|
|
113
|
-
console.warn(chalk.yellow('already exists. (skipped)'));
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
if (opt.force) {
|
|
117
|
-
console.warn(chalk.yellow('already exists. (re-creating)'));
|
|
118
|
-
return false;
|
|
33
|
+
const axiumDB = program
|
|
34
|
+
.command('db')
|
|
35
|
+
.alias('database')
|
|
36
|
+
.description('manage the database')
|
|
37
|
+
.option('-t, --timeout <ms>', 'how long to wait for commands to complete.', '1000')
|
|
38
|
+
.addOption(opts.host);
|
|
39
|
+
function db_output(state, message) {
|
|
40
|
+
switch (state) {
|
|
41
|
+
case 'start':
|
|
42
|
+
process.stdout.write(message + '... ');
|
|
43
|
+
break;
|
|
44
|
+
case 'log':
|
|
45
|
+
case 'warn':
|
|
46
|
+
process.stdout.write(chalk.yellow(message));
|
|
47
|
+
break;
|
|
48
|
+
case 'error':
|
|
49
|
+
process.stdout.write(chalk.red(message));
|
|
50
|
+
break;
|
|
51
|
+
case 'done':
|
|
52
|
+
console.log('done.');
|
|
53
|
+
break;
|
|
119
54
|
}
|
|
120
|
-
console.warn(chalk.yellow('already exists. Use --skip to skip or --force to re-create.'));
|
|
121
|
-
process.exit(2);
|
|
122
55
|
}
|
|
123
56
|
axiumDB
|
|
124
57
|
.command('init')
|
|
125
58
|
.description('initialize the database')
|
|
126
|
-
.addOption(opts.host)
|
|
127
|
-
.addOption(opts.timeout)
|
|
128
59
|
.addOption(opts.force)
|
|
129
|
-
.addOption(opts.verbose)
|
|
130
60
|
.option('-s, --skip', 'Skip existing database and/or user')
|
|
131
61
|
.action(async (opt) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.catch(async (error) => {
|
|
139
|
-
if (error != 'database "axium" already exists')
|
|
140
|
-
exit(error);
|
|
141
|
-
if (shouldRecreate(opt))
|
|
142
|
-
return;
|
|
143
|
-
await runSQL(opt, 'DROP DATABASE axium', 'Dropping database');
|
|
144
|
-
await runSQL(opt, 'CREATE DATABASE axium', 'Re-creating database');
|
|
145
|
-
})
|
|
146
|
-
.catch(exit);
|
|
147
|
-
const createQuery = `CREATE USER axium WITH ENCRYPTED PASSWORD '${config.password}' LOGIN`;
|
|
148
|
-
await runSQL(opt, createQuery, 'Creating user')
|
|
149
|
-
.catch(async (error) => {
|
|
150
|
-
if (error != 'role "axium" already exists')
|
|
151
|
-
exit(error);
|
|
152
|
-
if (shouldRecreate(opt))
|
|
153
|
-
return;
|
|
154
|
-
await runSQL(opt, 'REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
155
|
-
await runSQL(opt, 'DROP USER axium', 'Dropping user');
|
|
156
|
-
await runSQL(opt, createQuery, 'Re-creating user');
|
|
157
|
-
})
|
|
158
|
-
.catch(exit);
|
|
159
|
-
await runSQL(opt, 'GRANT ALL PRIVILEGES ON DATABASE axium TO axium', 'Granting database privileges').catch(exit);
|
|
160
|
-
await runSQL(opt, 'GRANT ALL PRIVILEGES ON SCHEMA public TO axium', 'Granting schema privileges').catch(exit);
|
|
161
|
-
await runSQL(opt, 'ALTER DATABASE axium OWNER TO axium', 'Setting database owner').catch(exit);
|
|
162
|
-
await runSQL(opt, 'SELECT pg_reload_conf()', 'Reloading configuration').catch(exit);
|
|
163
|
-
const db = __addDisposableResource(env_1, _db.connect(config), true);
|
|
164
|
-
const relationExists = (table) => (error) => (error == `relation "${table}" already exists` ? console.warn(chalk.yellow('already exists.')) : exit(error));
|
|
165
|
-
const created = chalk.green('created.');
|
|
166
|
-
await report(db.schema
|
|
167
|
-
.createTable('User')
|
|
168
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
169
|
-
.addColumn('name', 'text')
|
|
170
|
-
.addColumn('email', 'text', col => col.unique().notNull())
|
|
171
|
-
.addColumn('emailVerified', 'timestamptz')
|
|
172
|
-
.addColumn('image', 'text')
|
|
173
|
-
.execute(), 'Creating table User', created).catch(relationExists('User'));
|
|
174
|
-
await report(db.schema
|
|
175
|
-
.createTable('Account')
|
|
176
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
177
|
-
.addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
|
|
178
|
-
.addColumn('type', 'text', col => col.notNull())
|
|
179
|
-
.addColumn('provider', 'text', col => col.notNull())
|
|
180
|
-
.addColumn('providerAccountId', 'text', col => col.notNull())
|
|
181
|
-
.addColumn('refresh_token', 'text')
|
|
182
|
-
.addColumn('access_token', 'text')
|
|
183
|
-
.addColumn('expires_at', 'bigint')
|
|
184
|
-
.addColumn('token_type', 'text')
|
|
185
|
-
.addColumn('scope', 'text')
|
|
186
|
-
.addColumn('id_token', 'text')
|
|
187
|
-
.addColumn('session_state', 'text')
|
|
188
|
-
.execute(), 'Creating table Account', created).catch(relationExists('Account'));
|
|
189
|
-
await report(db.schema.createIndex('Account_userId_index').on('Account').column('userId').execute(), 'Creating index for Account.userId', created).catch(relationExists('Account_userId_index'));
|
|
190
|
-
await report(db.schema
|
|
191
|
-
.createTable('Session')
|
|
192
|
-
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
193
|
-
.addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
|
|
194
|
-
.addColumn('sessionToken', 'text', col => col.notNull().unique())
|
|
195
|
-
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
196
|
-
.execute(), 'Creating table Session', created).catch(relationExists('Session'));
|
|
197
|
-
await report(db.schema.createIndex('Session_userId_index').on('Session').column('userId').execute(), 'Creating index for Session.userId', created).catch(relationExists('Session_userId_index'));
|
|
198
|
-
await report(db.schema
|
|
199
|
-
.createTable('VerificationToken')
|
|
200
|
-
.addColumn('identifier', 'text', col => col.notNull())
|
|
201
|
-
.addColumn('token', 'text', col => col.notNull().unique())
|
|
202
|
-
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
203
|
-
.execute(), 'Creating table VerificationToken', created).catch(relationExists('VerificationToken'));
|
|
204
|
-
await report(db.schema
|
|
205
|
-
.createTable('Authenticator')
|
|
206
|
-
.addColumn('credentialID', 'text', col => col.primaryKey().notNull())
|
|
207
|
-
.addColumn('userId', 'uuid', col => col.notNull().references('User.id').onDelete('cascade').onUpdate('cascade'))
|
|
208
|
-
.addColumn('providerAccountId', 'text', col => col.notNull())
|
|
209
|
-
.addColumn('credentialPublicKey', 'text', col => col.notNull())
|
|
210
|
-
.addColumn('counter', 'integer', col => col.notNull())
|
|
211
|
-
.addColumn('credentialDeviceType', 'text', col => col.notNull())
|
|
212
|
-
.addColumn('credentialBackedUp', 'boolean', col => col.notNull())
|
|
213
|
-
.addColumn('transports', 'text')
|
|
214
|
-
.execute(), 'Creating table Authenticator', created).catch(relationExists('Authenticator'));
|
|
215
|
-
await report(db.schema.createIndex('Authenticator_credentialID_key').on('Authenticator').column('credentialID').execute(), 'Creating index for Authenticator.credentialID', created).catch(relationExists('Authenticator_credentialID_key'));
|
|
216
|
-
console.log('Done!\nPassword: ' + config.password);
|
|
217
|
-
}
|
|
218
|
-
catch (e_1) {
|
|
219
|
-
env_1.error = e_1;
|
|
220
|
-
env_1.hasError = true;
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
const result_1 = __disposeResources(env_1);
|
|
224
|
-
if (result_1)
|
|
225
|
-
await result_1;
|
|
226
|
-
}
|
|
62
|
+
await db.init({ ...opt, output: db_output }).catch((e) => {
|
|
63
|
+
if (typeof e == 'number')
|
|
64
|
+
process.exit(e);
|
|
65
|
+
else
|
|
66
|
+
exit(e);
|
|
67
|
+
});
|
|
227
68
|
});
|
|
228
69
|
axiumDB
|
|
229
70
|
.command('status')
|
|
230
71
|
.alias('stats')
|
|
231
72
|
.description('check the status of the database')
|
|
232
|
-
.
|
|
233
|
-
.
|
|
234
|
-
.action(async (opt) => _db
|
|
235
|
-
.statusText(opt)
|
|
73
|
+
.action(async () => db
|
|
74
|
+
.statusText()
|
|
236
75
|
.then(console.log)
|
|
237
76
|
.catch(() => exit('Unavailable')));
|
|
238
77
|
axiumDB
|
|
239
78
|
.command('drop')
|
|
240
79
|
.description('drop the database')
|
|
241
|
-
.addOption(opts.host)
|
|
242
|
-
.addOption(opts.verbose)
|
|
243
80
|
.addOption(opts.force)
|
|
244
|
-
.addOption(opts.timeout)
|
|
245
81
|
.action(async (opt) => {
|
|
246
|
-
|
|
247
|
-
_db.normalizeConfig(opt);
|
|
248
|
-
const stats = await _db.status(opt).catch(exit);
|
|
82
|
+
const stats = await db.status().catch(exit);
|
|
249
83
|
if (!opt.force)
|
|
250
84
|
for (const key of ['users', 'accounts', 'sessions']) {
|
|
251
85
|
if (stats[key] == 0)
|
|
@@ -253,21 +87,78 @@ axiumDB
|
|
|
253
87
|
console.warn(chalk.yellow(`Database has existing ${key}. Use --force if you really want to drop the database.`));
|
|
254
88
|
process.exit(2);
|
|
255
89
|
}
|
|
256
|
-
await
|
|
257
|
-
|
|
258
|
-
|
|
90
|
+
await db.remove({ ...opt, output: db_output }).catch(exit);
|
|
91
|
+
});
|
|
92
|
+
const axiumConfig = program
|
|
93
|
+
.command('config')
|
|
94
|
+
.alias('conf')
|
|
95
|
+
.description('manage the configuration')
|
|
96
|
+
.option('-j, --json', 'values are JSON encoded')
|
|
97
|
+
.option('-g, --global', 'apply to the global config');
|
|
98
|
+
axiumConfig
|
|
99
|
+
.command('dump')
|
|
100
|
+
.description('Output the entire current configuration')
|
|
101
|
+
.action(() => {
|
|
102
|
+
const value = config.get();
|
|
103
|
+
console.log(axiumConfig.optsWithGlobals().json ? JSON.stringify(value) : value);
|
|
104
|
+
});
|
|
105
|
+
axiumConfig
|
|
106
|
+
.command('get')
|
|
107
|
+
.description('get a config value')
|
|
108
|
+
.argument('<key>', 'the key to get')
|
|
109
|
+
.action((key) => {
|
|
110
|
+
const value = getByString(config, key);
|
|
111
|
+
console.log(axiumConfig.optsWithGlobals().json ? JSON.stringify(value) : value);
|
|
112
|
+
});
|
|
113
|
+
axiumConfig
|
|
114
|
+
.command('set')
|
|
115
|
+
.description('Set a config value. Note setting objects is not supported.')
|
|
116
|
+
.argument('<key>', 'the key to set')
|
|
117
|
+
.argument('<value>', 'the value')
|
|
118
|
+
.action((key, value, opt) => {
|
|
119
|
+
const useJSON = axiumConfig.optsWithGlobals().json;
|
|
120
|
+
if (useJSON && !isJSON(value))
|
|
121
|
+
exit('Invalid JSON');
|
|
122
|
+
const obj = {};
|
|
123
|
+
setByString(obj, key, useJSON ? JSON.parse(value) : value);
|
|
124
|
+
config.save(obj, opt.global);
|
|
125
|
+
});
|
|
126
|
+
axiumConfig
|
|
127
|
+
.command('list')
|
|
128
|
+
.alias('ls')
|
|
129
|
+
.alias('files')
|
|
130
|
+
.description('List loaded config files')
|
|
131
|
+
.action(() => {
|
|
132
|
+
for (const path of config.files.keys())
|
|
133
|
+
console.log(path);
|
|
259
134
|
});
|
|
260
135
|
program
|
|
261
136
|
.command('status')
|
|
262
137
|
.alias('stats')
|
|
263
138
|
.description('get information about the server')
|
|
264
|
-
.
|
|
265
|
-
.action(async (
|
|
139
|
+
.addOption(opts.host)
|
|
140
|
+
.action(async () => {
|
|
266
141
|
console.log('Axium Server v' + program.version());
|
|
142
|
+
console.log('Debug mode:', config.debug ? chalk.yellow('enabled') : 'disabled');
|
|
143
|
+
console.log('Loaded config files:', config.files.keys().toArray().join(', '));
|
|
267
144
|
process.stdout.write('Database: ');
|
|
268
|
-
await
|
|
269
|
-
.statusText(
|
|
145
|
+
await db
|
|
146
|
+
.statusText()
|
|
270
147
|
.then(console.log)
|
|
271
148
|
.catch(() => err('Unavailable'));
|
|
149
|
+
console.log('Enabled auth providers:', config.authProviders.filter(provider => config.auth[provider]).join(', '));
|
|
150
|
+
});
|
|
151
|
+
program
|
|
152
|
+
.command('init')
|
|
153
|
+
.description('Install Axium server')
|
|
154
|
+
.addOption(opts.force)
|
|
155
|
+
.addOption(opts.host)
|
|
156
|
+
.action(async (opt) => {
|
|
157
|
+
await db.init({ ...opt, skip: opt.dbSkip, output: db_output }).catch((e) => {
|
|
158
|
+
if (typeof e == 'number')
|
|
159
|
+
process.exit(e);
|
|
160
|
+
else
|
|
161
|
+
exit(e);
|
|
162
|
+
});
|
|
272
163
|
});
|
|
273
164
|
program.parse();
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { type PartialRecursive } from 'utilium';
|
|
2
|
+
import * as z from 'zod';
|
|
3
|
+
export declare const Database: z.ZodObject<{
|
|
4
|
+
host: z.ZodString;
|
|
5
|
+
port: z.ZodNumber;
|
|
6
|
+
password: z.ZodString;
|
|
7
|
+
user: z.ZodString;
|
|
8
|
+
database: z.ZodString;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
password: string;
|
|
13
|
+
user: string;
|
|
14
|
+
database: string;
|
|
15
|
+
}, {
|
|
16
|
+
host: string;
|
|
17
|
+
port: number;
|
|
18
|
+
password: string;
|
|
19
|
+
user: string;
|
|
20
|
+
database: string;
|
|
21
|
+
}>;
|
|
22
|
+
export type Database = z.infer<typeof Database>;
|
|
23
|
+
export declare const db: Database;
|
|
24
|
+
export declare const Auth: z.ZodObject<{
|
|
25
|
+
credentials: z.ZodBoolean;
|
|
26
|
+
debug: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
passkeys: z.ZodBoolean;
|
|
28
|
+
secret: z.ZodString;
|
|
29
|
+
secure_cookies: z.ZodBoolean;
|
|
30
|
+
}, "strip", z.ZodTypeAny, {
|
|
31
|
+
credentials: boolean;
|
|
32
|
+
passkeys: boolean;
|
|
33
|
+
secret: string;
|
|
34
|
+
secure_cookies: boolean;
|
|
35
|
+
debug?: boolean | undefined;
|
|
36
|
+
}, {
|
|
37
|
+
credentials: boolean;
|
|
38
|
+
passkeys: boolean;
|
|
39
|
+
secret: string;
|
|
40
|
+
secure_cookies: boolean;
|
|
41
|
+
debug?: boolean | undefined;
|
|
42
|
+
}>;
|
|
43
|
+
export type Auth = z.infer<typeof Auth>;
|
|
44
|
+
export declare const auth: Auth;
|
|
45
|
+
export declare const authProviders: readonly ["credentials", "passkeys"];
|
|
46
|
+
export type AuthProvider = (typeof authProviders)[number];
|
|
47
|
+
export declare const Config: z.ZodObject<{
|
|
48
|
+
auth: z.ZodObject<{
|
|
49
|
+
credentials: z.ZodOptional<z.ZodBoolean>;
|
|
50
|
+
debug: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
51
|
+
passkeys: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
+
secret: z.ZodOptional<z.ZodString>;
|
|
53
|
+
secure_cookies: z.ZodOptional<z.ZodBoolean>;
|
|
54
|
+
}, "strip", z.ZodTypeAny, {
|
|
55
|
+
credentials?: boolean | undefined;
|
|
56
|
+
debug?: boolean | undefined;
|
|
57
|
+
passkeys?: boolean | undefined;
|
|
58
|
+
secret?: string | undefined;
|
|
59
|
+
secure_cookies?: boolean | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
credentials?: boolean | undefined;
|
|
62
|
+
debug?: boolean | undefined;
|
|
63
|
+
passkeys?: boolean | undefined;
|
|
64
|
+
secret?: string | undefined;
|
|
65
|
+
secure_cookies?: boolean | undefined;
|
|
66
|
+
}>;
|
|
67
|
+
debug: z.ZodBoolean;
|
|
68
|
+
db: z.ZodObject<{
|
|
69
|
+
host: z.ZodOptional<z.ZodString>;
|
|
70
|
+
port: z.ZodOptional<z.ZodNumber>;
|
|
71
|
+
password: z.ZodOptional<z.ZodString>;
|
|
72
|
+
user: z.ZodOptional<z.ZodString>;
|
|
73
|
+
database: z.ZodOptional<z.ZodString>;
|
|
74
|
+
}, "strip", z.ZodTypeAny, {
|
|
75
|
+
host?: string | undefined;
|
|
76
|
+
port?: number | undefined;
|
|
77
|
+
password?: string | undefined;
|
|
78
|
+
user?: string | undefined;
|
|
79
|
+
database?: string | undefined;
|
|
80
|
+
}, {
|
|
81
|
+
host?: string | undefined;
|
|
82
|
+
port?: number | undefined;
|
|
83
|
+
password?: string | undefined;
|
|
84
|
+
user?: string | undefined;
|
|
85
|
+
database?: string | undefined;
|
|
86
|
+
}>;
|
|
87
|
+
}, "strip", z.ZodTypeAny, {
|
|
88
|
+
debug: boolean;
|
|
89
|
+
auth: {
|
|
90
|
+
credentials?: boolean | undefined;
|
|
91
|
+
debug?: boolean | undefined;
|
|
92
|
+
passkeys?: boolean | undefined;
|
|
93
|
+
secret?: string | undefined;
|
|
94
|
+
secure_cookies?: boolean | undefined;
|
|
95
|
+
};
|
|
96
|
+
db: {
|
|
97
|
+
host?: string | undefined;
|
|
98
|
+
port?: number | undefined;
|
|
99
|
+
password?: string | undefined;
|
|
100
|
+
user?: string | undefined;
|
|
101
|
+
database?: string | undefined;
|
|
102
|
+
};
|
|
103
|
+
}, {
|
|
104
|
+
debug: boolean;
|
|
105
|
+
auth: {
|
|
106
|
+
credentials?: boolean | undefined;
|
|
107
|
+
debug?: boolean | undefined;
|
|
108
|
+
passkeys?: boolean | undefined;
|
|
109
|
+
secret?: string | undefined;
|
|
110
|
+
secure_cookies?: boolean | undefined;
|
|
111
|
+
};
|
|
112
|
+
db: {
|
|
113
|
+
host?: string | undefined;
|
|
114
|
+
port?: number | undefined;
|
|
115
|
+
password?: string | undefined;
|
|
116
|
+
user?: string | undefined;
|
|
117
|
+
database?: string | undefined;
|
|
118
|
+
};
|
|
119
|
+
}>;
|
|
120
|
+
export declare const File: z.ZodObject<z.objectUtil.extendShape<{
|
|
121
|
+
auth: z.ZodOptional<z.ZodObject<{
|
|
122
|
+
credentials: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
123
|
+
debug: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodBoolean>>>;
|
|
124
|
+
passkeys: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
125
|
+
secret: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
126
|
+
secure_cookies: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
127
|
+
}, "strip", z.ZodTypeAny, {
|
|
128
|
+
credentials?: boolean | undefined;
|
|
129
|
+
debug?: boolean | undefined;
|
|
130
|
+
passkeys?: boolean | undefined;
|
|
131
|
+
secret?: string | undefined;
|
|
132
|
+
secure_cookies?: boolean | undefined;
|
|
133
|
+
}, {
|
|
134
|
+
credentials?: boolean | undefined;
|
|
135
|
+
debug?: boolean | undefined;
|
|
136
|
+
passkeys?: boolean | undefined;
|
|
137
|
+
secret?: string | undefined;
|
|
138
|
+
secure_cookies?: boolean | undefined;
|
|
139
|
+
}>>;
|
|
140
|
+
debug: z.ZodOptional<z.ZodBoolean>;
|
|
141
|
+
db: z.ZodOptional<z.ZodObject<{
|
|
142
|
+
host: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
143
|
+
port: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
144
|
+
password: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
145
|
+
user: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
146
|
+
database: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
147
|
+
}, "strip", z.ZodTypeAny, {
|
|
148
|
+
host?: string | undefined;
|
|
149
|
+
port?: number | undefined;
|
|
150
|
+
password?: string | undefined;
|
|
151
|
+
user?: string | undefined;
|
|
152
|
+
database?: string | undefined;
|
|
153
|
+
}, {
|
|
154
|
+
host?: string | undefined;
|
|
155
|
+
port?: number | undefined;
|
|
156
|
+
password?: string | undefined;
|
|
157
|
+
user?: string | undefined;
|
|
158
|
+
database?: string | undefined;
|
|
159
|
+
}>>;
|
|
160
|
+
}, {
|
|
161
|
+
include: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
162
|
+
}>, "strip", z.ZodTypeAny, {
|
|
163
|
+
debug?: boolean | undefined;
|
|
164
|
+
auth?: {
|
|
165
|
+
credentials?: boolean | undefined;
|
|
166
|
+
debug?: boolean | undefined;
|
|
167
|
+
passkeys?: boolean | undefined;
|
|
168
|
+
secret?: string | undefined;
|
|
169
|
+
secure_cookies?: boolean | undefined;
|
|
170
|
+
} | undefined;
|
|
171
|
+
db?: {
|
|
172
|
+
host?: string | undefined;
|
|
173
|
+
port?: number | undefined;
|
|
174
|
+
password?: string | undefined;
|
|
175
|
+
user?: string | undefined;
|
|
176
|
+
database?: string | undefined;
|
|
177
|
+
} | undefined;
|
|
178
|
+
include?: string[] | undefined;
|
|
179
|
+
}, {
|
|
180
|
+
debug?: boolean | undefined;
|
|
181
|
+
auth?: {
|
|
182
|
+
credentials?: boolean | undefined;
|
|
183
|
+
debug?: boolean | undefined;
|
|
184
|
+
passkeys?: boolean | undefined;
|
|
185
|
+
secret?: string | undefined;
|
|
186
|
+
secure_cookies?: boolean | undefined;
|
|
187
|
+
} | undefined;
|
|
188
|
+
db?: {
|
|
189
|
+
host?: string | undefined;
|
|
190
|
+
port?: number | undefined;
|
|
191
|
+
password?: string | undefined;
|
|
192
|
+
user?: string | undefined;
|
|
193
|
+
database?: string | undefined;
|
|
194
|
+
} | undefined;
|
|
195
|
+
include?: string[] | undefined;
|
|
196
|
+
}>;
|
|
197
|
+
export type File = z.infer<typeof File>;
|
|
198
|
+
export interface Config extends z.infer<typeof Config> {
|
|
199
|
+
}
|
|
200
|
+
export declare let debug: boolean;
|
|
201
|
+
export declare function get(): Config;
|
|
202
|
+
/**
|
|
203
|
+
* Update the current config
|
|
204
|
+
*/
|
|
205
|
+
export declare function set(config: PartialRecursive<Config>): void;
|
|
206
|
+
export declare const files: Map<string, PartialRecursive<Config>>;
|
|
207
|
+
export interface LoadOptions {
|
|
208
|
+
/**
|
|
209
|
+
* If enabled, the config file will be not be loaded if it does not match the schema.
|
|
210
|
+
*/
|
|
211
|
+
strict?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* If enabled, the config file will be skipped if it does not exist.
|
|
214
|
+
*/
|
|
215
|
+
optional?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* If `optional`, this function will be called with the error if the config file is invalid or can't be read.
|
|
218
|
+
*/
|
|
219
|
+
onError?(error: Error): void;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Load the config from the provided path
|
|
223
|
+
*/
|
|
224
|
+
export declare function load(path: string, options?: LoadOptions): void;
|
|
225
|
+
export declare function loadDefaults(): void;
|
|
226
|
+
/**
|
|
227
|
+
* Update the current config and write the updated config to the appropriate file
|
|
228
|
+
*/
|
|
229
|
+
export declare function save(changed: PartialRecursive<Config>, global?: boolean): void;
|
|
230
|
+
/**
|
|
231
|
+
* Update the current config and write the updated config to the provided path
|
|
232
|
+
*/
|
|
233
|
+
export declare function saveTo(path: string, changed: PartialRecursive<Config>): void;
|
|
234
|
+
/**
|
|
235
|
+
* Find the path to the config file
|
|
236
|
+
*/
|
|
237
|
+
export declare function findPath(global: boolean): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path/posix';
|
|
5
|
+
import { assignWithDefaults } from 'utilium';
|
|
6
|
+
import * as z from 'zod';
|
|
7
|
+
import { findDir } from './io.js';
|
|
8
|
+
export const Database = z.object({
|
|
9
|
+
host: z.string(),
|
|
10
|
+
port: z.number(),
|
|
11
|
+
password: z.string(),
|
|
12
|
+
user: z.string(),
|
|
13
|
+
database: z.string(),
|
|
14
|
+
});
|
|
15
|
+
export const db = {
|
|
16
|
+
host: process.env.PGHOST || 'localhost',
|
|
17
|
+
port: process.env.PGPORT && Number.isSafeInteger(parseInt(process.env.PGPORT)) ? parseInt(process.env.PGPORT) : 5432,
|
|
18
|
+
password: process.env.PGPASSWORD || '',
|
|
19
|
+
user: process.env.PGUSER || 'axium',
|
|
20
|
+
database: process.env.PGDATABASE || 'axium',
|
|
21
|
+
};
|
|
22
|
+
export const Auth = z.object({
|
|
23
|
+
credentials: z.boolean(),
|
|
24
|
+
debug: z.boolean().optional(),
|
|
25
|
+
passkeys: z.boolean(),
|
|
26
|
+
secret: z.string(),
|
|
27
|
+
secure_cookies: z.boolean(),
|
|
28
|
+
});
|
|
29
|
+
export const auth = {
|
|
30
|
+
credentials: true,
|
|
31
|
+
passkeys: true,
|
|
32
|
+
secret: '',
|
|
33
|
+
secure_cookies: false,
|
|
34
|
+
};
|
|
35
|
+
export const authProviders = ['credentials', 'passkeys'];
|
|
36
|
+
export const Config = z.object({
|
|
37
|
+
auth: Auth.partial(),
|
|
38
|
+
debug: z.boolean(),
|
|
39
|
+
db: Database.partial(),
|
|
40
|
+
});
|
|
41
|
+
export const File = Config.deepPartial().extend({
|
|
42
|
+
include: z.array(z.string()).optional(),
|
|
43
|
+
});
|
|
44
|
+
export let debug = false;
|
|
45
|
+
export function get() {
|
|
46
|
+
return {
|
|
47
|
+
auth,
|
|
48
|
+
db,
|
|
49
|
+
debug,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Update the current config
|
|
54
|
+
*/
|
|
55
|
+
export function set(config) {
|
|
56
|
+
assignWithDefaults(auth, config.auth ?? {});
|
|
57
|
+
debug = config.debug ?? debug;
|
|
58
|
+
assignWithDefaults(db, config.db ?? {});
|
|
59
|
+
}
|
|
60
|
+
export const files = new Map();
|
|
61
|
+
/**
|
|
62
|
+
* Load the config from the provided path
|
|
63
|
+
*/
|
|
64
|
+
export function load(path, options = {}) {
|
|
65
|
+
let json;
|
|
66
|
+
try {
|
|
67
|
+
json = JSON.parse(readFileSync(path, 'utf8'));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (!options.optional)
|
|
71
|
+
throw e;
|
|
72
|
+
debug && console.debug(chalk.gray(`Skipping config at ${path} (${e.message})`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const config = options.strict ? File.parse(json) : json;
|
|
76
|
+
files.set(path, config);
|
|
77
|
+
set(config);
|
|
78
|
+
for (const include of config.include ?? [])
|
|
79
|
+
load(join(dirname(path), include), { optional: true });
|
|
80
|
+
}
|
|
81
|
+
export function loadDefaults() {
|
|
82
|
+
load(findPath(true), { optional: true });
|
|
83
|
+
load(findPath(false), { optional: true });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Update the current config and write the updated config to the appropriate file
|
|
87
|
+
*/
|
|
88
|
+
export function save(changed, global = false) {
|
|
89
|
+
saveTo(process.env.AXIUM_CONFIG ?? findPath(global), changed);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Update the current config and write the updated config to the provided path
|
|
93
|
+
*/
|
|
94
|
+
export function saveTo(path, changed) {
|
|
95
|
+
set(changed);
|
|
96
|
+
const config = files.get(path) ?? {};
|
|
97
|
+
Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
|
|
98
|
+
debug && console.debug(chalk.gray(`Wrote config to ${path}`));
|
|
99
|
+
writeFileSync(path, JSON.stringify(config));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Find the path to the config file
|
|
103
|
+
*/
|
|
104
|
+
export function findPath(global) {
|
|
105
|
+
if (process.env.AXIUM_CONFIG)
|
|
106
|
+
return process.env.AXIUM_CONFIG;
|
|
107
|
+
return join(findDir(global), 'config.json');
|
|
108
|
+
}
|
|
109
|
+
if (process.env.AXIUM_CONFIG)
|
|
110
|
+
load(process.env.AXIUM_CONFIG);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AdapterAccountType as db } from '@auth/core/adapters';
|
|
2
|
+
import { Kysely, type GeneratedAlways } from 'kysely';
|
|
3
|
+
import * as config from './config.js';
|
|
4
|
+
export interface Schema {
|
|
5
|
+
User: {
|
|
6
|
+
id: GeneratedAlways<string>;
|
|
7
|
+
name: string | null;
|
|
8
|
+
email: string;
|
|
9
|
+
emailVerified: Date | null;
|
|
10
|
+
image: string | null;
|
|
11
|
+
password: string | null;
|
|
12
|
+
salt: string | null;
|
|
13
|
+
};
|
|
14
|
+
Account: {
|
|
15
|
+
id: GeneratedAlways<string>;
|
|
16
|
+
userId: string;
|
|
17
|
+
type: db;
|
|
18
|
+
provider: string;
|
|
19
|
+
providerAccountId: string;
|
|
20
|
+
refresh_token?: string;
|
|
21
|
+
access_token?: string;
|
|
22
|
+
expires_at?: number;
|
|
23
|
+
token_type?: Lowercase<string>;
|
|
24
|
+
scope?: string;
|
|
25
|
+
id_token?: string;
|
|
26
|
+
session_state: string | null;
|
|
27
|
+
};
|
|
28
|
+
Session: {
|
|
29
|
+
id: GeneratedAlways<string>;
|
|
30
|
+
userId: string;
|
|
31
|
+
sessionToken: string;
|
|
32
|
+
expires: Date;
|
|
33
|
+
};
|
|
34
|
+
VerificationToken: {
|
|
35
|
+
identifier: string;
|
|
36
|
+
token: string;
|
|
37
|
+
expires: Date;
|
|
38
|
+
};
|
|
39
|
+
Authenticator: {
|
|
40
|
+
credentialID: string;
|
|
41
|
+
userId: string;
|
|
42
|
+
providerAccountId: string;
|
|
43
|
+
credentialPublicKey: string;
|
|
44
|
+
counter: number;
|
|
45
|
+
credentialDeviceType: string;
|
|
46
|
+
credentialBackedUp: boolean;
|
|
47
|
+
transports: string | null;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export declare let database: Kysely<Schema> & AsyncDisposable;
|
|
51
|
+
export declare function connect(): Kysely<Schema> & AsyncDisposable;
|
|
52
|
+
export interface Stats {
|
|
53
|
+
users: number;
|
|
54
|
+
accounts: number;
|
|
55
|
+
sessions: number;
|
|
56
|
+
}
|
|
57
|
+
export declare function status(): Promise<Stats>;
|
|
58
|
+
export declare function statusText(): Promise<string>;
|
|
59
|
+
export type OpOutputState = 'done' | 'log' | 'warn' | 'error' | 'start';
|
|
60
|
+
export type OpOutput = {
|
|
61
|
+
(state: 'done'): void;
|
|
62
|
+
(state: Exclude<OpOutputState, 'done'>, message: string): void;
|
|
63
|
+
};
|
|
64
|
+
export interface OpOptions {
|
|
65
|
+
timeout: number;
|
|
66
|
+
force: boolean;
|
|
67
|
+
output?: OpOutput;
|
|
68
|
+
}
|
|
69
|
+
export interface InitOptions extends OpOptions {
|
|
70
|
+
skip: boolean;
|
|
71
|
+
}
|
|
72
|
+
export declare function init(opt: InitOptions): Promise<config.Database>;
|
|
73
|
+
/**
|
|
74
|
+
* Completely remove Axium from the database.
|
|
75
|
+
*/
|
|
76
|
+
export declare function remove(opt: OpOptions): Promise<void>;
|
package/dist/database.js
CHANGED
|
@@ -51,31 +51,17 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
51
51
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
52
|
});
|
|
53
53
|
import { Kysely, PostgresDialect, sql } from 'kysely';
|
|
54
|
+
import { exec } from 'node:child_process';
|
|
55
|
+
import { randomBytes } from 'node:crypto';
|
|
54
56
|
import pg from 'pg';
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (!config.port) {
|
|
58
|
-
const [hostname, port] = config.host.split(':');
|
|
59
|
-
config.port = port ? parseInt(port) : 5432;
|
|
60
|
-
config.host = hostname;
|
|
61
|
-
}
|
|
62
|
-
if (config.host.includes(':'))
|
|
63
|
-
config.host = config.host.split(':')[0];
|
|
64
|
-
return config;
|
|
65
|
-
}
|
|
57
|
+
import * as config from './config.js';
|
|
58
|
+
import { verbose } from './io.js';
|
|
66
59
|
export let database;
|
|
67
|
-
export function connect(
|
|
60
|
+
export function connect() {
|
|
68
61
|
if (database)
|
|
69
62
|
return database;
|
|
70
|
-
normalizeConfig(config);
|
|
71
63
|
const _db = new Kysely({
|
|
72
|
-
dialect: new PostgresDialect({
|
|
73
|
-
pool: new pg.Pool({
|
|
74
|
-
...config,
|
|
75
|
-
user: 'axium',
|
|
76
|
-
database: 'axium',
|
|
77
|
-
}),
|
|
78
|
-
}),
|
|
64
|
+
dialect: new PostgresDialect({ pool: new pg.Pool(config.db) }),
|
|
79
65
|
});
|
|
80
66
|
database = Object.assign(_db, {
|
|
81
67
|
async [Symbol.asyncDispose]() {
|
|
@@ -84,11 +70,10 @@ export function connect(config) {
|
|
|
84
70
|
});
|
|
85
71
|
return database;
|
|
86
72
|
}
|
|
87
|
-
export async function status(
|
|
73
|
+
export async function status() {
|
|
88
74
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
89
75
|
try {
|
|
90
|
-
|
|
91
|
-
const db = __addDisposableResource(env_1, connect(config), true);
|
|
76
|
+
const db = __addDisposableResource(env_1, connect(), true);
|
|
92
77
|
return {
|
|
93
78
|
users: (await db.selectFrom('User').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
|
|
94
79
|
accounts: (await db.selectFrom('Account').select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count,
|
|
@@ -105,12 +90,186 @@ export async function status(config) {
|
|
|
105
90
|
await result_1;
|
|
106
91
|
}
|
|
107
92
|
}
|
|
108
|
-
export async function statusText(
|
|
93
|
+
export async function statusText() {
|
|
109
94
|
try {
|
|
110
|
-
const stats = await status(
|
|
95
|
+
const stats = await status();
|
|
111
96
|
return `${stats.users} users, ${stats.accounts} accounts, ${stats.sessions} sessions`;
|
|
112
97
|
}
|
|
113
98
|
catch (error) {
|
|
114
99
|
throw typeof error == 'object' && 'message' in error ? error.message : error;
|
|
115
100
|
}
|
|
116
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* TS can't tell when we do this inline
|
|
104
|
+
*/
|
|
105
|
+
function _fixOutput(opt) {
|
|
106
|
+
opt.output ??= () => { };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convenience function for `sudo -u postgres psql -c "${command}"`, plus `report` coolness.
|
|
110
|
+
* @internal
|
|
111
|
+
*/
|
|
112
|
+
async function execSQL(opts, command, message) {
|
|
113
|
+
let stderr;
|
|
114
|
+
try {
|
|
115
|
+
opts.output('start', message);
|
|
116
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
117
|
+
exec(`sudo -u postgres psql -c "${command}"`, opts, (err, _, _stderr) => {
|
|
118
|
+
stderr = _stderr.startsWith('ERROR:') ? _stderr.slice(6).trim() : _stderr;
|
|
119
|
+
if (err)
|
|
120
|
+
reject('[command]');
|
|
121
|
+
else
|
|
122
|
+
resolve();
|
|
123
|
+
});
|
|
124
|
+
await promise;
|
|
125
|
+
opts.output('done');
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw error == '[command]' ? stderr?.slice(0, 100) || 'failed.' : typeof error == 'object' && 'message' in error ? error.message : error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function shouldRecreate(opt) {
|
|
132
|
+
if (opt.skip) {
|
|
133
|
+
opt.output('warn', 'already exists. (skipped)\n');
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (opt.force) {
|
|
137
|
+
opt.output('warn', 'already exists. (re-creating)\n');
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
opt.output('warn', 'already exists. Use --skip to skip or --force to re-create.\n');
|
|
141
|
+
throw 2;
|
|
142
|
+
}
|
|
143
|
+
export async function init(opt) {
|
|
144
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
145
|
+
try {
|
|
146
|
+
_fixOutput(opt);
|
|
147
|
+
if (!config.db.password) {
|
|
148
|
+
config.save({ db: { password: randomBytes(32).toString('base64') } }, true);
|
|
149
|
+
verbose('Generated password and wrote to global config');
|
|
150
|
+
}
|
|
151
|
+
const _sql = (command, message) => execSQL(opt, command, message);
|
|
152
|
+
await _sql('CREATE DATABASE axium', 'Creating database').catch(async (error) => {
|
|
153
|
+
if (error != 'database "axium" already exists')
|
|
154
|
+
throw error;
|
|
155
|
+
if (shouldRecreate(opt))
|
|
156
|
+
return;
|
|
157
|
+
await _sql('DROP DATABASE axium', 'Dropping database');
|
|
158
|
+
await _sql('CREATE DATABASE axium', 'Re-creating database');
|
|
159
|
+
});
|
|
160
|
+
const createQuery = `CREATE USER axium WITH ENCRYPTED PASSWORD '${config.db.password}' LOGIN`;
|
|
161
|
+
await _sql(createQuery, 'Creating user').catch(async (error) => {
|
|
162
|
+
if (error != 'role "axium" already exists')
|
|
163
|
+
throw error;
|
|
164
|
+
if (shouldRecreate(opt))
|
|
165
|
+
return;
|
|
166
|
+
await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
167
|
+
await _sql('DROP USER axium', 'Dropping user');
|
|
168
|
+
await _sql(createQuery, 'Re-creating user');
|
|
169
|
+
});
|
|
170
|
+
await _sql('GRANT ALL PRIVILEGES ON DATABASE axium TO axium', 'Granting database privileges');
|
|
171
|
+
await _sql('GRANT ALL PRIVILEGES ON SCHEMA public TO axium', 'Granting schema privileges');
|
|
172
|
+
await _sql('ALTER DATABASE axium OWNER TO axium', 'Setting database owner');
|
|
173
|
+
await _sql('SELECT pg_reload_conf()', 'Reloading configuration');
|
|
174
|
+
const db = __addDisposableResource(env_2, connect(), true);
|
|
175
|
+
const relationExists = (table) => (error) => {
|
|
176
|
+
error = typeof error == 'object' && 'message' in error ? error.message : error;
|
|
177
|
+
if (error == `relation "${table}" already exists`)
|
|
178
|
+
opt.output('warn', 'already exists.');
|
|
179
|
+
else
|
|
180
|
+
throw error;
|
|
181
|
+
};
|
|
182
|
+
opt.output('start', 'Creating table User');
|
|
183
|
+
await db.schema
|
|
184
|
+
.createTable('User')
|
|
185
|
+
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
186
|
+
.addColumn('name', 'text')
|
|
187
|
+
.addColumn('email', 'text', col => col.unique().notNull())
|
|
188
|
+
.addColumn('emailVerified', 'timestamptz')
|
|
189
|
+
.addColumn('image', 'text')
|
|
190
|
+
.addColumn('password', 'text')
|
|
191
|
+
.addColumn('salt', 'text')
|
|
192
|
+
.execute()
|
|
193
|
+
.catch(relationExists('User'));
|
|
194
|
+
opt.output('done');
|
|
195
|
+
opt.output('start', 'Creating table Account');
|
|
196
|
+
await db.schema
|
|
197
|
+
.createTable('Account')
|
|
198
|
+
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
199
|
+
.addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
|
|
200
|
+
.addColumn('type', 'text', col => col.notNull())
|
|
201
|
+
.addColumn('provider', 'text', col => col.notNull())
|
|
202
|
+
.addColumn('providerAccountId', 'text', col => col.notNull())
|
|
203
|
+
.addColumn('refresh_token', 'text')
|
|
204
|
+
.addColumn('access_token', 'text')
|
|
205
|
+
.addColumn('expires_at', 'bigint')
|
|
206
|
+
.addColumn('token_type', 'text')
|
|
207
|
+
.addColumn('scope', 'text')
|
|
208
|
+
.addColumn('id_token', 'text')
|
|
209
|
+
.addColumn('session_state', 'text')
|
|
210
|
+
.execute()
|
|
211
|
+
.catch(relationExists('Account'));
|
|
212
|
+
opt.output('done');
|
|
213
|
+
opt.output('start', 'Creating index for Account.userId');
|
|
214
|
+
db.schema.createIndex('Account_userId_index').on('Account').column('userId').execute().catch(relationExists('Account_userId_index'));
|
|
215
|
+
opt.output('done');
|
|
216
|
+
opt.output('start', 'Creating table Session');
|
|
217
|
+
await db.schema
|
|
218
|
+
.createTable('Session')
|
|
219
|
+
.addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
|
|
220
|
+
.addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
|
|
221
|
+
.addColumn('sessionToken', 'text', col => col.notNull().unique())
|
|
222
|
+
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
223
|
+
.execute()
|
|
224
|
+
.catch(relationExists('Session'));
|
|
225
|
+
opt.output('done');
|
|
226
|
+
opt.output('start', 'Creating index for Session.userId');
|
|
227
|
+
db.schema.createIndex('Session_userId_index').on('Session').column('userId').execute().catch(relationExists('Session_userId_index'));
|
|
228
|
+
opt.output('done');
|
|
229
|
+
opt.output('start', 'Creating table VerificationToken');
|
|
230
|
+
await db.schema
|
|
231
|
+
.createTable('VerificationToken')
|
|
232
|
+
.addColumn('identifier', 'text', col => col.notNull())
|
|
233
|
+
.addColumn('token', 'text', col => col.notNull().unique())
|
|
234
|
+
.addColumn('expires', 'timestamptz', col => col.notNull())
|
|
235
|
+
.execute()
|
|
236
|
+
.catch(relationExists('VerificationToken'));
|
|
237
|
+
opt.output('done');
|
|
238
|
+
opt.output('start', 'Creating table Authenticator');
|
|
239
|
+
await db.schema
|
|
240
|
+
.createTable('Authenticator')
|
|
241
|
+
.addColumn('credentialID', 'text', col => col.primaryKey().notNull())
|
|
242
|
+
.addColumn('userId', 'uuid', col => col.notNull().references('User.id').onDelete('cascade').onUpdate('cascade'))
|
|
243
|
+
.addColumn('providerAccountId', 'text', col => col.notNull())
|
|
244
|
+
.addColumn('credentialPublicKey', 'text', col => col.notNull())
|
|
245
|
+
.addColumn('counter', 'integer', col => col.notNull())
|
|
246
|
+
.addColumn('credentialDeviceType', 'text', col => col.notNull())
|
|
247
|
+
.addColumn('credentialBackedUp', 'boolean', col => col.notNull())
|
|
248
|
+
.addColumn('transports', 'text')
|
|
249
|
+
.execute()
|
|
250
|
+
.catch(relationExists('Authenticator'));
|
|
251
|
+
opt.output('done');
|
|
252
|
+
opt.output('start', 'Creating index for Authenticator.credentialID');
|
|
253
|
+
db.schema.createIndex('Authenticator_credentialID_key').on('Authenticator').column('credentialID').execute().catch(relationExists('Authenticator_credentialID_key'));
|
|
254
|
+
opt.output('done');
|
|
255
|
+
return config.db;
|
|
256
|
+
}
|
|
257
|
+
catch (e_2) {
|
|
258
|
+
env_2.error = e_2;
|
|
259
|
+
env_2.hasError = true;
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
const result_2 = __disposeResources(env_2);
|
|
263
|
+
if (result_2)
|
|
264
|
+
await result_2;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Completely remove Axium from the database.
|
|
269
|
+
*/
|
|
270
|
+
export async function remove(opt) {
|
|
271
|
+
_fixOutput(opt);
|
|
272
|
+
await execSQL(opt, 'DROP DATABASE axium', 'Dropping database');
|
|
273
|
+
await execSQL(opt, 'REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
|
|
274
|
+
await execSQL(opt, 'DROP USER axium', 'Dropping user');
|
|
275
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
package/dist/io.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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;
|
|
7
|
+
/**
|
|
8
|
+
* Find the Axium directory.
|
|
9
|
+
* This directory includes things like config files, secrets, etc.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findDir(global: boolean): string;
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
*/
|
|
15
|
+
export declare function checkDir(path: string): void;
|
package/dist/io.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
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
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Find the Axium directory.
|
|
35
|
+
* This directory includes things like config files, secrets, etc.
|
|
36
|
+
*/
|
|
37
|
+
export function findDir(global) {
|
|
38
|
+
if (process.env.AXIUM_DIR)
|
|
39
|
+
return process.env.AXIUM_DIR;
|
|
40
|
+
if (process.getuid?.() === 0)
|
|
41
|
+
return '/etc/axium';
|
|
42
|
+
if (global)
|
|
43
|
+
return join(homedir(), '.axium');
|
|
44
|
+
return '.axium';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
*/
|
|
49
|
+
export function checkDir(path) {
|
|
50
|
+
if (existsSync(path))
|
|
51
|
+
return;
|
|
52
|
+
mkdirSync(path, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
if (process.getuid?.() === 0)
|
|
55
|
+
checkDir('/etc/axium');
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"main": "dist/index.js",
|
|
3
|
+
"version": "0.0.2",
|
|
5
4
|
"author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
|
|
6
5
|
"funding": {
|
|
7
6
|
"type": "individual",
|
|
@@ -15,23 +14,38 @@
|
|
|
15
14
|
"bugs": {
|
|
16
15
|
"url": "https://github.com/james-pre/axium/issues"
|
|
17
16
|
},
|
|
18
|
-
"types": "dist/index.d.ts",
|
|
19
17
|
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
|
-
"axium": "
|
|
24
|
+
"axium": "dist/cli.js"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc"
|
|
28
28
|
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@axium/core": ">=0.0.1"
|
|
31
|
+
},
|
|
29
32
|
"dependencies": {
|
|
33
|
+
"@auth/core": "^0.38.0",
|
|
34
|
+
"@auth/kysely-adapter": "^1.8.0",
|
|
30
35
|
"@types/pg": "^8.11.11",
|
|
36
|
+
"bcryptjs": "^3.0.2",
|
|
31
37
|
"chalk": "^5.4.1",
|
|
32
38
|
"commander": "^13.1.0",
|
|
33
39
|
"kysely": "^0.27.5",
|
|
34
40
|
"pg": "^8.14.1",
|
|
35
|
-
"
|
|
41
|
+
"utilium": "^2.2.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@auth/sveltekit": "^1.8.0",
|
|
45
|
+
"@sveltejs/adapter-node": "^5.2.12",
|
|
46
|
+
"@sveltejs/kit": "^2.20.2",
|
|
47
|
+
"@vitejs/plugin-basic-ssl": "^2.0.0",
|
|
48
|
+
"svelte": "^5.25.3",
|
|
49
|
+
"vite-plugin-mkcert": "^1.17.8"
|
|
36
50
|
}
|
|
37
51
|
}
|