@axium/server 0.26.2 → 0.27.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.
package/dist/acl.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccessControl, Permission, UserInternal } from '@axium/core';
1
+ import type { AccessControl, AccessMap, Permission, UserInternal } from '@axium/core';
2
2
  import type { AliasedRawBuilder, Expression, ExpressionBuilder, Selectable } from 'kysely';
3
3
  import * as db from './database.js';
4
4
  export interface Target {
@@ -57,4 +57,5 @@ export interface ACLSelectionOptions {
57
57
  */
58
58
  export declare function from(table: TargetName, opt?: ACLSelectionOptions): (eb: ExpressionBuilder<db.Schema, any>) => AliasedRawBuilder<Required<AccessControl>[], 'acl'>;
59
59
  export declare function get(itemType: TargetName, itemId: string): Promise<Required<AccessControlInternal>[]>;
60
+ export declare function set(itemType: TargetName, itemId: string, data: AccessMap): Promise<AccessControlInternal[]>;
60
61
  export {};
package/dist/acl.js CHANGED
@@ -64,3 +64,21 @@ export function from(table, opt = {}) {
64
64
  export async function get(itemType, itemId) {
65
65
  return await db.database.selectFrom(`acl.${itemType}`).where('itemId', '=', itemId).selectAll().select(db.userFromId).execute();
66
66
  }
67
+ export async function set(itemType, itemId, data) {
68
+ if ('public' in data) {
69
+ // @ts-expect-error 2353 - TS misses the column
70
+ await db.database.updateTable(itemType).set({ publicPermission: data.public }).where('id', '=', itemId).execute();
71
+ delete data.public;
72
+ }
73
+ const entries = Object.entries(data).map(([userId, perm]) => ({ userId, perm }));
74
+ if (!entries.length)
75
+ return [];
76
+ return await db.database
77
+ .updateTable(`acl.${itemType}`)
78
+ .from(db.values(entries, 'data'))
79
+ .set('permission', eb => eb.ref('data.perm'))
80
+ .whereRef(`acl.${itemType}.userId`, '=', 'data.userId')
81
+ .where('itemId', '=', itemId)
82
+ .returningAll()
83
+ .execute();
84
+ }
package/dist/api/acl.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Permission } from '@axium/core/access';
1
+ import { AccessMap } from '@axium/core/access';
2
2
  import * as z from 'zod';
3
3
  import * as acl from '../acl.js';
4
4
  import { parseBody, withError } from '../requests.js';
@@ -9,11 +9,11 @@ addRoute({
9
9
  itemType: z.string(),
10
10
  itemId: z.uuid(),
11
11
  },
12
- async PUT(request, params) {
13
- const type = params.itemType;
14
- const itemId = params.itemId;
15
- const data = await parseBody(request, z.object({ userId: z.uuid(), permission: Permission }));
16
- const share = await acl.createEntry(type, { ...data, itemId }).catch(withError('Failed to create access control'));
17
- return share;
12
+ async GET(request, { itemType, itemId }) {
13
+ return await acl.get(itemType, itemId).catch(withError('Failed to get access controls'));
14
+ },
15
+ async POST(request, { itemType, itemId }) {
16
+ const data = await parseBody(request, AccessMap);
17
+ return await acl.set(itemType, itemId, data).catch(withError('Failed to set access controls'));
18
18
  },
19
19
  });
package/dist/api/admin.js CHANGED
@@ -55,9 +55,9 @@ addRoute({
55
55
  addRoute({
56
56
  path: '/api/admin/users/:userId',
57
57
  params: { userId: z.uuid() },
58
- async GET(req, params) {
58
+ async GET(req, { userId }) {
59
59
  await assertAdmin(this, req);
60
- if (!params.userId)
60
+ if (!userId)
61
61
  error(400, 'Missing user ID');
62
62
  const user = await db
63
63
  .selectFrom('users')
@@ -65,7 +65,7 @@ addRoute({
65
65
  .select(eb => jsonArrayFrom(eb.selectFrom('sessions').whereRef('sessions.userId', '=', 'users.id').selectAll())
66
66
  .$castTo()
67
67
  .as('sessions'))
68
- .where('id', '=', params.userId)
68
+ .where('id', '=', userId)
69
69
  .executeTakeFirstOrThrow()
70
70
  .catch(withError('User not found', 404));
71
71
  return {
@@ -134,9 +134,9 @@ addRoute({
134
134
  addRoute({
135
135
  path: '/api/admin/audit/:eventId',
136
136
  params: { eventId: z.uuid() },
137
- async GET(req, params) {
137
+ async GET(req, { eventId }) {
138
138
  await assertAdmin(this, req);
139
- if (!params.eventId)
139
+ if (!eventId)
140
140
  error(400, 'Missing event ID');
141
141
  const event = await db
142
142
  .selectFrom('audit_log')
@@ -144,7 +144,7 @@ addRoute({
144
144
  .select(eb => jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'audit_log.userId').selectAll())
145
145
  .$castTo()
146
146
  .as('user'))
147
- .where('id', '=', params.eventId)
147
+ .where('id', '=', eventId)
148
148
  .executeTakeFirstOrThrow()
149
149
  .catch(withError('Audit event not found', 404));
150
150
  return event;
@@ -10,14 +10,14 @@ addRoute({
10
10
  params: {
11
11
  id: z.string(),
12
12
  },
13
- async GET(request, params) {
14
- const passkey = await getPasskey(params.id);
13
+ async GET(request, { id }) {
14
+ const passkey = await getPasskey(id);
15
15
  await checkAuthForUser(request, passkey.userId);
16
16
  return omit(passkey, 'counter', 'publicKey');
17
17
  },
18
- async PATCH(request, params) {
18
+ async PATCH(request, { id }) {
19
19
  const body = await parseBody(request, PasskeyChangeable);
20
- const passkey = await getPasskey(params.id);
20
+ const passkey = await getPasskey(id);
21
21
  await checkAuthForUser(request, passkey.userId);
22
22
  const result = await db
23
23
  .updateTable('passkeys')
@@ -28,8 +28,8 @@ addRoute({
28
28
  .catch(withError('Could not update passkey'));
29
29
  return omit(result, 'counter', 'publicKey');
30
30
  },
31
- async DELETE(request, params) {
32
- const passkey = await getPasskey(params.id);
31
+ async DELETE(request, { id }) {
32
+ const passkey = await getPasskey(id);
33
33
  await checkAuthForUser(request, passkey.userId);
34
34
  const { count } = await db
35
35
  .selectFrom('passkeys')
@@ -48,7 +48,7 @@ async function POST(request) {
48
48
  const { verified, registrationInfo } = await verifyRegistrationResponse({
49
49
  response,
50
50
  expectedChallenge,
51
- expectedOrigin: config.auth.origin,
51
+ expectedOrigin: config.origin,
52
52
  }).catch(() => error(400, 'Verification failed'));
53
53
  if (!verified || !registrationInfo)
54
54
  error(401, 'Verification failed');
package/dist/api/users.js CHANGED
@@ -32,14 +32,12 @@ addRoute({
32
32
  addRoute({
33
33
  path: '/api/users/:id',
34
34
  params,
35
- async GET(request, params) {
36
- const userId = params.id;
35
+ async GET(request, { id: userId }) {
37
36
  const auth = await checkAuthForUser(request, userId).catch(() => null);
38
37
  const user = auth?.user || (await getUser(userId).catch(withError('User does not exist', 404)));
39
38
  return stripUser(user, !!auth);
40
39
  },
41
- async PATCH(request, params) {
42
- const userId = params.id;
40
+ async PATCH(request, { id: userId }) {
43
41
  const body = await parseBody(request, UserChangeable);
44
42
  await checkAuthForUser(request, userId);
45
43
  if ('email' in body)
@@ -55,8 +53,7 @@ addRoute({
55
53
  .catch(withError('Failed to update user'));
56
54
  return stripUser(result, true);
57
55
  },
58
- async DELETE(request, params) {
59
- const userId = params.id;
56
+ async DELETE(request, { id: userId }) {
60
57
  await checkAuthForUser(request, userId, true);
61
58
  const result = await db
62
59
  .deleteFrom('users')
@@ -71,8 +68,7 @@ addRoute({
71
68
  addRoute({
72
69
  path: '/api/users/:id/full',
73
70
  params,
74
- async GET(request, params) {
75
- const userId = params.id;
71
+ async GET(request, { id: userId }) {
76
72
  const { user } = await checkAuthForUser(request, userId);
77
73
  const sessions = await getSessions(userId);
78
74
  return {
@@ -84,8 +80,7 @@ addRoute({
84
80
  addRoute({
85
81
  path: '/api/users/:id/auth',
86
82
  params,
87
- async OPTIONS(request, params) {
88
- const userId = params.id;
83
+ async OPTIONS(request, { id: userId }) {
89
84
  const { type } = await parseBody(request, UserAuthOptions);
90
85
  const user = await getUser(userId).catch(withError('User does not exist', 404));
91
86
  if (user.isSuspended)
@@ -100,8 +95,7 @@ addRoute({
100
95
  challenges.set(userId, { data: options.challenge, type });
101
96
  return options;
102
97
  },
103
- async POST(request, params) {
104
- const userId = params.id;
98
+ async POST(request, { id: userId }) {
105
99
  const response = await parseBody(request, PasskeyAuthenticationResponse);
106
100
  const auth = challenges.get(userId);
107
101
  if (!auth)
@@ -116,7 +110,7 @@ addRoute({
116
110
  response,
117
111
  credential: passkey,
118
112
  expectedChallenge,
119
- expectedOrigin: config.auth.origin,
113
+ expectedOrigin: config.origin,
120
114
  expectedRPID: config.auth.rp_id,
121
115
  })
122
116
  .catch(withError('Verification failed', 400));
@@ -143,8 +137,7 @@ addRoute({
143
137
  /**
144
138
  * Get passkey registration options for a user.
145
139
  */
146
- async OPTIONS(request, params) {
147
- const userId = params.id;
140
+ async OPTIONS(request, { id: userId }) {
148
141
  const existing = await getPasskeysByUserId(userId);
149
142
  const { user } = await checkAuthForUser(request, userId);
150
143
  const options = await webauthn.generateRegistrationOptions({
@@ -166,8 +159,7 @@ addRoute({
166
159
  /**
167
160
  * Get passkeys for a user.
168
161
  */
169
- async GET(request, params) {
170
- const userId = params.id;
162
+ async GET(request, { id: userId }) {
171
163
  await checkAuthForUser(request, userId);
172
164
  const passkeys = await getPasskeysByUserId(userId);
173
165
  return passkeys.map(p => omit(p, 'publicKey', 'counter'));
@@ -175,8 +167,7 @@ addRoute({
175
167
  /**
176
168
  * Register a new passkey for an existing user.
177
169
  */
178
- async PUT(request, params) {
179
- const userId = params.id;
170
+ async PUT(request, { id: userId }) {
180
171
  const response = await parseBody(request, PasskeyRegistration);
181
172
  await checkAuthForUser(request, userId);
182
173
  const expectedChallenge = registrations.get(userId);
@@ -187,7 +178,7 @@ addRoute({
187
178
  .verifyRegistrationResponse({
188
179
  response,
189
180
  expectedChallenge,
190
- expectedOrigin: config.auth.origin,
181
+ expectedOrigin: config.origin,
191
182
  })
192
183
  .catch(withError('Verification failed', 400));
193
184
  if (!verified || !registrationInfo)
@@ -205,13 +196,11 @@ addRoute({
205
196
  addRoute({
206
197
  path: '/api/users/:id/sessions',
207
198
  params,
208
- async GET(request, params) {
209
- const userId = params.id;
199
+ async GET(request, { id: userId }) {
210
200
  await checkAuthForUser(request, userId);
211
201
  return (await getSessions(userId).catch(e => error(503, 'Failed to get sessions' + (config.debug ? ': ' + e : '')))).map(s => omit(s, 'token'));
212
202
  },
213
- async DELETE(request, params) {
214
- const userId = params.id;
203
+ async DELETE(request, { id: userId }) {
215
204
  const body = await parseBody(request, LogoutSessions);
216
205
  await checkAuthForUser(request, userId, body.confirm_all);
217
206
  if (!body.confirm_all && !Array.isArray(body.id))
@@ -229,8 +218,7 @@ addRoute({
229
218
  addRoute({
230
219
  path: '/api/users/:id/verify_email',
231
220
  params,
232
- async OPTIONS(request, params) {
233
- const userId = params.id;
221
+ async OPTIONS(request, { id: userId }) {
234
222
  if (!config.auth.email_verification)
235
223
  return { enabled: false };
236
224
  await checkAuthForUser(request, userId);
@@ -238,16 +226,14 @@ addRoute({
238
226
  return { enabled: false };
239
227
  return { enabled: true };
240
228
  },
241
- async GET(request, params) {
242
- const userId = params.id;
229
+ async GET(request, { id: userId }) {
243
230
  const { user } = await checkAuthForUser(request, userId);
244
231
  if (user.emailVerified)
245
232
  error(409, 'Email already verified');
246
233
  const verification = await createVerification('verify_email', userId, config.auth.verification_timeout * 60);
247
234
  return omit(verification, 'token', 'role');
248
235
  },
249
- async POST(request, params) {
250
- const userId = params.id;
236
+ async POST(request, { id: userId }) {
251
237
  const { token } = await parseBody(request, z.object({ token: z.string() }));
252
238
  const { user } = await checkAuthForUser(request, userId);
253
239
  if (user.emailVerified)
package/dist/audit.d.ts CHANGED
@@ -25,6 +25,9 @@ export interface $EventTypes {
25
25
  route: string;
26
26
  session: string;
27
27
  };
28
+ response_error: {
29
+ stack?: string;
30
+ };
28
31
  }
29
32
  export type EventName = keyof $EventTypes;
30
33
  export type EventExtra<T extends EventName> = $EventTypes[T];
package/dist/audit.js CHANGED
@@ -74,7 +74,7 @@ export async function audit(eventName, userId, extra) {
74
74
  }
75
75
  }
76
76
  export function getEvents(filter) {
77
- let query = database.selectFrom('audit_log').selectAll();
77
+ let query = database.selectFrom('audit_log').selectAll().orderBy('timestamp', 'desc');
78
78
  if ('user' in filter && !filter.user)
79
79
  query = query.where('userId', 'is', null);
80
80
  else if (filter.user)
@@ -120,3 +120,10 @@ addEvent({
120
120
  tags: ['auth'],
121
121
  extra: { route: z.string(), session: z.string() },
122
122
  });
123
+ addEvent({
124
+ source: '@axium/server',
125
+ name: 'response_error',
126
+ severity: Severity.Error,
127
+ tags: [],
128
+ extra: { stack: z.string().optional() },
129
+ });
package/dist/cli.js CHANGED
@@ -529,8 +529,9 @@ try {
529
529
  .addOption(opts.host)
530
530
  .addOption(opts.check)
531
531
  .addOption(opts.packagesDir)
532
+ .option('-s, --skip', 'Skip already initialized steps')
532
533
  .action(async (opt) => {
533
- await db.init({ ...opt, skip: opt.dbSkip }).catch(io.handleError);
534
+ await db.init(opt).catch(io.handleError);
534
535
  await restrictedPorts({ method: 'node-cap', action: 'enable' }).catch(io.handleError);
535
536
  });
536
537
  program
@@ -546,7 +547,7 @@ try {
546
547
  ssl_key: opt.ssl ? join(opt.ssl, 'key.pem') : config.web.ssl_key,
547
548
  build: opt.build ? resolve(opt.build) : config.web.build,
548
549
  });
549
- const port = !Number.isNaN(Number.parseInt(opt.port ?? '')) ? Number.parseInt(opt.port) : config.web.port;
550
+ const port = !Number.isNaN(Number.parseInt(opt.port ?? 'NaN')) ? Number.parseInt(opt.port) : config.web.port;
550
551
  server.listen(port, () => {
551
552
  console.log('Server is listening on port ' + port);
552
553
  });
package/dist/config.d.ts CHANGED
@@ -13,7 +13,6 @@ export declare const ConfigSchema: z.ZodObject<{
13
13
  auto_suspend: z.ZodOptional<z.ZodLiteral<"Emergency" | "Alert" | "Critical" | "Error" | "Warning" | "Notice" | "Info" | "Debug" | "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug">>;
14
14
  }, z.core.$loose>>;
15
15
  auth: z.ZodOptional<z.ZodObject<{
16
- origin: z.ZodOptional<z.ZodString>;
17
16
  passkey_probation: z.ZodOptional<z.ZodNumber>;
18
17
  rp_id: z.ZodOptional<z.ZodString>;
19
18
  rp_name: z.ZodOptional<z.ZodString>;
@@ -41,6 +40,7 @@ export declare const ConfigSchema: z.ZodObject<{
41
40
  }>>;
42
41
  console: z.ZodOptional<z.ZodBoolean>;
43
42
  }, z.core.$loose>>;
43
+ origin: z.ZodOptional<z.ZodString>;
44
44
  request_size_limit: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
45
45
  show_duplicate_state: z.ZodOptional<z.ZodBoolean>;
46
46
  web: z.ZodOptional<z.ZodObject<{
@@ -87,7 +87,6 @@ export declare const FileSchema: z.ZodObject<{
87
87
  auto_suspend: z.ZodOptional<z.ZodLiteral<"Emergency" | "Alert" | "Critical" | "Error" | "Warning" | "Notice" | "Info" | "Debug" | "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug">>;
88
88
  }, z.core.$loose>>>;
89
89
  auth: z.ZodOptional<z.ZodOptional<z.ZodObject<{
90
- origin: z.ZodOptional<z.ZodString>;
91
90
  passkey_probation: z.ZodOptional<z.ZodNumber>;
92
91
  rp_id: z.ZodOptional<z.ZodString>;
93
92
  rp_name: z.ZodOptional<z.ZodString>;
@@ -115,6 +114,7 @@ export declare const FileSchema: z.ZodObject<{
115
114
  }>>;
116
115
  console: z.ZodOptional<z.ZodBoolean>;
117
116
  }, z.core.$loose>>>;
117
+ origin: z.ZodOptional<z.ZodOptional<z.ZodString>>;
118
118
  request_size_limit: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodNumber>>>;
119
119
  show_duplicate_state: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
120
120
  web: z.ZodOptional<z.ZodOptional<z.ZodObject<{
package/dist/config.js CHANGED
@@ -30,7 +30,6 @@ export const ConfigSchema = z
30
30
  .partial(),
31
31
  auth: z
32
32
  .looseObject({
33
- origin: z.string(),
34
33
  /** In minutes */
35
34
  passkey_probation: z.number(),
36
35
  rp_id: z.string(),
@@ -62,6 +61,7 @@ export const ConfigSchema = z
62
61
  console: z.boolean(),
63
62
  })
64
63
  .partial(),
64
+ origin: z.string(),
65
65
  request_size_limit: z.number().min(0).optional(),
66
66
  show_duplicate_state: z.boolean(),
67
67
  web: z
@@ -95,7 +95,6 @@ export const defaultConfig = {
95
95
  auto_suspend: 'critical',
96
96
  },
97
97
  auth: {
98
- origin: 'https://test.localhost',
99
98
  passkey_probation: 60,
100
99
  rp_id: 'test.localhost',
101
100
  rp_name: 'Axium',
@@ -122,6 +121,7 @@ export const defaultConfig = {
122
121
  console: true,
123
122
  level: 'info',
124
123
  },
124
+ origin: 'https://test.localhost',
125
125
  show_duplicate_state: false,
126
126
  request_size_limit: 0,
127
127
  web: {
@@ -75,6 +75,10 @@ export type TablesMatching<T> = (string & keyof Schema) & keyof {
75
75
  export declare function userFromId<TB extends TablesMatching<{
76
76
  userId: string;
77
77
  }>>(builder: kysely.ExpressionBuilder<Schema, TB>): kysely.AliasedRawBuilder<UserInternal, 'user' | TB>;
78
+ /**
79
+ * Used for `update ... set ... from`
80
+ */
81
+ export declare function values<R extends Record<string, unknown>, A extends string>(records: R[], alias: A): kysely.AliasedRawBuilder<R, A>;
78
82
  export interface Stats {
79
83
  users: number;
80
84
  passkeys: number;
package/dist/database.js CHANGED
@@ -93,6 +93,30 @@ export function userFromId(builder) {
93
93
  .$castTo()
94
94
  .as('user');
95
95
  }
96
+ /**
97
+ * Used for `update ... set ... from`
98
+ */
99
+ export function values(records, alias) {
100
+ if (!records?.length)
101
+ throw new Error('Can not create values() with empty records array');
102
+ // Assume there's at least one record and all records
103
+ // have the same keys.
104
+ const keys = Object.keys(records[0]);
105
+ // Transform the records into a list of lists such as
106
+ // ($1, $2, $3), ($4, $5, $6)
107
+ const values = sql.join(records.map(r => sql `(${sql.join(keys.map(k => r[k]))})`));
108
+ // Create the alias `v(id, v1, v2)` that specifies the table alias
109
+ // AND a name for each column.
110
+ const wrappedAlias = sql.ref(alias);
111
+ // eslint-disable-next-line @typescript-eslint/unbound-method
112
+ const wrappedColumns = sql.join(keys.map(sql.ref));
113
+ const aliasSql = sql `${wrappedAlias}(${wrappedColumns})`;
114
+ // Finally create a single `AliasedRawBuilder` instance of the
115
+ // whole thing. Note that we need to explicitly specify
116
+ // the alias type using `.as<A>` because we are using a
117
+ // raw sql snippet as the alias.
118
+ return sql `(values ${values})`.as(aliasSql);
119
+ }
96
120
  export async function statText() {
97
121
  try {
98
122
  const stats = await count('users', 'passkeys', 'sessions');
@@ -60,7 +60,7 @@ function get_raw_body(req) {
60
60
  }
61
61
  export function convertToRequest(req) {
62
62
  const headers = req.headers;
63
- const request = new Request(config.auth.origin + req.url, {
63
+ const request = new Request(config.origin + req.url, {
64
64
  // @ts-expect-error 2353
65
65
  duplex: 'half',
66
66
  method: req.method,
@@ -1,4 +1,4 @@
1
- import type { User, UserInternal } from '@axium/core';
1
+ import { type User, type UserInternal } from '@axium/core';
2
2
  import * as z from 'zod';
3
3
  import type { ServerRoute } from './routes.js';
4
4
  /**
package/dist/requests.js CHANGED
@@ -1,7 +1,10 @@
1
+ import {} from '@axium/core';
2
+ import * as io from '@axium/core/io';
1
3
  import { userProtectedFields, userPublicFields } from '@axium/core/user';
2
4
  import * as cookie from 'cookie_v1';
3
5
  import { pick } from 'utilium';
4
6
  import * as z from 'zod';
7
+ import { audit } from './audit.js';
5
8
  import { createSession } from './auth.js';
6
9
  import { config } from './config.js';
7
10
  export function isResponseError(e) {
@@ -25,7 +28,7 @@ export function redirect(location, status = 302) {
25
28
  export function json(data, init) {
26
29
  const response = Response.json(data, init);
27
30
  if (!response.headers.has('content-length')) {
28
- response.headers.set('content-length', JSON.stringify(data).length.toString());
31
+ response.headers.set('content-length', new TextEncoder().encode(JSON.stringify(data)).length.toString());
29
32
  }
30
33
  return response;
31
34
  }
@@ -71,6 +74,10 @@ export function withError(text, code = 500) {
71
74
  return function (e) {
72
75
  if (e.name == 'ResponseError')
73
76
  throw e;
77
+ if (code == 500) {
78
+ void audit('response_error', undefined, { stack: e.stack });
79
+ io.error('(in response) ' + e.stack);
80
+ }
74
81
  error(code, text + (config.debug && e.message ? `: ${e.message}` : ''));
75
82
  };
76
83
  }
package/dist/routes.d.ts CHANGED
@@ -1,53 +1,53 @@
1
1
  import type { RequestMethod } from '@axium/core/requests';
2
2
  import type { Component } from 'svelte';
3
3
  import type z from 'zod';
4
- type _Params = Partial<Record<string, string>>;
4
+ type RouteParams = Record<string, z.ZodType>;
5
+ type ParamValues<P extends RouteParams> = {
6
+ [K in keyof P]: z.infer<P[K]>;
7
+ };
5
8
  export type MaybePromise<T> = T | Promise<T>;
6
- export type EndpointHandlers<Params extends _Params = _Params, This = unknown> = Partial<Record<RequestMethod, (this: This, request: Request, params: Params) => MaybePromise<object | Response>>>;
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
- }
9
+ export type EndpointHandlers<Params, This = unknown> = Partial<Record<RequestMethod, (this: This, request: Request, params: Params) => MaybePromise<object | Response>>>;
14
10
  /**
15
11
  * A route with server-side handlers for different HTTP methods.
16
12
  */
17
- export interface ServerRouteOptions<Params extends _Params = _Params> extends CommonRouteOptions<Params>, EndpointHandlers<Params, RouteCommon> {
13
+ export interface ServerRouteInit<Params extends RouteParams = RouteParams> extends EndpointHandlers<ParamValues<Params>, RouteCommon<Params>> {
14
+ path: string;
15
+ params?: Params;
18
16
  api?: boolean;
19
17
  }
20
- export interface WebRouteOptions extends CommonRouteOptions {
18
+ export interface WebRouteInit<Params extends RouteParams = RouteParams> {
19
+ path: string;
20
+ params?: Params;
21
21
  load?(request: Request): object | Promise<object>;
22
22
  /** the Svelte page */
23
23
  page?: Component;
24
24
  }
25
- export type RouteOptions = ServerRouteOptions | WebRouteOptions;
26
- export interface RouteCommon {
25
+ export type RouteInit<Params extends RouteParams = RouteParams> = ServerRouteInit<Params> | WebRouteInit<Params>;
26
+ export interface RouteCommon<Params extends RouteParams = RouteParams> {
27
27
  path: string;
28
- params?: Record<string, RouteParamOptions>;
28
+ params?: Params;
29
29
  }
30
- export interface ServerRoute extends RouteCommon, EndpointHandlers {
30
+ export interface ServerRoute<Params extends RouteParams = RouteParams> extends RouteCommon<Params>, EndpointHandlers<ParamValues<Params>> {
31
31
  api: boolean;
32
32
  server: true;
33
33
  }
34
- export interface WebRoute extends RouteCommon {
34
+ export interface WebRoute<Params extends RouteParams = RouteParams> extends RouteCommon<Params> {
35
35
  server: false;
36
36
  load?(request: Request): object | Promise<object>;
37
37
  page: Component;
38
38
  }
39
- export type Route = ServerRoute | WebRoute;
39
+ export type Route<Params extends RouteParams = RouteParams> = ServerRoute<Params> | WebRoute<Params>;
40
40
  /**
41
41
  * @internal
42
42
  */
43
- export declare const routes: Map<string, Route>;
43
+ export declare const routes: Map<string, Route<any>>;
44
44
  /**
45
45
  * @category Plugin API
46
46
  */
47
- export declare function addRoute(opt: RouteOptions): void;
47
+ export declare function addRoute<const P extends RouteParams = RouteParams>(opt: RouteInit<P>): void;
48
48
  /**
49
49
  * Resolve a request URL into a route.
50
50
  * This handles parsing of parameters in the URL.
51
51
  */
52
- export declare function resolveRoute(url: URL): [Route, params: object] | void;
52
+ export declare function resolveRoute<P extends RouteParams = RouteParams>(url: URL): [Route<P>, params: ParamValues<P>] | void;
53
53
  export {};
package/dist/routes.js CHANGED
@@ -27,8 +27,9 @@ export function addRoute(opt) {
27
27
  */
28
28
  export function resolveRoute(url) {
29
29
  const { pathname } = url;
30
- if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
30
+ if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':'))) {
31
31
  return [routes.get(pathname), {}];
32
+ }
32
33
  // Otherwise we must have a parameterized route
33
34
  _routes: for (const route of routes.values()) {
34
35
  const params = {};
package/dist/serve.js CHANGED
@@ -131,7 +131,7 @@ async function _runRoute(run, request, params) {
131
131
  async function _getLinkedBuildHandler(buildPath = '../build/handler.js') {
132
132
  const { handler: handleFrontendRequest } = await import(buildPath);
133
133
  return function handle(req, res) {
134
- const url = new URL(req.url, config.auth.origin);
134
+ const url = new URL(req.url, config.origin);
135
135
  const [route, params = {}] = resolveRoute(url) ?? [];
136
136
  if (!route && url.pathname === '/' && config.debug_home) {
137
137
  res.writeHead(303, { Location: '/_axium/default' }).end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/server",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "funding": {
6
6
  "type": "individual",
@@ -40,4 +40,9 @@
40
40
 
41
41
  <h4>Extra Data</h4>
42
42
 
43
- <pre>{JSON.stringify(event.extra, null, 4)}</pre>
43
+ {#if event.name == 'response_error'}
44
+ <h5>Error Stack</h5>
45
+ <pre>{event.extra.stack}</pre>
46
+ {:else}
47
+ <pre>{JSON.stringify(event.extra, null, 4)}</pre>
48
+ {/if}
@@ -15,7 +15,11 @@
15
15
  <p><strong>Author:</strong> {plugin.author}</p>
16
16
  <p>
17
17
  <strong>Provided apps:</strong>
18
- {#if plugin.apps?.length}{plugin.apps?.map(a => a.name).join(', ')}{:else}<i>None</i>{/if}
18
+ {#if plugin.apps?.length}
19
+ {#each plugin.apps as app, i}
20
+ <a href="/{app.id}">{app.name}</a>{i != plugin.apps.length - 1 ? ', ' : ''}
21
+ {/each}
22
+ {:else}<i>None</i>{/if}
19
23
  </p>
20
24
  <p>{plugin.description}</p>
21
25
  {:else}
package/svelte.config.js CHANGED
@@ -10,6 +10,9 @@ export default {
10
10
  warningFilter(w) {
11
11
  if (w.code.startsWith('a11y')) return false;
12
12
  },
13
+ experimental: {
14
+ async: true,
15
+ },
13
16
  },
14
17
  kit: {
15
18
  adapter: node(),