@axium/server 0.26.3 → 0.28.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,60 +1,41 @@
1
- import type { AccessControl, Permission, UserInternal } from '@axium/core';
2
- import type { AliasedRawBuilder, Expression, ExpressionBuilder, Selectable } from 'kysely';
1
+ import type { AccessControl, AccessMap, UserInternal } from '@axium/core';
2
+ import type * as kysely from 'kysely';
3
+ import type { WithRequired } from 'utilium';
3
4
  import * as db from './database.js';
4
- export interface Target {
5
- userId: string;
6
- publicPermission: Permission;
7
- }
8
- type _TableNames = (string & keyof db.Schema) & keyof {
9
- [K in Exclude<keyof db.Schema, `acl.${string}`> as Selectable<db.Schema[K]> extends Omit<Target, 'acl'> ? K : never]: null;
5
+ type _TableNames = keyof {
6
+ [K in keyof db.Schema as db.Schema[K] extends db.DBAccessControl ? K : never]: null;
7
+ };
8
+ type _TargetNames = keyof db.Schema & keyof {
9
+ [K in _TableNames as K extends `acl.${infer TB extends keyof db.Schema}` ? TB : never]: null;
10
10
  };
11
11
  /**
12
12
  * `never` causes a ton of problems, so we use `string` if none of the tables are shareable.
13
13
  */
14
- export type TargetName = _TableNames extends never ? keyof db.Schema : _TableNames;
14
+ export type TableName = _TableNames extends never ? keyof db.Schema : _TableNames;
15
+ export type TargetName = _TargetNames extends never ? keyof db.Schema : _TargetNames;
15
16
  export interface AccessControlInternal extends AccessControl {
16
- user?: UserInternal;
17
+ tag?: string | null;
17
18
  }
18
- export declare const expectedTypes: {
19
- userId: {
20
- type: string;
21
- required: true;
22
- };
23
- createdAt: {
24
- type: string;
25
- required: true;
26
- hasDefault: true;
27
- };
28
- itemId: {
29
- type: string;
30
- required: true;
31
- };
32
- permission: {
33
- type: string;
34
- required: true;
35
- hasDefault: true;
36
- };
37
- };
38
- /**
39
- * Adds an Access Control List (ACL) in the database for managing access to rows in an existing table.
40
- * @category Plugin API
41
- */
42
- export declare function createTable(table: TargetName): Promise<void>;
43
- export declare function dropTable(table: TargetName): Promise<void>;
44
- export declare function wipeTable(table: TargetName): Promise<void>;
45
- export declare function createEntry(itemType: TargetName, data: Omit<AccessControl, 'createdAt'>): Promise<AccessControlInternal>;
46
- export declare function deleteEntry(itemType: TargetName, itemId: string, userId: string): Promise<void>;
19
+ export type PermissionsFor<TB extends TableName> = Omit<kysely.Selectable<db.Schema[TB]>, keyof AccessControlInternal | number | symbol>;
20
+ export type Result<TB extends TableName> = AccessControlInternal & PermissionsFor<TB>;
21
+ export type WithACL<TB extends TargetName> = kysely.Selectable<db.Schema[TB]> & {
22
+ userId: string;
23
+ acl: Result<`acl.${TB}`>[];
24
+ } & Record<string, any>;
47
25
  export interface ACLSelectionOptions {
48
- /** If specified, only returns the access control for the given user ID. */
49
- onlyId?: string | Expression<any>;
26
+ /** If specified, files by user UUID */
27
+ user?: Pick<UserInternal, 'id' | 'roles' | 'tags'>;
50
28
  /** Instead of using the `id` from `table`, use the `id` from this instead */
51
29
  alias?: string;
52
30
  }
53
31
  /**
54
32
  * Helper to select all access controls for a given table, including the user information.
55
- *
56
- * @param onlyId If specified, only returns the access control for the given user ID.
33
+ * Optionally filter for the entries applicable to a specific user.
34
+ * This includes entries matching the user's ID, roles, or tags along with the "public" entry where all three "target" columns are null.
57
35
  */
58
- export declare function from(table: TargetName, opt?: ACLSelectionOptions): (eb: ExpressionBuilder<db.Schema, any>) => AliasedRawBuilder<Required<AccessControl>[], 'acl'>;
59
- export declare function get(itemType: TargetName, itemId: string): Promise<Required<AccessControlInternal>[]>;
36
+ export declare function from<const TB extends TargetName>(table: TB, opt?: ACLSelectionOptions): (eb: kysely.ExpressionBuilder<db.Schema, any>) => kysely.AliasedRawBuilder<Result<`acl.${TB}`>[], 'acl'>;
37
+ export declare function get<const TB extends TableName>(table: TB, itemId: string): Promise<WithRequired<AccessControlInternal & kysely.Selectable<db.Schema[TB]>, 'user'>[]>;
38
+ export declare function set<const TB extends TableName>(table: TB, itemId: string, data: AccessMap): Promise<(AccessControlInternal & kysely.Selectable<db.Schema[TB]>)[]>;
39
+ export declare function check<const TB extends TableName>(acl: Result<TB>[], permissions: Partial<PermissionsFor<TB>>): Set<keyof PermissionsFor<TB>>;
40
+ export declare function listTables(): Record<string, TableName>;
60
41
  export {};
package/dist/acl.js CHANGED
@@ -1,66 +1,64 @@
1
- import * as io from '@axium/core/node/io';
2
- import { sql } from 'kysely';
3
1
  import { jsonArrayFrom } from 'kysely/helpers/postgres';
4
2
  import * as db from './database.js';
5
- const accessControllableTypes = {
6
- userId: { type: 'uuid', required: true },
7
- publicPermission: { type: 'int4', required: true, hasDefault: true },
8
- };
9
- export const expectedTypes = {
10
- userId: { type: 'uuid', required: true },
11
- createdAt: { type: 'timestamptz', required: true, hasDefault: true },
12
- itemId: { type: 'uuid', required: true },
13
- permission: { type: 'int4', required: true, hasDefault: true },
14
- };
15
- /**
16
- * Adds an Access Control List (ACL) in the database for managing access to rows in an existing table.
17
- * @category Plugin API
18
- */
19
- export async function createTable(table) {
20
- await db.checkTableTypes(table, accessControllableTypes, { strict: true, extra: false });
21
- io.start(`Creating table acl.${table}`);
22
- await db.database.schema
23
- .createTable(`acl.${table}`)
24
- .addColumn('userId', 'uuid', col => col.references('users.id').onDelete('cascade'))
25
- .addColumn('itemId', 'uuid', col => col.references(`${table}.id`).onDelete('cascade'))
26
- .addPrimaryKeyConstraint('PK_acl_' + table, ['userId', 'itemId'])
27
- .addColumn('createdAt', 'timestamptz', col => col.notNull().defaultTo(sql `now()`))
28
- .addColumn('permission', 'integer', col => col.notNull().check(sql `permission >= 0 AND permission <= 5`))
29
- .execute()
30
- .then(io.done)
31
- .catch(db.warnExists);
32
- await db.createIndex(`acl.${table}`, 'userId');
33
- await db.createIndex(`acl.${table}`, 'itemId');
34
- }
35
- export async function dropTable(table) {
36
- io.start(`Dropping table acl.${table}`);
37
- await db.database.schema.dropTable(`acl.${table}`).execute().then(io.done).catch(db.warnExists);
38
- }
39
- export async function wipeTable(table) {
40
- io.start(`Wiping table acl.${table}`);
41
- await db.database.deleteFrom(`acl.${table}`).execute().then(io.done).catch(db.warnExists);
42
- }
43
- export async function createEntry(itemType, data) {
44
- return await db.database.insertInto(`acl.${itemType}`).values(data).returningAll().executeTakeFirstOrThrow();
45
- }
46
- export async function deleteEntry(itemType, itemId, userId) {
47
- await db.database.deleteFrom(`acl.${itemType}`).where('itemId', '=', itemId).where('userId', '=', userId).execute();
48
- }
49
3
  /**
50
4
  * Helper to select all access controls for a given table, including the user information.
51
- *
52
- * @param onlyId If specified, only returns the access control for the given user ID.
5
+ * Optionally filter for the entries applicable to a specific user.
6
+ * This includes entries matching the user's ID, roles, or tags along with the "public" entry where all three "target" columns are null.
53
7
  */
54
8
  export function from(table, opt = {}) {
55
9
  return (eb) => jsonArrayFrom(eb
56
10
  .selectFrom(`acl.${table} as _acl`)
57
11
  .selectAll()
58
- .select(db.userFromId)
59
12
  .whereRef(`_acl.itemId`, '=', `${opt.alias || table}.id`)
60
- .$if(!!opt.onlyId, qb => qb.where('userId', '=', opt.onlyId)))
13
+ .where(eb => {
14
+ const allNull = eb.and([eb('userId', 'is', null), eb('role', 'is', null), eb('tag', 'is', null)]);
15
+ if (!opt.user)
16
+ return allNull;
17
+ const ors = [allNull, eb('userId', '=', opt.user.id)];
18
+ if (opt.user.roles.length)
19
+ ors.push(eb('role', 'in', opt.user.roles));
20
+ if (opt.user.tags.length)
21
+ ors.push(eb('tag', 'in', opt.user.tags));
22
+ return eb.or(ors);
23
+ }))
61
24
  .$castTo()
62
25
  .as('acl');
63
26
  }
