@axium/server 0.7.6 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/apps.d.ts +15 -0
  2. package/dist/apps.js +20 -0
  3. package/dist/auth.d.ts +53 -30
  4. package/dist/auth.js +103 -130
  5. package/dist/cli.js +176 -42
  6. package/dist/config.d.ts +57 -312
  7. package/dist/config.js +65 -31
  8. package/dist/database.d.ts +31 -40
  9. package/dist/database.js +165 -62
  10. package/dist/io.js +6 -2
  11. package/dist/plugins.d.ts +8 -24
  12. package/dist/plugins.js +10 -14
  13. package/dist/routes.d.ts +55 -0
  14. package/dist/routes.js +54 -0
  15. package/package.json +11 -16
  16. package/web/api/index.ts +7 -0
  17. package/web/api/metadata.ts +35 -0
  18. package/web/api/passkeys.ts +56 -0
  19. package/web/api/readme.md +1 -0
  20. package/web/api/register.ts +83 -0
  21. package/web/api/schemas.ts +22 -0
  22. package/web/api/session.ts +33 -0
  23. package/web/api/users.ts +351 -0
  24. package/web/api/utils.ts +66 -0
  25. package/web/auth.ts +1 -5
  26. package/web/hooks.server.ts +12 -1
  27. package/web/index.server.ts +0 -1
  28. package/web/lib/ClipboardCopy.svelte +42 -0
  29. package/web/lib/Dialog.svelte +3 -6
  30. package/web/lib/FormDialog.svelte +61 -14
  31. package/web/lib/Toast.svelte +8 -1
  32. package/web/lib/UserCard.svelte +1 -1
  33. package/web/lib/auth.ts +12 -0
  34. package/web/lib/icons/Icon.svelte +7 -13
  35. package/web/lib/index.ts +0 -2
  36. package/web/lib/styles.css +18 -1
  37. package/web/routes/+layout.svelte +1 -1
  38. package/web/routes/[...path]/+page.server.ts +13 -0
  39. package/web/routes/[appId]/[...page]/+page.server.ts +14 -0
  40. package/web/routes/_axium/default/+page.svelte +11 -0
  41. package/web/routes/account/+page.svelte +291 -0
  42. package/web/routes/api/[...path]/+server.ts +49 -0
  43. package/web/routes/login/+page.svelte +25 -0
  44. package/web/routes/logout/+page.svelte +13 -0
  45. package/web/routes/register/+page.svelte +21 -0
  46. package/web/tsconfig.json +2 -1
  47. package/web/utils.ts +9 -15
  48. package/web/actions.ts +0 -58
  49. package/web/lib/Account.svelte +0 -76
  50. package/web/lib/SignUp.svelte +0 -20
  51. package/web/lib/account.css +0 -36
  52. package/web/routes/+page.server.ts +0 -16
  53. package/web/routes/+page.svelte +0 -10
  54. package/web/routes/name/+page.server.ts +0 -5
  55. package/web/routes/name/+page.svelte +0 -20
  56. package/web/routes/signup/+page.server.ts +0 -10
  57. package/web/routes/signup/+page.svelte +0 -15
package/dist/database.js CHANGED
@@ -52,6 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
52
52
  });
53
53
  import { Kysely, PostgresDialect, sql } from 'kysely';
54
54
  import { randomBytes } from 'node:crypto';
55
+ import { readFileSync, writeFileSync } from 'node:fs';
55
56
  import pg from 'pg';
56
57
  import config from './config.js';
57
58
  import { _fixOutput, run, someWarnings } from './io.js';
@@ -72,19 +73,21 @@ export function connect() {
72
73
  }
73
74
  export async function count(table) {
74
75
  const db = connect();
75
- return (await db.selectFrom(table).select(db.fn.countAll().as('count')).executeTakeFirstOrThrow()).count;
76
+ return (await db.selectFrom(table)
77
+ .select(db.fn.countAll().as('count'))
78
+ .executeTakeFirstOrThrow()).count;
76
79
  }
77
80
  export async function status() {
78
81
  return {
79
- users: await count('User'),
80
- accounts: await count('Account'),
81
- sessions: await count('Session'),
82
+ users: await count('users'),
83
+ passkeys: await count('passkeys'),
84
+ sessions: await count('sessions'),
82
85
  };
83
86
  }
