@axium/server 0.37.0 → 0.37.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acl.d.ts +1 -0
- package/dist/api/register.js +1 -1
- package/dist/api/users.js +7 -8
- package/dist/auth.d.ts +3 -3
- package/dist/auth.js +32 -10
- package/dist/db/schema.json +10 -0
- package/dist/requests.d.ts +3 -1
- package/dist/requests.js +23 -2
- package/package.json +4 -1
- package/routes/login/client/+page.ts +3 -1
package/dist/acl.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type PermissionsFor<TB extends TableName> = Omit<kysely.Selectable<db.Sch
|
|
|
19
19
|
export type Result<TB extends TableName> = AccessControlInternal & PermissionsFor<TB>;
|
|
20
20
|
export type WithACL<TB extends TargetName> = kysely.Selectable<db.Schema[TB]> & {
|
|
21
21
|
userId: string;
|
|
22
|
+
parentId?: string | null;
|
|
22
23
|
acl: Result<`acl.${TB}`>[];
|
|
23
24
|
};
|
|
24
25
|
export interface ACLSelectionOptions {
|
package/dist/api/register.js
CHANGED
|
@@ -64,7 +64,7 @@ async function POST(request) {
|
|
|
64
64
|
deviceType: registrationInfo.credentialDeviceType,
|
|
65
65
|
backedUp: registrationInfo.credentialBackedUp,
|
|
66
66
|
}).catch(withError('Failed to create passkey', 500));
|
|
67
|
-
return await createSessionData(userId);
|
|
67
|
+
return await createSessionData(userId, request);
|
|
68
68
|
}
|
|
69
69
|
addRoute({
|
|
70
70
|
path: '/api/register',
|
package/dist/api/users.js
CHANGED
|
@@ -100,7 +100,7 @@ addRoute({
|
|
|
100
100
|
path: '/api/users/:id/auth',
|
|
101
101
|
params,
|
|
102
102
|
async OPTIONS(request, { id: userId }) {
|
|
103
|
-
const { type } = await parseBody(request, UserAuthOptions);
|
|
103
|
+
const { type, client } = await parseBody(request, UserAuthOptions);
|
|
104
104
|
const user = await getUser(userId).catch(withError('User does not exist', 404));
|
|
105
105
|
if (user.isSuspended)
|
|
106
106
|
error(403, 'User is suspended');
|
|
@@ -111,7 +111,7 @@ addRoute({
|
|
|
111
111
|
rpID: config.auth.rp_id,
|
|
112
112
|
allowCredentials: passkeys.map(passkey => pick(passkey, 'id', 'transports')),
|
|
113
113
|
});
|
|
114
|
-
challenges.set(userId, { data: options.challenge, type });
|
|
114
|
+
challenges.set(userId, { data: options.challenge, type, client });
|
|
115
115
|
return options;
|
|
116
116
|
},
|
|
117
117
|
async POST(request, { id: userId }) {
|
|
@@ -119,7 +119,7 @@ addRoute({
|
|
|
119
119
|
const auth = challenges.get(userId);
|
|
120
120
|
if (!auth)
|
|
121
121
|
error(404, 'No challenge');
|
|
122
|
-
const { data: expectedChallenge, type } = auth;
|
|
122
|
+
const { data: expectedChallenge, type, client } = auth;
|
|
123
123
|
challenges.delete(userId);
|
|
124
124
|
const passkey = await getPasskey(response.id).catch(withError('Passkey does not exist', 404));
|
|
125
125
|
if (passkey.userId !== userId)
|
|
@@ -137,14 +137,13 @@ addRoute({
|
|
|
137
137
|
error(401, 'Verification failed');
|
|
138
138
|
switch (type) {
|
|
139
139
|
case 'login':
|
|
140
|
-
return await createSessionData(userId);
|
|
140
|
+
return await createSessionData(userId, request);
|
|
141
141
|
case 'client_login':
|
|
142
|
-
|
|
143
|
-
return await createSessionData(userId, { noCookie: true });
|
|
142
|
+
return await createSessionData(userId, request, { noCookie: true, clientUA: client });
|
|
144
143
|
case 'action':
|
|
145
144
|
if ((Date.now() - passkey.createdAt.getTime()) / 60_000 < config.auth.passkey_probation)
|
|
146
145
|
error(403, 'You can not authorize sensitive actions with a newly created passkey');
|
|
147
|
-
return await createSessionData(userId, { elevated: true });
|
|
146
|
+
return await createSessionData(userId, request, { elevated: true });
|
|
148
147
|
}
|
|
149
148
|
},
|
|
150
149
|
});
|
|
@@ -266,6 +265,6 @@ addRoute({
|
|
|
266
265
|
async POST(request, { id: userId }) {
|
|
267
266
|
const { token } = await parseBody(request, z.object({ token: z.string() }));
|
|
268
267
|
await useVerification('login', userId, token).catch(withError('Invalid or expired verification token', 400));
|
|
269
|
-
return await createSessionData(userId);
|
|
268
|
+
return await createSessionData(userId, request);
|
|
270
269
|
},
|
|
271
270
|
});
|
package/dist/auth.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function updateUser({ id, ...user }: WithRequired<Insertable<Sche
|
|
|
8
8
|
export interface SessionInternal extends Session {
|
|
9
9
|
token: string;
|
|
10
10
|
}
|
|
11
|
-
export declare function createSession(userId: string, elevated?: boolean): Promise<SessionInternal>;
|
|
11
|
+
export declare function createSession(userId: string, name: string | null, elevated?: boolean): Promise<SessionInternal>;
|
|
12
12
|
export interface SessionAndUser extends SessionInternal {
|
|
13
13
|
user: UserInternal;
|
|
14
14
|
}
|
|
@@ -41,9 +41,9 @@ export interface ItemAuthResult<TB extends acl.TargetName> {
|
|
|
41
41
|
user?: UserInternal;
|
|
42
42
|
session?: SessionInternal;
|
|
43
43
|
}
|
|
44
|
-
export declare function authSessionForItem<const TB extends acl.TargetName>(itemType: TB, itemId: string, permissions: Partial<acl.PermissionsFor<`acl.${TB}`>>, session?: SessionAndUser | null): Promise<ItemAuthResult<TB>>;
|
|
44
|
+
export declare function authSessionForItem<const TB extends acl.TargetName>(itemType: TB, itemId: string, permissions: Partial<acl.PermissionsFor<`acl.${TB}`>>, session?: SessionAndUser | null, recursive?: boolean): Promise<ItemAuthResult<TB>>;
|
|
45
45
|
/**
|
|
46
46
|
* Authenticate a request against an "item" which has an ACL table.
|
|
47
47
|
* This will fetch the item, ACLs, users, and the authenticating session.
|
|
48
48
|
*/
|
|
49
|
-
export declare function authRequestForItem<const TB extends acl.TargetName>(request: Request, itemType: TB, itemId: string, permissions: Partial<acl.PermissionsFor<`acl.${TB}
|
|
49
|
+
export declare function authRequestForItem<const TB extends acl.TargetName>(request: Request, itemType: TB, itemId: string, permissions: Partial<acl.PermissionsFor<`acl.${TB}`>>, recursive?: boolean): Promise<ItemAuthResult<TB>>;
|
package/dist/auth.js
CHANGED
|
@@ -13,10 +13,11 @@ export async function updateUser({ id, ...user }) {
|
|
|
13
13
|
}
|
|
14
14
|
const in30days = () => new Date(Date.now() + 2592000000);
|
|
15
15
|
const in10minutes = () => new Date(Date.now() + 600000);
|
|
16
|
-
export async function createSession(userId, elevated = false) {
|
|
16
|
+
export async function createSession(userId, name, elevated = false) {
|
|
17
17
|
const session = {
|
|
18
18
|
id: randomUUID(),
|
|
19
19
|
userId,
|
|
20
|
+
name,
|
|
20
21
|
token: randomBytes(64).toString('base64'),
|
|
21
22
|
expires: elevated ? in10minutes() : in30days(),
|
|
22
23
|
elevated,
|
|
@@ -110,7 +111,7 @@ export async function checkAuthForUser(request, userId, sensitive = false) {
|
|
|
110
111
|
error(403, 'This token can not be used for sensitive actions');
|
|
111
112
|
return Object.assign(session, { accessor: session.user });
|
|
112
113
|
}
|
|
113
|
-
export async function authSessionForItem(itemType, itemId, permissions, session) {
|
|
114
|
+
export async function authSessionForItem(itemType, itemId, permissions, session, recursive = false) {
|
|
114
115
|
const { userId, user } = session ?? {};
|
|
115
116
|
// Note: we need to do casting because of TS limitations with generics
|
|
116
117
|
const item = (await db
|
|
@@ -137,21 +138,42 @@ export async function authSessionForItem(itemType, itemId, permissions, session)
|
|
|
137
138
|
if (userId == item.userId)
|
|
138
139
|
return result;
|
|
139
140
|
result.fromACL = true;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
let current = item;
|
|
142
|
+
for (let i = 0; i < 25; i++) {
|
|
143
|
+
try {
|
|
144
|
+
if (!current.acl || !current.acl.length)
|
|
145
|
+
error(403, 'Item is not shared with you');
|
|
146
|
+
const missing = Array.from(acl.check(current.acl, permissions));
|
|
147
|
+
if (missing.length)
|
|
148
|
+
error(403, 'Missing permissions: ' + missing.join(', '));
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
if (!current.parentId || !recursive)
|
|
153
|
+
throw e;
|
|
154
|
+
current = (await db
|
|
155
|
+
.selectFrom(itemType)
|
|
156
|
+
.selectAll()
|
|
157
|
+
.where('id', '=', current.parentId)
|
|
158
|
+
.$if(!!userId, eb => eb.select(acl.from(itemType, { user })))
|
|
159
|
+
.executeTakeFirstOrThrow()
|
|
160
|
+
.catch(e => {
|
|
161
|
+
if (e.message.includes('no rows'))
|
|
162
|
+
error(404, itemType + ' not found');
|
|
163
|
+
throw e;
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
error(403, 'You do not have permissions for any of the last 25 parent items');
|
|
146
168
|
}
|
|
147
169
|
/**
|
|
148
170
|
* Authenticate a request against an "item" which has an ACL table.
|
|
149
171
|
* This will fetch the item, ACLs, users, and the authenticating session.
|
|
150
172
|
*/
|
|
151
|
-
export async function authRequestForItem(request, itemType, itemId, permissions) {
|
|
173
|
+
export async function authRequestForItem(request, itemType, itemId, permissions, recursive = false) {
|
|
152
174
|
const token = getToken(request, false);
|
|
153
175
|
if (!token)
|
|
154
176
|
error(401, 'Missing token');
|
|
155
177
|
const session = await getSessionAndUser(token).catch(() => null);
|
|
156
|
-
return await authSessionForItem(itemType, itemId, permissions, session);
|
|
178
|
+
return await authSessionForItem(itemType, itemId, permissions, session, recursive);
|
|
157
179
|
}
|
package/dist/db/schema.json
CHANGED
package/dist/requests.d.ts
CHANGED
|
@@ -35,11 +35,13 @@ export declare function json(data: object, init?: ResponseInit): Response;
|
|
|
35
35
|
export declare function parseBody<const Schema extends z.ZodType>(request: Request, schema: Schema): Promise<z.infer<Schema>>;
|
|
36
36
|
export declare function parseSearch<const Schema extends z.ZodType>(request: Request, schema: Schema): z.infer<Schema>;
|
|
37
37
|
export declare function getToken(request: Request, sensitive?: boolean): string | undefined;
|
|
38
|
+
export declare function getPrettyUA(request: Request, uaOverride?: string): Promise<string | null>;
|
|
38
39
|
export interface CreateSessionOptions {
|
|
39
40
|
elevated?: boolean;
|
|
40
41
|
noCookie?: boolean;
|
|
42
|
+
clientUA?: string;
|
|
41
43
|
}
|
|
42
|
-
export declare function createSessionData(userId: string, { elevated, noCookie }?: CreateSessionOptions): Promise<Response>;
|
|
44
|
+
export declare function createSessionData(userId: string, request: Request, { elevated, noCookie, clientUA }?: CreateSessionOptions): Promise<Response>;
|
|
43
45
|
export declare function stripUser(user: UserInternal, includeProtected?: boolean): User;
|
|
44
46
|
export declare function withError(text: string, code?: number): (e: Error | ResponseError) => never;
|
|
45
47
|
export declare function handleAPIRequest(request: Request, params: Record<string, any>, route: ServerRoute): Promise<Response>;
|
package/dist/requests.js
CHANGED
|
@@ -62,8 +62,29 @@ export function getToken(request, sensitive = false) {
|
|
|
62
62
|
return cookie.parse(request.headers.get('cookie') || '')[sensitive ? 'elevated_token' : 'session_token'];
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const uaParserExtension = {
|
|
66
|
+
browser: [[/(axium[- ]client)\/([\d.]+)/i], ['name', 'version']],
|
|
67
|
+
};
|
|
68
|
+
export async function getPrettyUA(request, uaOverride) {
|
|
69
|
+
const uaString = uaOverride || request.headers.get('User-Agent') || '';
|
|
70
|
+
try {
|
|
71
|
+
const { UAParser } = await import('ua-parser-js');
|
|
72
|
+
const ua = UAParser(uaString, uaParserExtension);
|
|
73
|
+
return [
|
|
74
|
+
ua.browser.name,
|
|
75
|
+
ua.browser.name && /axium[- ]client/i.test(ua.browser.name) ? ua.browser.version : ua.browser.major,
|
|
76
|
+
ua.os.name && ' on ' + ua.os.name,
|
|
77
|
+
]
|
|
78
|
+
.filter(p => p)
|
|
79
|
+
.join(' ');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function createSessionData(userId, request, { elevated = false, noCookie, clientUA } = {}) {
|
|
86
|
+
const name = await getPrettyUA(request, clientUA);
|
|
87
|
+
const { token, expires } = await createSession(userId, name, elevated);
|
|
67
88
|
const response = json({ userId, token: elevated ? '[[redacted:elevated]]' : token }, { status: 201 });
|
|
68
89
|
if (noCookie)
|
|
69
90
|
return response;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/server",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"funding": {
|
|
6
6
|
"type": "individual",
|
|
@@ -66,5 +66,8 @@
|
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@sveltejs/adapter-node": "^5.2.12",
|
|
68
68
|
"vite-plugin-mkcert": "^1.17.8"
|
|
69
|
+
},
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"ua-parser-js": "^2.0.9"
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -13,12 +13,14 @@ export async function load({ parent, url }: Omit<PageLoadEvent, 'parent'> & { pa
|
|
|
13
13
|
const port = parseInt(url.searchParams.get('port') ?? '!');
|
|
14
14
|
const localCallback = new URL('http://localhost');
|
|
15
15
|
|
|
16
|
+
const client = url.searchParams.get('client') ?? '';
|
|
17
|
+
|
|
16
18
|
let options,
|
|
17
19
|
error: string | null = null;
|
|
18
20
|
try {
|
|
19
21
|
if (Number.isNaN(port)) throw new Error('Invalid port number provided by local client');
|
|
20
22
|
localCallback.port = port.toString();
|
|
21
|
-
options = await fetchAPI('OPTIONS', 'users/:id/auth', { type: 'client_login' }, session.userId);
|
|
23
|
+
options = await fetchAPI('OPTIONS', 'users/:id/auth', { type: 'client_login', client }, session.userId);
|
|
22
24
|
} catch (e: any) {
|
|
23
25
|
error = e.message;
|
|
24
26
|
}
|