64
- export async function get(itemType, itemId) {
65
- return await db.database.selectFrom(`acl.${itemType}`).where('itemId', '=', itemId).selectAll().select(db.userFromId).execute();
27
+ export async function get(table, itemId) {
28
+ // @ts-expect-error 2349
29
+ return await db.database.selectFrom(table).where('itemId', '=', itemId).selectAll().select(db.userFromId).execute();
30
+ }
31
+ export async function set(table, itemId, data) {
32
+ const entries = Object.entries(data).map(([userId, perm]) => ({ userId, ...perm }));
33
+ if (!entries.length)
34
+ return [];
35
+ return await db.database
36
+ .updateTable(table)
37
+ // @ts-expect-error 2349
38
+ .from(db.values(entries, 'data'))
39
+ .set()
40
+ .whereRef(`${table}.userId`, '=', 'data.userId')
41
+ .where('itemId', '=', itemId)
42
+ .returningAll()
43
+ .execute();
44
+ }
45
+ export function check(acl, permissions) {
46
+ const allowed = new Set();
47
+ const all = new Set(Object.keys(permissions));
48
+ const entries = Object.entries(permissions);
49
+ for (const control of acl) {
50
+ for (const [key, needed] of entries) {
51
+ const value = control[key];
52
+ if (value === needed)
53
+ allowed.add(key);
54
+ }
55
+ }
56
+ return all.difference(allowed);
57
+ }
58
+ export function listTables() {
59
+ const tables = {};
60
+ for (const [, file] of db.getSchemaFiles()) {
61
+ Object.assign(tables, file.acl_tables || {});
62
+ }
63
+ return tables;
66
64
  }
package/dist/api/acl.js CHANGED
@@ -1,19 +1,27 @@
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
- import { parseBody, withError } from '../requests.js';
4
+ import { error, parseBody, withError } from '../requests.js';
5
5
  import { addRoute } from '../routes.js';
6
+ import { checkAuthForItem } from '../auth.js';
6
7
  addRoute({
7
8
  path: '/api/acl/:itemType/:itemId',
8
9
  params: {
9
10
  itemType: z.string(),
10
11
  itemId: z.uuid(),
11
12
  },
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;
13
+ async GET(request, { itemType, itemId }) {
14
+ const tables = acl.listTables();
15
+ if (!(itemType in tables))
16
+ error(400, 'Invalid item type: ' + itemType);
17
+ return await acl.get(tables[itemType], itemId).catch(withError('Failed to get access controls'));
18
+ },
19
+ async POST(request, { itemType, itemId }) {
20
+ const data = await parseBody(request, AccessMap);
21
+ const tables = acl.listTables();
22
+ if (!(itemType in tables))
23
+ error(400, 'Invalid item type: ' + itemType);
24
+ await checkAuthForItem(request, itemType, itemId, { manage: true });
25
+ return await acl.set(tables[itemType], itemId, data);
18
26
  },
19
27
  });
package/dist/api/admin.js CHANGED
@@ -5,16 +5,13 @@ import { omit } from 'utilium';
5
5
  import * as z from 'zod';
6
6
  import pkg from '../../package.json' with { type: 'json' };
7
7
  import { audit, events, getEvents } from '../audit.js';
8
- import { getSessionAndUser } from '../auth.js';
8
+ import { requireSession } from '../auth.js';
9
9
  import { config } from '../config.js';
10
10
  import { count, database as db } from '../database.js';
11
- import { error, getToken, withError } from '../requests.js';
11
+ import { error, withError } from '../requests.js';
12
12
  import { addRoute } from '../routes.js';
13
13
  async function assertAdmin(route, req) {
14
- const token = getToken(req);
15
- if (!token)
16
- error(401, 'Missing token');
17
- const admin = await getSessionAndUser(token).catch(withError('Invalid session', 400));
14
+ const admin = await requireSession(req);
18
15
  if (!admin.user.isAdmin)
19
16
  error(403, 'Not an administrator');
20
17
  if (!config.admin_api)
@@ -55,9 +52,9 @@ addRoute({
55
52
  addRoute({
56
53
  path: '/api/admin/users/:userId',
57
54
  params: { userId: z.uuid() },
58
- async GET(req, params) {
55
+ async GET(req, { userId }) {
59
56
  await assertAdmin(this, req);
60
- if (!params.userId)
57
+ if (!userId)
61
58
  error(400, 'Missing user ID');
62
59
  const user = await db
63
60
  .selectFrom('users')
@@ -65,7 +62,7 @@ addRoute({
65
62
  .select(eb => jsonArrayFrom(eb.selectFrom('sessions').whereRef('sessions.userId', '=', 'users.id').selectAll())
66
63
  .$castTo()
67
64
  .as('sessions'))
68
- .where('id', '=', params.userId)
65
+ .where('id', '=', userId)
69
66
  .executeTakeFirstOrThrow()
70
67
  .catch(withError('User not found', 404));
71
68
  return {
@@ -134,9 +131,9 @@ addRoute({
134
131
  addRoute({
135
132
  path: '/api/admin/audit/:eventId',
136
133
  params: { eventId: z.uuid() },
137
- async GET(req, params) {
134
+ async GET(req, { eventId }) {
138
135
  await assertAdmin(this, req);
139
- if (!params.eventId)
136
+ if (!eventId)
140
137
  error(400, 'Missing event ID');
141
138
  const event = await db
142
139
  .selectFrom('audit_log')
@@ -144,7 +141,7 @@ addRoute({
144
141
  .select(eb => jsonObjectFrom(eb.selectFrom('users').whereRef('users.id', '=', 'audit_log.userId').selectAll())
145
142
  .$castTo()
146
143
  .as('user'))
147
- .where('id', '=', params.eventId)
144
+ .where('id', '=', eventId)
148
145
  .executeTakeFirstOrThrow()
149
146
  .catch(withError('Audit event not found', 404));
150
147
  return event;
@@ -2,24 +2,17 @@ import { apps } from '@axium/core';
2
2
  import { plugins } from '@axium/core/plugins';
3
3
  import { requestMethods } from '@axium/core/requests';
4
4
  import pkg from '../../package.json' with { type: 'json' };
5
- import { getSessionAndUser } from '../auth.js';
5
+ import { requireSession } from '../auth.js';
6
6
  import { config } from '../config.js';
7
- import { error, getToken } from '../requests.js';
7
+ import { error } from '../requests.js';
8
8
  import { addRoute, routes } from '../routes.js';
9
9
  addRoute({
10
10
  path: '/api/metadata',
11
11
  async GET(request) {
12
12
  if (!config.debug) {
13
- const token = getToken(request);
14
- if (!token)
15
- error(401, 'Missing session token');
16
- const session = await getSessionAndUser(token);
17
- if (!session)
18
- error(401, 'Invalid session');
19
- if (!session.user.isAdmin)
13
+ const { user } = await requireSession(request);
14
+ if (!user.isAdmin)
20
15
  error(403, 'User is not an administrator');
21
- if (session.user.isSuspended)
22
- error(403, 'User is suspended');
23
16
  }
24
17
  return {
25
18
  version: pkg.version,
@@ -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
@@ -15,9 +15,6 @@ export interface $EventTypes {
15
15
  logout: {
16
16
  sessions: string[];
17
17
  };
18
- acl_id_mismatch: {
19
- item: string;
20
- };
21
18
  admin_change: {
22
19
  user: string;
23
20
  };
@@ -25,6 +22,9 @@ export interface $EventTypes {
25
22
  route: string;
26
23
  session: string;
27
24
  };
25
+ response_error: {
26
+ stack?: string;
27
+ };
28
28
  }
29
29
  export type EventName = keyof $EventTypes;
30
30
  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)
@@ -105,14 +105,6 @@ addEvent({
105
105
  tags: ['cli'],
106
106
  extra: { user: z.string() },
107
107
  });
108
- addEvent({
109
- source: '@axium/server',
110
- name: 'acl_id_mismatch',
111
- severity: Severity.Critical,
112
- tags: ['acl', 'auth'],
113
- extra: { item: z.string() },
114
- noAutoSuspend: true,
115
- });
116
108
  addEvent({
117
109
  source: '@axium/server',
118
110
  name: 'admin_api',
@@ -120,3 +112,10 @@ addEvent({
120
112
  tags: ['auth'],
121
113
  extra: { route: z.string(), session: z.string() },
122
114
  });
115
+ addEvent({
116
+ source: '@axium/server',
117
+ name: 'response_error',
118
+ severity: Severity.Error,
119
+ tags: [],
120
+ extra: { stack: z.string().optional() },
121
+ });
package/dist/auth.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { Passkey, Permission, Session, UserInternal, Verification } from '@axium/core';
2
- import type { Insertable } from 'kysely';
1
+ import type { Passkey, Session, UserInternal, Verification } from '@axium/core';
2
+ import type { Insertable, Selectable } from 'kysely';
3
3
  import * as acl from './acl.js';
4
4
  import { type Schema } from './database.js';
5
5
  export declare function getUser(id: string): Promise<UserInternal>;
@@ -13,6 +13,7 @@ export interface SessionAndUser extends SessionInternal {
13
13
  }
14
14
  export declare function getSessionAndUser(token: string): Promise<SessionAndUser>;
15
15
  export declare function getSession(sessionId: string): Promise<SessionInternal>;
16
+ export declare function requireSession(request: Request, sensitive?: boolean): Promise<SessionAndUser>;
16
17
  export declare function getSessions(userId: string): Promise<SessionInternal[]>;
17
18
  export type VerificationRole = 'verify_email' | 'login';
18
19
  export interface VerificationInternal extends Verification {
@@ -38,10 +39,14 @@ export interface UserAuthResult extends SessionAndUser {
38
39
  accessor: UserInternal;
39
40
  }
40
41
  export declare function checkAuthForUser(request: Request, userId: string, sensitive?: boolean): Promise<UserAuthResult>;
41
- export interface ItemAuthResult<T extends acl.Target> {
42
+ export interface ItemAuthResult<TB extends acl.TargetName> {
42
43
  fromACL: boolean;
43
- item: T;
44
+ item: Selectable<Schema[TB]>;
44
45
  user?: UserInternal;
45
46
  session?: SessionInternal;
46
47
  }
47
- export declare function checkAuthForItem<const V extends acl.Target>(request: Request, itemType: acl.TargetName, itemId: string, permission: Permission): Promise<ItemAuthResult<V>>;
48
+ /**
49
+ * Authenticate a request against an "item" which has an ACL table.
50
+ * This will fetch the item, ACLs, users, and the authenticating session.
51
+ */
52
+ export declare function checkAuthForItem<const TB extends acl.TargetName>(request: Request, itemType: TB, itemId: string, permissions: Partial<acl.PermissionsFor<`acl.${TB}`>>): Promise<ItemAuthResult<TB>>;