84
87
  export async function statusText() {
85
88
  try {
86
89
  const stats = await status();
87
- return `${stats.users} users, ${stats.accounts} accounts, ${stats.sessions} sessions`;
90
+ return `${stats.users} users, ${stats.passkeys} passkeys, ${stats.sessions} sessions`;
88
91
  }
89
92
  catch (error) {
90
93
  throw typeof error == 'object' && 'message' in error ? error.message : error;
@@ -102,6 +105,30 @@ export function shouldRecreate(opt) {
102
105
  opt.output('warn', 'already exists. Use --skip to skip or --force to re-create.');
103
106
  throw 2;
104
107
  }
108
+ export async function getHBA(opt) {
109
+ const hbaShowResult = await run(opt, 'Finding pg_hba.conf', `sudo -u postgres psql -c "SHOW hba_file"`);
110
+ opt.output('start', 'Resolving pg_hba.conf path');
111
+ const hbaPath = hbaShowResult.match(/^\s*(.+\.conf)\s*$/m)?.[1]?.trim();
112
+ if (!hbaPath) {
113
+ throw 'failed. You will need to add password-based auth for the axium user manually.';
114
+ }
115
+ opt.output('done');
116
+ opt.output('debug', `Found pg_hba.conf at ${hbaPath}`);
117
+ opt.output('start', 'Reading HBA configuration');
118
+ const content = readFileSync(hbaPath, 'utf-8');
119
+ opt.output('done');
120
+ const writeBack = (newContent) => {
121
+ opt.output('start', 'Writing HBA configuration');
122
+ writeFileSync(hbaPath, newContent);
123
+ opt.output('done');
124
+ };
125
+ return [content, writeBack];
126
+ }
127
+ const pgHba = `
128
+ local axium axium md5
129
+ host axium axium 127.0.0.1/32 md5
130
+ host axium axium ::1/128 md5
131
+ `;
105
132
  export async function init(opt) {
106
133
  const env_1 = { stack: [], error: void 0, hasError: false };
107
134
  try {
@@ -110,8 +137,10 @@ export async function init(opt) {
110
137
  config.save({ db: { password: randomBytes(32).toString('base64') } }, true);
111
138
  opt.output('debug', 'Generated password and wrote to global config');
112
139
  }
140
+ await run(opt, 'Checking for sudo', 'which sudo');
141
+ await run(opt, 'Checking for psql', 'which psql');
113
142
  const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
114
- const warnExists = someWarnings(opt.output, [/(schema|relation) "\w+" already exists/, 'already exists.']);
143
+ const warnExists = someWarnings(opt.output, [/(schema|relation) "[\w.]+" already exists/, 'already exists.']);
115
144
  const done = () => opt.output('done');
116
145
  await _sql('CREATE DATABASE axium', 'Creating database').catch(async (error) => {
117
146
  if (error != 'database "axium" already exists')
@@ -134,79 +163,73 @@ export async function init(opt) {
134
163
  await _sql('GRANT ALL PRIVILEGES ON DATABASE axium TO axium', 'Granting database privileges');
135
164
  await _sql('GRANT ALL PRIVILEGES ON SCHEMA public TO axium', 'Granting schema privileges');
136
165
  await _sql('ALTER DATABASE axium OWNER TO axium', 'Setting database owner');
166
+ await getHBA(opt)
167
+ .then(([content, writeBack]) => {
168
+ opt.output('start', 'Checking for Axium HBA configuration');
169
+ if (content.includes(pgHba))
170
+ throw 'already exists.';
171
+ done();
172
+ opt.output('start', 'Adding Axium HBA configuration');
173
+ const newContent = content.replace(/^local\s+all\s+all.*$/m, `$&\n${pgHba}`);
174
+ done();
175
+ writeBack(newContent);
176
+ })
177
+ .catch(e => opt.output('warn', e));
137
178
  await _sql('SELECT pg_reload_conf()', 'Reloading configuration');
138
179
  const db = __addDisposableResource(env_1, connect(), true);
139
- opt.output('start', 'Creating table User');
180
+ opt.output('start', 'Creating table users');
140
181
  await db.schema
141
- .createTable('User')
182
+ .createTable('users')
142
183
  .addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
143
184
  .addColumn('name', 'text')
144
185
  .addColumn('email', 'text', col => col.unique().notNull())
145
186
  .addColumn('emailVerified', 'timestamptz')
146
187
  .addColumn('image', 'text')
147
- .addColumn('password', 'text')
148
- .addColumn('salt', 'text')
149
188
  .addColumn('preferences', 'jsonb', col => col.notNull().defaultTo(sql `'{}'::jsonb`))
150
189
  .execute()
151
190
  .then(done)
152
191
  .catch(warnExists);
153
- opt.output('start', 'Creating table Account');
192
+ opt.output('start', 'Creating table sessions');
154
193
  await db.schema
155
- .createTable('Account')
194
+ .createTable('sessions')
156
195
  .addColumn('id', 'uuid', col => col.primaryKey().defaultTo(sql `gen_random_uuid()`))
157
- .addColumn('userId', 'uuid', col => col.references('User.id').onDelete('cascade').notNull())
158
- .addColumn('type', 'text', col => col.notNull())
159
- .addColumn('provider', 'text', col => col.notNull())
160
- .addColumn('providerAccountId', 'text', col => col.notNull())
161
- .addColumn('refresh_token', 'text')
162
- .addColumn('access_token', 'text')
163
- .addColumn('expires_at', 'bigint')
164
- .addColumn('token_type', 'text')
165
- .addColumn('scope', 'text')
166
- .addColumn('id_token', 'text')
167
- .addColumn('session_state', 'text')
168
- .execute()
169
- .then(done)
170
- .catch(warnExists);
171
- opt.output('start', 'Creating index for Account.userId');
172
- await db.schema.createIndex('Account_userId_index').on('Account').column('userId').execute().then(done).catch(warnExists);
173
- opt.output('start', 'Creating table Session');
174
- await db.schema
175
- .createTable('Session')
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('sessionToken', 'text', col => col.notNull().unique())
196
+ .addColumn('userId', 'uuid', col => col.references('users.id').onDelete('cascade').notNull())
197
+ .addColumn('token', 'text', col => col.notNull().unique())
198
+ .addColumn('created', 'timestamptz', col => col.notNull())
179
199
  .addColumn('expires', 'timestamptz', col => col.notNull())
200
+ .addColumn('elevated', 'boolean', col => col.notNull())
180
201
  .execute()
181
202
  .then(done)
182
203
  .catch(warnExists);
183
- opt.output('start', 'Creating index for Session.userId');
184
- await db.schema.createIndex('Session_userId_index').on('Session').column('userId').execute().then(done).catch(warnExists);
185
- opt.output('start', 'Creating table VerificationToken');
204
+ opt.output('start', 'Creating index for sessions.userId');
205
+ await db.schema.createIndex('sessions_userId_index').on('sessions').column('userId').execute().then(done).catch(warnExists);
206
+ opt.output('start', 'Creating table verifications');
186
207
  await db.schema
187
- .createTable('VerificationToken')
188
- .addColumn('identifier', 'text', col => col.notNull())
208
+ .createTable('verifications')
209
+ .addColumn('userId', 'uuid', col => col.references('users.id').onDelete('cascade').notNull())
189
210
  .addColumn('token', 'text', col => col.notNull().unique())
190
211
  .addColumn('expires', 'timestamptz', col => col.notNull())
212
+ .addColumn('role', 'text', col => col.notNull())
191
213
  .execute()
192
214
  .then(done)
193
215
  .catch(warnExists);
194
- opt.output('start', 'Creating table Authenticator');
216
+ opt.output('start', 'Creating table passkeys');
195
217
  await db.schema
196
- .createTable('Authenticator')
197
- .addColumn('credentialID', 'text', col => col.primaryKey().notNull())
198
- .addColumn('userId', 'uuid', col => col.notNull().references('User.id').onDelete('cascade').onUpdate('cascade'))
199
- .addColumn('providerAccountId', 'text', col => col.notNull())
200
- .addColumn('credentialPublicKey', 'text', col => col.notNull())
218
+ .createTable('passkeys')
219
+ .addColumn('id', 'text', col => col.primaryKey().notNull())
220
+ .addColumn('name', 'text')
221
+ .addColumn('createdAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
222
+ .addColumn('userId', 'uuid', col => col.notNull().references('users.id').onDelete('cascade').onUpdate('cascade'))
223
+ .addColumn('publicKey', 'bytea', col => col.notNull())
201
224
  .addColumn('counter', 'integer', col => col.notNull())
202
- .addColumn('credentialDeviceType', 'text', col => col.notNull())
203
- .addColumn('credentialBackedUp', 'boolean', col => col.notNull())
204
- .addColumn('transports', 'text')
225
+ .addColumn('deviceType', 'text', col => col.notNull())
226
+ .addColumn('backedUp', 'boolean', col => col.notNull())
227
+ .addColumn('transports', sql `text[]`)
205
228
  .execute()
206
229
  .then(done)
207
230
  .catch(warnExists);
208
- opt.output('start', 'Creating index for Authenticator.credentialID');
209
- await db.schema.createIndex('Authenticator_credentialID_key').on('Authenticator').column('credentialID').execute().then(done).catch(warnExists);
231
+ opt.output('start', 'Creating index for passkeys.id');
232
+ await db.schema.createIndex('passkeys_id_key').on('passkeys').column('id').execute().then(done).catch(warnExists);
210
233
  for (const plugin of plugins) {
211
234
  if (!plugin.db_init)
212
235
  continue;
@@ -224,23 +247,103 @@ export async function init(opt) {
224
247
  await result_1;
225
248
  }
226
249
  }
227
- /**
228
- * Completely remove Axium from the database.
229
- */
230
- export async function uninstall(opt) {
250
+ export async function check(opt) {
251
+ const env_2 = { stack: [], error: void 0, hasError: false };
252
+ try {
253
+ _fixOutput(opt);
254
+ await run(opt, 'Checking for sudo', 'which sudo');
255
+ await run(opt, 'Checking for psql', 'which psql');
256
+ const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
257
+ const done = () => opt.output('done');
258
+ const throwUnlessRows = (text) => {
259
+ if (text.includes('(0 rows)'))
260
+ throw 'missing.';
261
+ return text;
262
+ };
263
+ await _sql(`SELECT 1 FROM pg_database WHERE datname = 'axium'`, 'Checking for database').then(throwUnlessRows);
264
+ await _sql(`SELECT 1 FROM pg_roles WHERE rolname = 'axium'`, 'Checking for user').then(throwUnlessRows);
265
+ opt.output('start', 'Connecting to database');
266
+ const db = __addDisposableResource(env_2, connect(), true);
267
+ done();
268
+ opt.output('start', `Checking users table`);
269
+ await db.selectFrom('users').select(['id', 'email', 'emailVerified', 'image', 'name', 'preferences']).execute().then(done);
270
+ opt.output('start', `Checking sessions table`);
271
+ await db.selectFrom('sessions').select(['id', 'userId', 'token', 'created', 'expires', 'elevated']).execute().then(done);
272
+ opt.output('start', `Checking verifications table`);
273
+ await db.selectFrom('verifications').select(['userId', 'token', 'expires', 'role']).execute().then(done);
274
+ opt.output('start', `Checking passkeys table`);
275
+ await db
276
+ .selectFrom('passkeys')
277
+ .select(['id', 'name', 'createdAt', 'userId', 'publicKey', 'counter', 'deviceType', 'backedUp', 'transports'])
278
+ .execute()
279
+ .then(done);
280
+ }
281
+ catch (e_2) {
282
+ env_2.error = e_2;
283
+ env_2.hasError = true;
284
+ }
285
+ finally {
286
+ const result_2 = __disposeResources(env_2);
287
+ if (result_2)
288
+ await result_2;
289
+ }
290
+ }
291
+ export async function clean(opt) {
231
292
  _fixOutput(opt);
293
+ const done = () => opt.output('done');
294
+ const now = new Date();
232
295
  const db = connect();
296
+ opt.output('start', 'Removing expired sessions');
297
+ await db.deleteFrom('sessions').where('sessions.expires', '<', now).execute().then(done);
298
+ opt.output('start', 'Removing expired verifications');
299
+ await db.deleteFrom('verifications').where('verifications.expires', '<', now).execute().then(done);
233
300
  for (const plugin of plugins) {
234
- if (!plugin.db_remove)
301
+ if (!plugin.db_clean)
235
302
  continue;
236
303
  opt.output('plugin', plugin.name);
237
- await plugin.db_remove(opt, db);
304
+ await plugin.db_clean(opt, db);
305
+ }
306
+ }
307
+ /**
308
+ * Completely remove Axium from the database.
309
+ */
310
+ export async function uninstall(opt) {
311
+ const env_3 = { stack: [], error: void 0, hasError: false };
312
+ try {
313
+ _fixOutput(opt);
314
+ const db = __addDisposableResource(env_3, connect(), true);
315
+ for (const plugin of plugins) {
316
+ if (!plugin.db_remove)
317
+ continue;
318
+ opt.output('plugin', plugin.name);
319
+ await plugin.db_remove(opt, db);
320
+ }
321
+ const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
322
+ await _sql('DROP DATABASE axium', 'Dropping database');
323
+ await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
324
+ await _sql('DROP USER axium', 'Dropping user');
325
+ await getHBA(opt)
326
+ .then(([content, writeBack]) => {
327
+ opt.output('start', 'Checking for Axium HBA configuration');
328
+ if (!content.includes(pgHba))
329
+ throw 'missing.';
330
+ opt.output('done');
331
+ opt.output('start', 'Removing Axium HBA configuration');
332
+ const newContent = content.replace(pgHba, '');
333
+ opt.output('done');
334
+ writeBack(newContent);
335
+ })
336
+ .catch(e => opt.output('warn', e));
337
+ }
338
+ catch (e_3) {
339
+ env_3.error = e_3;
340
+ env_3.hasError = true;
341
+ }
342
+ finally {
343
+ const result_3 = __disposeResources(env_3);
344
+ if (result_3)
345
+ await result_3;
238
346
  }
239
- await db.destroy();
240
- const _sql = (command, message) => run(opt, message, `sudo -u postgres psql -c "${command}"`);
241
- await _sql('DROP DATABASE axium', 'Dropping database');
242
- await _sql('REVOKE ALL PRIVILEGES ON SCHEMA public FROM axium', 'Revoking schema privileges');
243
- await _sql('DROP USER axium', 'Dropping user');
244
347
  }
245
348
  /**
246
349
  * Removes all data from tables.
@@ -254,7 +357,7 @@ export async function wipe(opt) {
254
357
  opt.output('plugin', plugin.name);
255
358
  await plugin.db_wipe(opt, db);
256
359
  }
257
- for (const table of ['User', 'Account', 'Session', 'VerificationToken', 'Authenticator']) {
360
+ for (const table of ['users', 'passkeys', 'sessions', 'verifications']) {
258
361
  opt.output('start', `Removing data from ${table}`);
259
362
  await db.deleteFrom(table).execute();
260
363
  opt.output('done');
package/dist/io.js CHANGED
@@ -65,7 +65,11 @@ export async function run(opts, message, command) {
65
65
  return value;
66
66
  }
67
67
  catch (error) {
68
- throw error == '[command]' ? stderr?.slice(0, 100) || 'failed.' : typeof error == 'object' && 'message' in error ? error.message : error;
68
+ throw error == '[command]'
69
+ ? stderr?.slice(0, 100) || 'failed.'
70
+ : typeof error == 'object' && 'message' in error
71
+ ? error.message
72
+ : error;
69
73
  }
70
74
  }
71
75
  /** Yet another convenience function */
@@ -149,7 +153,7 @@ export async function restrictedPorts(opt) {
149
153
  opt.output('done');
150
154
  return '/usr/sbin/setcap';
151
155
  });
152
- opt.output('debug', 'Using setup at ' + setcap);
156
+ opt.output('debug', 'Using setcap at ' + setcap);
153
157
  let { node } = opt;
154
158
  node ||= await run(opt, 'Finding node', 'command -v node')
155
159
  .then(e => e.trim())
package/dist/plugins.d.ts CHANGED
@@ -1,32 +1,16 @@
1
- import * as z from 'zod';
1
+ import z from 'zod/v4';
2
+ export declare const fn: z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>;
2
3
  export declare const Plugin: z.ZodObject<{
3
4
  id: z.ZodString;
4
5
  name: z.ZodString;
5
6
  version: z.ZodString;
6
7
  description: z.ZodOptional<z.ZodString>;
7
- statusText: z.ZodOptional<z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnion<[z.ZodString, z.ZodPromise<z.ZodString>]>>>;
8
- db_init: z.ZodOptional<z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>>;
9
- db_remove: z.ZodOptional<z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>>;
10
- db_wipe: z.ZodOptional<z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodUnknown>>;
11
- }, "strip", z.ZodTypeAny, {
12
- name: string;
13
- id: string;
14
- version: string;
15
- description?: string | undefined;
16
- statusText?: ((...args: unknown[]) => string | Promise<string>) | undefined;
17
- db_init?: ((...args: unknown[]) => unknown) | undefined;
18
- db_remove?: ((...args: unknown[]) => unknown) | undefined;
19
- db_wipe?: ((...args: unknown[]) => unknown) | undefined;
20
- }, {
21
- name: string;
22
- id: string;
23
- version: string;
24
- description?: string | undefined;
25
- statusText?: ((...args: unknown[]) => string | Promise<string>) | undefined;
26
- db_init?: ((...args: unknown[]) => unknown) | undefined;
27
- db_remove?: ((...args: unknown[]) => unknown) | undefined;
28
- db_wipe?: ((...args: unknown[]) => unknown) | undefined;
29
- }>;
8
+ statusText: z.ZodCustom<z.core.$InferInnerFunctionTypeAsync<z.core.$ZodTuple<[], null>, z.ZodString>, z.core.$InferInnerFunctionTypeAsync<z.core.$ZodTuple<[], null>, z.ZodString>>;
9
+ db_init: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
10
+ db_remove: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
11
+ db_wipe: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
12
+ db_clean: z.ZodOptional<z.ZodCustom<(...args: unknown[]) => any, (...args: unknown[]) => any>>;
13
+ }, z.core.$strip>;
30
14
  export interface Plugin extends z.infer<typeof Plugin> {
31
15
  }
32
16
  export declare const plugins: Set<Plugin>;
package/dist/plugins.js CHANGED
@@ -1,23 +1,20 @@
1
1
  import * as fs from 'node:fs';
2
2
  import { join, resolve } from 'node:path/posix';
3
3
  import { styleText } from 'node:util';
4
- import * as z from 'zod';
5
- import { fromZodError } from 'zod-validation-error';
4
+ import z from 'zod/v4';
6
5
  import { findDir, output } from './io.js';
7
- import { fileURLToPath } from 'node:url';
6
+ import { zAsyncFunction } from '@axium/core/schemas';
7
+ export const fn = z.custom(data => typeof data === 'function');
8
8
  export const Plugin = z.object({
9
9
  id: z.string(),
10
10
  name: z.string(),
11
11
  version: z.string(),
12
12
  description: z.string().optional(),
13
- statusText: z
14
- .function()
15
- .args()
16
- .returns(z.union([z.string(), z.promise(z.string())]))
17
- .optional(),
18
- db_init: z.function().optional(),
19
- db_remove: z.function().optional(),
20
- db_wipe: z.function().optional(),
13
+ statusText: zAsyncFunction(z.function({ input: [], output: z.string() })),
14
+ db_init: fn.optional(),
15
+ db_remove: fn.optional(),
16
+ db_wipe: fn.optional(),
17
+ db_clean: fn.optional(),
21
18
  });
22
19
  export const plugins = new Set();
23
20
  export function resolvePlugin(search) {
@@ -32,7 +29,6 @@ export function pluginText(plugin) {
32
29
  plugin.id,
33
30
  `Version: ${plugin.version}`,
34
31
  `Description: ${plugin.description ?? styleText('dim', '(none)')}`,
35
- `Status text integration: ${plugin.statusText ? styleText('whiteBright', 'yes') : styleText('yellow', 'no')}`,
36
32
  `Database integration: ${[plugin.db_init, plugin.db_remove, plugin.db_wipe]
37
33
  .filter(Boolean)
38
34
  .map(fn => fn?.name.slice(3))
@@ -41,8 +37,8 @@ export function pluginText(plugin) {
41
37
  }
42
38
  export async function loadPlugin(specifier) {
43
39
  try {
44
- const plugin = await Plugin.parseAsync(await import(specifier)).catch(e => {
45
- throw fromZodError(e);
40
+ const plugin = await Plugin.parseAsync(await import(/* @vite-ignore */ specifier)).catch(e => {
41
+ throw z.prettifyError(e);
46
42
  });
47
43
  plugins.add(plugin);
48
44
  output.debug(`Loaded plugin: "${plugin.name}" (${plugin.id}) ${plugin.version}`);
@@ -0,0 +1,55 @@
1
+ import type { RequestMethod } from '@axium/core/requests';
2
+ import type { LoadEvent, RequestEvent } from '@sveltejs/kit';
3
+ import type { Component } from 'svelte';
4
+ import type z from 'zod/v4';
5
+ type _Params = Partial<Record<string, string>>;
6
+ export type EndpointHandlers<Params extends _Params = _Params> = Partial<Record<RequestMethod, (event: RequestEvent<Params>) => object | Promise<object>>>;
7
+ export type RouteParamOptions = z.ZodType;
8
+ export interface CommonRouteOptions<Params extends _Params = _Params> {
9
+ path: string;
10
+ params?: {
11
+ [K in keyof Params]?: RouteParamOptions;
12
+ };
13
+ }
14
+ /**
15
+ * A route with server-side handlers for different HTTP methods.
16
+ */
17
+ export interface ServerRouteOptions<Params extends _Params = _Params> extends CommonRouteOptions<Params>, EndpointHandlers<Params> {
18
+ }
19
+ export interface WebRouteOptions extends CommonRouteOptions {
20
+ load?(event: RequestEvent): object | Promise<object>;
21
+ /** the Svelte page */
22
+ page?: Component;
23
+ }
24
+ export type RouteOptions = ServerRouteOptions | WebRouteOptions;
25
+ export interface RouteCommon {
26
+ path: string;
27
+ params?: Record<string, RouteParamOptions>;
28
+ [kBuiltin]: boolean;
29
+ }
30
+ export interface ServerRoute extends RouteCommon, EndpointHandlers {
31
+ server: true;
32
+ }
33
+ export interface WebRoute extends RouteCommon {
34
+ server: false;
35
+ load?(event: LoadEvent): object | Promise<object>;
36
+ page?: Component;
37
+ }
38
+ export type Route = ServerRoute | WebRoute;
39
+ /**
40
+ * @internal
41
+ */
42
+ export declare const routes: Map<string, Route>;
43
+ declare const kBuiltin: unique symbol;
44
+ export declare function addRoute(opt: RouteOptions, _routeMap?: Map<string, Route>): void;
45
+ /**
46
+ * Resolve a request URL into a route.
47
+ * This handles parsing of parameters in the URL.
48
+ */
49
+ export declare function resolveRoute<T extends Route>(event: RequestEvent | LoadEvent, _routeMap?: Map<string, T>): T | undefined;
50
+ /**
51
+ * This function marks all existing routes as built-in.
52
+ * @internal
53
+ */
54
+ export declare function _markDefaults(): void;
55
+ export {};
package/dist/routes.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @internal
3
+ */
4
+ export const routes = new Map();
5
+ const kBuiltin = Symbol('kBuiltin');
6
+ export function addRoute(opt, _routeMap = routes) {
7
+ const route = { ...opt, server: !('page' in opt), [kBuiltin]: false };
8
+ if (!route.path.startsWith('/')) {
9
+ throw new Error(`Route path must start with a slash: ${route.path}`);
10
+ }
11
+ if (route.path.startsWith('/api/') && !route.server) {
12
+ throw new Error(`API routes cannot have a client page: ${route.path}`);
13
+ }
14
+ _routeMap.set(route.path, route);
15
+ }
16
+ /**
17
+ * Resolve a request URL into a route.
18
+ * This handles parsing of parameters in the URL.
19
+ */
20
+ export function resolveRoute(event, _routeMap = routes) {
21
+ const { pathname } = event.url;
22
+ if (_routeMap.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
23
+ return _routeMap.get(pathname);
24
+ // Otherwise we must have a parameterized route
25
+ routes: for (const route of _routeMap.values()) {
26
+ const params = {};
27
+ // Split the path and route into parts, zipped together
28
+ const pathParts = pathname.split('/').filter(Boolean);
29
+ for (const routePart of route.path.split('/').filter(Boolean)) {
30
+ const pathPart = pathParts.shift();
31
+ if (!pathPart)
32
+ continue routes;
33
+ if (pathPart == routePart)
34
+ continue;
35
+ if (!routePart.startsWith(':'))
36
+ continue routes;
37
+ params[routePart.slice(1)] = pathPart;
38
+ }
39
+ // we didn't find a match, since an exact match would have been found already
40
+ if (pathParts.length || !Object.keys(params).length)
41
+ continue;
42
+ event.params = params;
43
+ return route;
44
+ }
45
+ }
46
+ /**
47
+ * This function marks all existing routes as built-in.
48
+ * @internal
49
+ */
50
+ export function _markDefaults() {
51
+ for (const route of routes.values()) {
52
+ route[kBuiltin] = true;
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.7.6",
3
+ "version": "0.9.0",
4
4
  "author": "James Prevett <axium@jamespre.dev> (https://jamespre.dev)",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -19,10 +19,9 @@
19
19
  "types": "dist/index.d.ts",
20
20
  "exports": {
21
21
  ".": "./dist/index.js",
22
- "./*": "./dist/*",
22
+ "./*": "./dist/*.js",
23
23
  "./web": "./web/index.js",
24
- "./web/*": "./web/*",
25
- "./web/server": "./web/index.server.js"
24
+ "./web/*": "./web/*"
26
25
  },
27
26
  "files": [
28
27
  "dist",
@@ -35,27 +34,23 @@
35
34
  "build": "tsc"
36
35
  },
37
36
  "peerDependencies": {
38
- "@axium/core": ">=0.0.2"
37
+ "@axium/core": ">=0.3.0",
38
+ "@axium/client": ">=0.0.2",
39
+ "utilium": "^2.3.8",
40
+ "zod": "^3.25.61"
39
41
  },
40
42
  "dependencies": {
41
- "@auth/core": "^0.38.0",
42
- "@auth/kysely-adapter": "^1.8.0",
43
+ "@simplewebauthn/server": "^13.1.1",
44
+ "@sveltejs/kit": "^2.20.2",
43
45
  "@types/pg": "^8.11.11",
44
- "bcryptjs": "^3.0.2",
45
46
  "commander": "^13.1.0",
46
- "kysely": "^0.27.5",
47
+ "kysely": "^0.28.0",
47
48
  "logzen": "^0.7.0",
48
49
  "mime": "^4.0.7",
49
- "pg": "^8.14.1",
50
- "utilium": "^2.3.0",
51
- "zod-validation-error": "^3.4.0"
50
+ "pg": "^8.14.1"
52
51
  },
53
52
  "devDependencies": {
54
- "@auth/sveltekit": "^1.8.0",
55
- "@simplewebauthn/browser": "^9.0.1",
56
- "@simplewebauthn/server": "^9.0.3",
57
53
  "@sveltejs/adapter-node": "^5.2.12",
58
- "@sveltejs/kit": "^2.20.2",
59
54
  "svelte": "^5.25.3",
60
55
  "vite-plugin-mkcert": "^1.17.8"
61
56
  }
@@ -0,0 +1,7 @@
1
+ import './metadata.js';
2
+ import './passkeys.js';
3
+ import './register.js';
4
+ import './session.js';
5
+ import './users.js';
6
+ export * from './schemas.js';
7
+ export * from './utils.js';
@@ -0,0 +1,35 @@
1
+ import type { Result } from '@axium/core/api';
2
+ import { requestMethods } from '@axium/core/requests';
3
+ import { config } from '@axium/server/config';
4
+ import { plugins } from '@axium/server/plugins';
5
+ import { addRoute, routes } from '@axium/server/routes';
6
+ import { error } from '@sveltejs/kit';
7
+ import pkg from '../../package.json' with { type: 'json' };
8
+
9
+ addRoute({
10
+ path: '/api/metadata',
11
+ async GET(): Result<'GET', 'metadata'> {
12
+ if (config.api.disable_metadata) {
13
+ error(401, { message: 'API metadata is disabled' });
14
+ }
15
+
16
+ return {
17
+ version: pkg.version,
18
+ routes: Object.fromEntries(
19
+ routes
20
+ .entries()
21
+ .filter(([path]) => path.startsWith('/api/'))
22
+ .map(([path, route]) => [
23
+ path,
24
+ {
25
+ params: Object.fromEntries(
26
+ Object.entries(route.params || {}).map(([key, type]) => [key, type ? type.def.type : null])
27
+ ),
28
+ methods: requestMethods.filter(m => m in route),
29
+ },
30
+ ])
31
+ ),
32
+ plugins: Object.fromEntries(plugins.values().map(plugin => [plugin.id, plugin.version])),
33
+ };
34
+ },
35
+ });