@axium/server 0.33.3 → 0.34.1
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 +6 -4
- package/dist/acl.js +34 -14
- package/dist/api/acl.js +25 -11
- package/dist/api/users.js +24 -1
- package/dist/audit.js +4 -2
- package/dist/config.d.ts +6 -4
- package/dist/config.js +8 -5
- package/package.json +3 -3
- package/schemas/config.json +11 -18
package/dist/acl.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AccessControl, type AccessTarget, type UserInternal } from '@axium/core';
|
|
2
2
|
import type * as kysely from 'kysely';
|
|
3
3
|
import type { WithRequired } from 'utilium';
|
|
4
4
|
import * as db from './database.js';
|
|
@@ -23,7 +23,7 @@ export type WithACL<TB extends TargetName> = kysely.Selectable<db.Schema[TB]> &
|
|
|
23
23
|
acl: Result<`acl.${TB}`>[];
|
|
24
24
|
};
|
|
25
25
|
export interface ACLSelectionOptions {
|
|
26
|
-
/** If specified,
|
|
26
|
+
/** If specified, filters by user UUID */
|
|
27
27
|
user?: Pick<UserInternal, 'id' | 'roles' | 'tags'>;
|
|
28
28
|
/** Instead of using the `id` from `table`, use the `id` from this instead */
|
|
29
29
|
alias?: string;
|
|
@@ -34,8 +34,10 @@ export interface ACLSelectionOptions {
|
|
|
34
34
|
* This includes entries matching the user's ID, roles, or tags along with the "public" entry where all three "target" columns are null.
|
|
35
35
|
*/
|
|
36
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<
|
|
38
|
-
export declare function
|
|
37
|
+
export declare function get<const TB extends TableName>(table: TB, itemId: string): Promise<WithRequired<Result<TB>, 'user'>[]>;
|
|
38
|
+
export declare function update<const TB extends TableName>(table: TB, itemId: string, target: AccessTarget, permissions: PermissionsFor<TB>): Promise<Result<TB>>;
|
|
39
|
+
export declare function remove<const TB extends TableName>(table: TB, itemId: string, target: AccessTarget): Promise<Result<TB>>;
|
|
40
|
+
export declare function add<const TB extends TableName>(table: TB, itemId: string, target: AccessTarget): Promise<Result<TB>>;
|
|
39
41
|
export declare function check<const TB extends TableName>(acl: Result<TB>[], permissions: Partial<PermissionsFor<TB>>): Set<keyof PermissionsFor<TB>>;
|
|
40
42
|
export declare function listTables(): Record<string, TableName>;
|
|
41
43
|
export {};
|
package/dist/acl.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromTarget } from '@axium/core';
|
|
1
2
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
2
3
|
import * as db from './database.js';
|
|
3
4
|
/**
|
|
@@ -9,8 +10,9 @@ export function from(table, opt = {}) {
|
|
|
9
10
|
return (eb) => jsonArrayFrom(eb
|
|
10
11
|
.selectFrom(`acl.${table} as _acl`)
|
|
11
12
|
.selectAll()
|
|
12
|
-
|
|
13
|
-
.
|
|
13
|
+
.$if(!opt.user, qb => qb.select(db.userFromId))
|
|
14
|
+
.whereRef('_acl.itemId', '=', `${opt.alias || table}.id`)
|
|
15
|
+
.$if(!!opt.user, qb => qb.where(eb => {
|
|
14
16
|
const allNull = eb.and([eb('userId', 'is', null), eb('role', 'is', null), eb('tag', 'is', null)]);
|
|
15
17
|
if (!opt.user)
|
|
16
18
|
return allNull;
|
|
@@ -20,27 +22,45 @@ export function from(table, opt = {}) {
|
|
|
20
22
|
if (opt.user.tags.length)
|
|
21
23
|
ors.push(eb('tag', 'in', opt.user.tags));
|
|
22
24
|
return eb.or(ors);
|
|
23
|
-
}))
|
|
25
|
+
})))
|
|
24
26
|
.$castTo()
|
|
25
27
|
.as('acl');
|
|
26
28
|
}
|
|
27
29
|
export async function get(table, itemId) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
return await db.database
|
|
31
|
+
.selectFrom(table)
|
|
32
|
+
.where('itemId', '=', itemId)
|
|
33
|
+
.selectAll()
|
|
34
|
+
.select(db.userFromId)
|
|
35
|
+
.$castTo()
|
|
36
|
+
.execute();
|
|
30
37
|
}
|
|
31
|
-
export async function
|
|
32
|
-
const entries = Object.entries(data).map(([userId, perm]) => ({ userId, ...perm }));
|
|
33
|
-
if (!entries.length)
|
|
34
|
-
return [];
|
|
38
|
+
export async function update(table, itemId, target, permissions) {
|
|
35
39
|
return await db.database
|
|
36
40
|
.updateTable(table)
|
|
37
|
-
|
|
38
|
-
.from(db.values(entries, 'data'))
|
|
39
|
-
.set()
|
|
40
|
-
.whereRef(`${table}.userId`, '=', 'data.userId')
|
|
41
|
+
.set(permissions)
|
|
41
42
|
.where('itemId', '=', itemId)
|
|
43
|
+
.where(eb => eb.and(fromTarget(target)))
|
|
42
44
|
.returningAll()
|
|
43
|
-
|
|
45
|
+
.$castTo()
|
|
46
|
+
.executeTakeFirstOrThrow();
|
|
47
|
+
}
|
|
48
|
+
export async function remove(table, itemId, target) {
|
|
49
|
+
return await db.database
|
|
50
|
+
.deleteFrom(table)
|
|
51
|
+
.where('itemId', '=', itemId)
|
|
52
|
+
.where(eb => eb.and(fromTarget(target)))
|
|
53
|
+
.returningAll()
|
|
54
|
+
.$castTo()
|
|
55
|
+
.executeTakeFirstOrThrow();
|
|
56
|
+
}
|
|
57
|
+
export async function add(table, itemId, target) {
|
|
58
|
+
return await db.database
|
|
59
|
+
.insertInto(table)
|
|
60
|
+
.values({ itemId, ...fromTarget(target) })
|
|
61
|
+
.returningAll()
|
|
62
|
+
.$castTo()
|
|
63
|
+
.executeTakeFirstOrThrow();
|
|
44
64
|
}
|
|
45
65
|
export function check(acl, permissions) {
|
|
46
66
|
const allowed = new Set();
|
package/dist/api/acl.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { AccessMap } from '@axium/core/access';
|
|
2
1
|
import * as z from 'zod';
|
|
3
2
|
import * as acl from '../acl.js';
|
|
4
3
|
import { error, parseBody, withError } from '../requests.js';
|
|
5
4
|
import { addRoute } from '../routes.js';
|
|
6
5
|
import { checkAuthForItem } from '../auth.js';
|
|
6
|
+
import { AccessControlUpdate, AccessTarget } from '@axium/core';
|
|
7
|
+
function getTable(itemType) {
|
|
8
|
+
const tables = acl.listTables();
|
|
9
|
+
if (!(itemType in tables))
|
|
10
|
+
error(400, 'Invalid item type: ' + itemType);
|
|
11
|
+
return tables[itemType];
|
|
12
|
+
}
|
|
7
13
|
addRoute({
|
|
8
14
|
path: '/api/acl/:itemType/:itemId',
|
|
9
15
|
params: {
|
|
@@ -11,17 +17,25 @@ addRoute({
|
|
|
11
17
|
itemId: z.uuid(),
|
|
12
18
|
},
|
|
13
19
|
async GET(request, { itemType, itemId }) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
error(400, 'Invalid item type: ' + itemType);
|
|
17
|
-
return await acl.get(tables[itemType], itemId).catch(withError('Failed to get access controls'));
|
|
20
|
+
const table = getTable(itemType);
|
|
21
|
+
return await acl.get(table, itemId).catch(withError('Failed to get access controls'));
|
|
18
22
|
},
|
|
19
|
-
async
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
if (!(itemType in tables))
|
|
23
|
-
error(400, 'Invalid item type: ' + itemType);
|
|
23
|
+
async PATCH(request, { itemType, itemId }) {
|
|
24
|
+
const table = getTable(itemType);
|
|
25
|
+
const { target, permissions } = await parseBody(request, AccessControlUpdate);
|
|
24
26
|
await checkAuthForItem(request, itemType, itemId, { manage: true });
|
|
25
|
-
return await acl.
|
|
27
|
+
return await acl.update(table, itemId, target, permissions);
|
|
28
|
+
},
|
|
29
|
+
async PUT(request, { itemType, itemId }) {
|
|
30
|
+
const table = getTable(itemType);
|
|
31
|
+
const target = await parseBody(request, AccessTarget);
|
|
32
|
+
await checkAuthForItem(request, itemType, itemId, { manage: true });
|
|
33
|
+
return await acl.add(table, itemId, target);
|
|
34
|
+
},
|
|
35
|
+
async DELETE(request, { itemType, itemId }) {
|
|
36
|
+
const table = getTable(itemType);
|
|
37
|
+
const target = await parseBody(request, AccessTarget);
|
|
38
|
+
await checkAuthForItem(request, itemType, itemId, { manage: true });
|
|
39
|
+
return await acl.remove(table, itemId, target);
|
|
26
40
|
},
|
|
27
41
|
});
|
package/dist/api/users.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as webauthn from '@simplewebauthn/server';
|
|
|
4
4
|
import { encodeUUID, omit, pick } from 'utilium';
|
|
5
5
|
import * as z from 'zod';
|
|
6
6
|
import { audit } from '../audit.js';
|
|
7
|
-
import { checkAuthForUser, createPasskey, createVerification, getPasskey, getPasskeysByUserId, getSessions, getUser, useVerification, } from '../auth.js';
|
|
7
|
+
import { checkAuthForUser, createPasskey, createVerification, getPasskey, getPasskeysByUserId, getSessions, getUser, requireSession, useVerification, } from '../auth.js';
|
|
8
8
|
import { config } from '../config.js';
|
|
9
9
|
import { database as db } from '../database.js';
|
|
10
10
|
import { createSessionData, error, parseBody, stripUser, withError } from '../requests.js';
|
|
@@ -27,6 +27,29 @@ addRoute({
|
|
|
27
27
|
return { id };
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
|
+
addRoute({
|
|
31
|
+
path: '/api/users/discover',
|
|
32
|
+
async POST(request) {
|
|
33
|
+
const input = await parseBody(request, z.string());
|
|
34
|
+
if (config.user_discovery === 'disabled')
|
|
35
|
+
error(503, 'User discovery is disabled');
|
|
36
|
+
const { user } = (await requireSession(request).catch(() => null)) ?? {};
|
|
37
|
+
if (config.user_discovery != 'public' && !user)
|
|
38
|
+
error(401, 'User discovery restricted');
|
|
39
|
+
if (config.user_discovery == 'admin' && !user?.isAdmin)
|
|
40
|
+
error(403, 'User discovery is restricted to administrators');
|
|
41
|
+
const search = `%${input.trim().replaceAll(/[\\%_]/g, '\\$&')}%`;
|
|
42
|
+
const results = await db
|
|
43
|
+
.selectFrom('users')
|
|
44
|
+
.selectAll()
|
|
45
|
+
.limit(10)
|
|
46
|
+
.$if(!!user, qb => qb.where('id', '!=', user.id))
|
|
47
|
+
// @todo make `%...%` more robust against DoS? Consider using vectors or fancy indexing.
|
|
48
|
+
.where(eb => eb.or([eb('email', 'ilike', search), eb('name', 'ilike', search)]))
|
|
49
|
+
.execute();
|
|
50
|
+
return results.map(u => stripUser(u));
|
|
51
|
+
},
|
|
52
|
+
});
|
|
30
53
|
addRoute({
|
|
31
54
|
path: '/api/users/:id',
|
|
32
55
|
params,
|
package/dist/audit.js
CHANGED
|
@@ -20,8 +20,6 @@ export function styleSeverity(sev, align = false) {
|
|
|
20
20
|
return styleText(severityFormat[sev], text.toUpperCase());
|
|
21
21
|
}
|
|
22
22
|
function output(event) {
|
|
23
|
-
if (event.severity > Severity[capitalize(config.audit.min_severity)])
|
|
24
|
-
return;
|
|
25
23
|
console.error('[audit]', styleText('dim', io.prettyDate(event.timestamp)), styleSeverity(event.severity), event.name);
|
|
26
24
|
}
|
|
27
25
|
export async function audit_raw(event) {
|
|
@@ -29,6 +27,8 @@ export async function audit_raw(event) {
|
|
|
29
27
|
io.warn('[audit] Ignoring raw event (disabled)');
|
|
30
28
|
return;
|
|
31
29
|
}
|
|
30
|
+
if (event.severity > Severity[capitalize(config.audit.min_severity)])
|
|
31
|
+
return;
|
|
32
32
|
const result = await database.insertInto('audit_log').values(event).returningAll().executeTakeFirstOrThrow();
|
|
33
33
|
output(result);
|
|
34
34
|
}
|
|
@@ -48,6 +48,8 @@ export async function audit(eventName, userId, extra) {
|
|
|
48
48
|
io.warn('Ignoring audit event with unknown event name: ' + eventName);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
+
if (cfg.severity > Severity[capitalize(config.audit.min_severity)])
|
|
52
|
+
return;
|
|
51
53
|
try {
|
|
52
54
|
if (cfg.extra)
|
|
53
55
|
extra = cfg.extra.parse(extra);
|
package/dist/config.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export declare const Config: z.ZodObject<{
|
|
|
9
9
|
audit: z.ZodOptional<z.ZodObject<{
|
|
10
10
|
allow_raw: z.ZodOptional<z.ZodBoolean>;
|
|
11
11
|
retention: z.ZodOptional<z.ZodNumber>;
|
|
12
|
-
min_severity: z.ZodOptional<z.ZodLiteral<"
|
|
13
|
-
auto_suspend: z.ZodOptional<z.ZodLiteral<"
|
|
12
|
+
min_severity: z.ZodOptional<z.ZodLiteral<"emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug">>;
|
|
13
|
+
auto_suspend: z.ZodOptional<z.ZodLiteral<"emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug">>;
|
|
14
14
|
}, z.core.$loose>>;
|
|
15
15
|
auth: z.ZodOptional<z.ZodObject<{
|
|
16
16
|
passkey_probation: z.ZodOptional<z.ZodNumber>;
|
|
@@ -41,6 +41,7 @@ export declare const Config: z.ZodObject<{
|
|
|
41
41
|
origin: z.ZodOptional<z.ZodString>;
|
|
42
42
|
request_size_limit: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
43
43
|
show_duplicate_state: z.ZodOptional<z.ZodBoolean>;
|
|
44
|
+
user_discovery: z.ZodOptional<z.ZodLiteral<"disabled" | "user" | "admin" | "public">>;
|
|
44
45
|
verifications: z.ZodOptional<z.ZodObject<{
|
|
45
46
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
46
47
|
email: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -85,8 +86,8 @@ export declare const ConfigFile: z.ZodObject<{
|
|
|
85
86
|
audit: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
86
87
|
allow_raw: z.ZodOptional<z.ZodBoolean>;
|
|
87
88
|
retention: z.ZodOptional<z.ZodNumber>;
|
|
88
|
-
min_severity: z.ZodOptional<z.ZodLiteral<"
|
|
89
|
-
auto_suspend: z.ZodOptional<z.ZodLiteral<"
|
|
89
|
+
min_severity: z.ZodOptional<z.ZodLiteral<"emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug">>;
|
|
90
|
+
auto_suspend: z.ZodOptional<z.ZodLiteral<"emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug">>;
|
|
90
91
|
}, z.core.$loose>>>;
|
|
91
92
|
auth: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
92
93
|
passkey_probation: z.ZodOptional<z.ZodNumber>;
|
|
@@ -117,6 +118,7 @@ export declare const ConfigFile: z.ZodObject<{
|
|
|
117
118
|
origin: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
118
119
|
request_size_limit: z.ZodOptional<z.ZodOptional<z.ZodOptional<z.ZodNumber>>>;
|
|
119
120
|
show_duplicate_state: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
|
|
121
|
+
user_discovery: z.ZodOptional<z.ZodOptional<z.ZodLiteral<"disabled" | "user" | "admin" | "public">>>;
|
|
120
122
|
verifications: z.ZodOptional<z.ZodOptional<z.ZodObject<{
|
|
121
123
|
timeout: z.ZodOptional<z.ZodNumber>;
|
|
122
124
|
email: z.ZodOptional<z.ZodBoolean>;
|
package/dist/config.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
+
import { serverConfigs, toBaseName } from '@axium/core';
|
|
1
2
|
import * as io from '@axium/core/node/io';
|
|
2
3
|
import { loadPlugin } from '@axium/core/node/plugins';
|
|
3
4
|
import { levelText } from 'logzen';
|
|
4
5
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { dirname, join, resolve } from 'node:path/posix';
|
|
6
|
-
import {
|
|
7
|
+
import { deepAssign, omit } from 'utilium';
|
|
7
8
|
import * as z from 'zod';
|
|
8
9
|
import { dirs, logger, systemDir } from './io.js';
|
|
9
10
|
import { _duplicateStateWarnings, _unique } from './state.js';
|
|
10
|
-
import { serverConfigs, toBaseName } from '@axium/core';
|
|
11
11
|
const audit_severity_levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
|
|
12
|
-
const z_audit_severity = z.literal([...audit_severity_levels, ...audit_severity_levels.map(capitalize)]);
|
|
13
12
|
export const Config = z
|
|
14
13
|
.looseObject({
|
|
15
14
|
/** Whether /api/admin is enabled */
|
|
@@ -25,8 +24,9 @@ export const Config = z
|
|
|
25
24
|
allow_raw: z.boolean(),
|
|
26
25
|
/** How many days to keep events in the audit log */
|
|
27
26
|
retention: z.number().min(0),
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
/** Minimum severity level. Less severe events will be ignored. */
|
|
28
|
+
min_severity: z.literal(audit_severity_levels),
|
|
29
|
+
auto_suspend: z.literal(audit_severity_levels),
|
|
30
30
|
})
|
|
31
31
|
.partial(),
|
|
32
32
|
auth: z
|
|
@@ -61,6 +61,8 @@ export const Config = z
|
|
|
61
61
|
origin: z.string(),
|
|
62
62
|
request_size_limit: z.number().min(0).optional(),
|
|
63
63
|
show_duplicate_state: z.boolean(),
|
|
64
|
+
/** Who can use the user discovery API. For example, setting to `admin` means regular users need to type a full email in the ACL dialog and won't be shown results */
|
|
65
|
+
user_discovery: z.literal(['disabled', 'admin', 'user', 'public']),
|
|
64
66
|
verifications: z
|
|
65
67
|
.looseObject({
|
|
66
68
|
/** In minutes */
|
|
@@ -127,6 +129,7 @@ export const defaultConfig = {
|
|
|
127
129
|
origin: 'https://test.localhost',
|
|
128
130
|
show_duplicate_state: false,
|
|
129
131
|
request_size_limit: 0,
|
|
132
|
+
user_discovery: 'user',
|
|
130
133
|
verifications: {
|
|
131
134
|
timeout: 60,
|
|
132
135
|
email: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"clean": "rm -rf build .svelte-kit node_modules/{.vite,.vite-temp}"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@axium/client": ">=0.
|
|
51
|
-
"@axium/core": ">=0.
|
|
50
|
+
"@axium/client": ">=0.13.0",
|
|
51
|
+
"@axium/core": ">=0.19.0",
|
|
52
52
|
"kysely": "^0.28.0",
|
|
53
53
|
"utilium": "^2.6.0",
|
|
54
54
|
"zod": "^4.0.5"
|
package/schemas/config.json
CHANGED
|
@@ -40,15 +40,7 @@
|
|
|
40
40
|
"warning",
|
|
41
41
|
"notice",
|
|
42
42
|
"info",
|
|
43
|
-
"debug"
|
|
44
|
-
"Emergency",
|
|
45
|
-
"Alert",
|
|
46
|
-
"Critical",
|
|
47
|
-
"Error",
|
|
48
|
-
"Warning",
|
|
49
|
-
"Notice",
|
|
50
|
-
"Info",
|
|
51
|
-
"Debug"
|
|
43
|
+
"debug"
|
|
52
44
|
]
|
|
53
45
|
},
|
|
54
46
|
"auto_suspend": {
|
|
@@ -61,15 +53,7 @@
|
|
|
61
53
|
"warning",
|
|
62
54
|
"notice",
|
|
63
55
|
"info",
|
|
64
|
-
"debug"
|
|
65
|
-
"Emergency",
|
|
66
|
-
"Alert",
|
|
67
|
-
"Critical",
|
|
68
|
-
"Error",
|
|
69
|
-
"Warning",
|
|
70
|
-
"Notice",
|
|
71
|
-
"Info",
|
|
72
|
-
"Debug"
|
|
56
|
+
"debug"
|
|
73
57
|
]
|
|
74
58
|
}
|
|
75
59
|
},
|
|
@@ -154,6 +138,15 @@
|
|
|
154
138
|
"show_duplicate_state": {
|
|
155
139
|
"type": "boolean"
|
|
156
140
|
},
|
|
141
|
+
"user_discovery": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"enum": [
|
|
144
|
+
"disabled",
|
|
145
|
+
"admin",
|
|
146
|
+
"user",
|
|
147
|
+
"public"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
157
150
|
"verifications": {
|
|
158
151
|
"type": "object",
|
|
159
152
|
"properties": {
|