@axium/server 0.18.6 → 0.19.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/build/client/_app/immutable/chunks/BYEtX5Az.js +1 -0
- package/build/client/_app/immutable/chunks/BYEtX5Az.js.br +0 -0
- package/build/client/_app/immutable/chunks/BYEtX5Az.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Cyzk4yeH.js +3 -0
- package/build/client/_app/immutable/chunks/Cyzk4yeH.js.br +0 -0
- package/build/client/_app/immutable/chunks/Cyzk4yeH.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B3bf7yrh.js → DCUM25og.js} +1 -1
- package/build/client/_app/immutable/chunks/DCUM25og.js.br +0 -0
- package/build/client/_app/immutable/chunks/{B3bf7yrh.js.gz → DCUM25og.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/DY1x3fBx.js +1 -0
- package/build/client/_app/immutable/chunks/DY1x3fBx.js.br +0 -0
- package/build/client/_app/immutable/chunks/DY1x3fBx.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B4IjTTLF.js → DYZBB-Ix.js} +8 -8
- package/build/client/_app/immutable/chunks/DYZBB-Ix.js.br +0 -0
- package/build/client/_app/immutable/chunks/DYZBB-Ix.js.gz +0 -0
- package/build/client/_app/immutable/chunks/FWeOBoAM.js +1 -0
- package/build/client/_app/immutable/chunks/FWeOBoAM.js.br +0 -0
- package/build/client/_app/immutable/chunks/FWeOBoAM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/IgiyYk6u.js +1 -0
- package/build/client/_app/immutable/chunks/IgiyYk6u.js.br +0 -0
- package/build/client/_app/immutable/chunks/IgiyYk6u.js.gz +0 -0
- package/build/client/_app/immutable/chunks/p-F-zYzk.js +1 -0
- package/build/client/_app/immutable/chunks/p-F-zYzk.js.br +0 -0
- package/build/client/_app/immutable/chunks/p-F-zYzk.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.BOAQU4lu.js +2 -0
- package/build/client/_app/immutable/entry/app.BOAQU4lu.js.br +0 -0
- package/build/client/_app/immutable/entry/app.BOAQU4lu.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BcxVnXqP.js +1 -0
- package/build/client/_app/immutable/entry/start.BcxVnXqP.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BcxVnXqP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.BbGefPez.js → 0.fU2Fkia3.js} +1 -1
- package/build/client/_app/immutable/nodes/0.fU2Fkia3.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.fU2Fkia3.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.ebxToP8U.js +1 -0
- package/build/client/_app/immutable/nodes/1.ebxToP8U.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.ebxToP8U.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2._Jmh03he.js → 2.Cczp5_pl.js} +1 -1
- package/build/client/_app/immutable/nodes/2.Cczp5_pl.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.Cczp5_pl.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BwwptDiR.js +1 -0
- package/build/client/_app/immutable/nodes/3.BwwptDiR.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BwwptDiR.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.DFZa97F0.js +1 -0
- package/build/client/_app/immutable/nodes/4.DFZa97F0.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.DFZa97F0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.CObFvODa.js +1 -0
- package/build/client/_app/immutable/nodes/5.CObFvODa.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.CObFvODa.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.ezb4muQm.js +1 -0
- package/build/client/_app/immutable/nodes/6.ezb4muQm.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.ezb4muQm.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-CUfr8UwD.js → 0-BwO1RCtn.js} +2 -2
- package/build/server/chunks/{0-CUfr8UwD.js.map → 0-BwO1RCtn.js.map} +1 -1
- package/build/server/chunks/1-BtBlW1Zd.js +9 -0
- package/build/server/chunks/{1-DffE3_iJ.js.map → 1-BtBlW1Zd.js.map} +1 -1
- package/build/server/chunks/{2-c1bljkMr.js → 2-BwMXZ3uH.js} +2 -2
- package/build/server/chunks/{2-c1bljkMr.js.map → 2-BwMXZ3uH.js.map} +1 -1
- package/build/server/chunks/3-DZ3qU23G.js +9 -0
- package/build/server/chunks/3-DZ3qU23G.js.map +1 -0
- package/build/server/chunks/4-CiYhCg7w.js +9 -0
- package/build/server/chunks/4-CiYhCg7w.js.map +1 -0
- package/build/server/chunks/5-DeLLDZUB.js +9 -0
- package/build/server/chunks/5-DeLLDZUB.js.map +1 -0
- package/build/server/chunks/6-WSYEiBK_.js +9 -0
- package/build/server/chunks/6-WSYEiBK_.js.map +1 -0
- package/build/server/chunks/{Logout-CkwmxNTt.js → Logout-CXx10rDN.js} +4 -5
- package/build/server/chunks/Logout-CXx10rDN.js.map +1 -0
- package/build/server/chunks/_page.svelte-BBp1dML7.js +11 -0
- package/build/server/chunks/{_page.svelte-DPdvFuWg.js.map → _page.svelte-BBp1dML7.js.map} +1 -1
- package/build/server/chunks/{_page.svelte-CVJoQAAB.js → _page.svelte-C_AR-luT.js} +4 -5
- package/build/server/chunks/_page.svelte-C_AR-luT.js.map +1 -0
- package/build/server/chunks/{_page.svelte-whmO3TtB.js → _page.svelte-CrDbYLUc.js} +10 -365
- package/build/server/chunks/_page.svelte-CrDbYLUc.js.map +1 -0
- package/build/server/chunks/{_page.svelte-D3kXcWlK.js → _page.svelte-Ds-0vPQG.js} +4 -5
- package/build/server/chunks/_page.svelte-Ds-0vPQG.js.map +1 -0
- package/build/server/chunks/error.svelte-DkJRpKga.js +46 -0
- package/build/server/chunks/error.svelte-DkJRpKga.js.map +1 -0
- package/build/server/chunks/exports-Cc9yggiy.js +226 -0
- package/build/server/chunks/exports-Cc9yggiy.js.map +1 -0
- package/build/server/chunks/hooks.server-PyYsuy1-.js +1324 -0
- package/build/server/chunks/hooks.server-PyYsuy1-.js.map +1 -0
- package/build/server/chunks/{exports-DeY8o0ws.js → index-CBVFb-Fk.js} +2 -223
- package/build/server/chunks/index-CBVFb-Fk.js.map +1 -0
- package/build/server/chunks/{user-DW-tjDe0.js → schemas-C2VqNPFY.js} +129 -2079
- package/build/server/chunks/schemas-C2VqNPFY.js.map +1 -0
- package/build/server/chunks/string-CafUlmcI.js +355 -0
- package/build/server/chunks/string-CafUlmcI.js.map +1 -0
- package/build/server/chunks/user-B61vTdij.js +2078 -0
- package/build/server/chunks/user-B61vTdij.js.map +1 -0
- package/build/server/index.js +4 -3
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +8 -8
- package/build/server/manifest.js.map +1 -1
- package/dist/api/acl.js +2 -2
- package/dist/api/metadata.js +3 -4
- package/dist/api/passkeys.js +1 -2
- package/dist/api/register.js +9 -10
- package/dist/api/session.js +1 -2
- package/dist/api/users.js +11 -12
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -2
- package/dist/cli.js +2 -2
- package/dist/config.d.ts +74 -70
- package/dist/config.js +62 -57
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/requests.d.ts +36 -2
- package/dist/requests.js +65 -7
- package/dist/routes.d.ts +1 -1
- package/dist/serve.d.ts +6 -1
- package/dist/serve.js +19 -1
- package/dist/sveltekit.d.ts +3 -3
- package/dist/sveltekit.js +11 -46
- package/package.json +1 -2
- package/routes/account/+page.svelte +4 -5
- package/svelte.config.js +1 -2
- package/web/hooks.server.ts +3 -17
- package/web/lib/FormDialog.svelte +4 -5
- package/web/lib/Logout.svelte +6 -2
- package/web/tsconfig.json +1 -1
- package/build/client/_app/immutable/chunks/B3bf7yrh.js.br +0 -0
- package/build/client/_app/immutable/chunks/B4IjTTLF.js.br +0 -0
- package/build/client/_app/immutable/chunks/B4IjTTLF.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BeFE4kE2.js +0 -3
- package/build/client/_app/immutable/chunks/BeFE4kE2.js.br +0 -0
- package/build/client/_app/immutable/chunks/BeFE4kE2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CtT1TXOG.js +0 -1
- package/build/client/_app/immutable/chunks/CtT1TXOG.js.br +0 -0
- package/build/client/_app/immutable/chunks/CtT1TXOG.js.gz +0 -0
- package/build/client/_app/immutable/chunks/D5lcOOwR.js +0 -1
- package/build/client/_app/immutable/chunks/D5lcOOwR.js.br +0 -0
- package/build/client/_app/immutable/chunks/D5lcOOwR.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DCs3KlJy.js +0 -1
- package/build/client/_app/immutable/chunks/DCs3KlJy.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCs3KlJy.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ct9jA5hl.js +0 -1
- package/build/client/_app/immutable/chunks/ct9jA5hl.js.br +0 -0
- package/build/client/_app/immutable/chunks/ct9jA5hl.js.gz +0 -0
- package/build/client/_app/immutable/chunks/rXU5YtEm.js +0 -1
- package/build/client/_app/immutable/chunks/rXU5YtEm.js.br +0 -0
- package/build/client/_app/immutable/chunks/rXU5YtEm.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.VDao1I0a.js +0 -2
- package/build/client/_app/immutable/entry/app.VDao1I0a.js.br +0 -0
- package/build/client/_app/immutable/entry/app.VDao1I0a.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.CqVn4hZK.js +0 -1
- package/build/client/_app/immutable/entry/start.CqVn4hZK.js.br +0 -0
- package/build/client/_app/immutable/entry/start.CqVn4hZK.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.BbGefPez.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.BbGefPez.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.C3jvF6zj.js +0 -1
- package/build/client/_app/immutable/nodes/1.C3jvF6zj.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.C3jvF6zj.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2._Jmh03he.js.br +0 -0
- package/build/client/_app/immutable/nodes/2._Jmh03he.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.C1G23fTe.js +0 -1
- package/build/client/_app/immutable/nodes/3.C1G23fTe.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.C1G23fTe.js.gz +0 -0
- package/build/client/_app/immutable/nodes/4.TQjMBeyV.js +0 -1
- package/build/client/_app/immutable/nodes/4.TQjMBeyV.js.br +0 -0
- package/build/client/_app/immutable/nodes/4.TQjMBeyV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.SHyxFZfo.js +0 -1
- package/build/client/_app/immutable/nodes/5.SHyxFZfo.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.SHyxFZfo.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.ikul5Bcl.js +0 -1
- package/build/client/_app/immutable/nodes/6.ikul5Bcl.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.ikul5Bcl.js.gz +0 -0
- package/build/server/chunks/1-DffE3_iJ.js +0 -9
- package/build/server/chunks/3-D6wMMS06.js +0 -9
- package/build/server/chunks/3-D6wMMS06.js.map +0 -1
- package/build/server/chunks/4-BIBXJVxM.js +0 -9
- package/build/server/chunks/4-BIBXJVxM.js.map +0 -1
- package/build/server/chunks/5-DnTszr4V.js +0 -9
- package/build/server/chunks/5-DnTszr4V.js.map +0 -1
- package/build/server/chunks/6-Ls1UJLAz.js +0 -9
- package/build/server/chunks/6-Ls1UJLAz.js.map +0 -1
- package/build/server/chunks/Logout-CkwmxNTt.js.map +0 -1
- package/build/server/chunks/_page.svelte-CVJoQAAB.js.map +0 -1
- package/build/server/chunks/_page.svelte-D3kXcWlK.js.map +0 -1
- package/build/server/chunks/_page.svelte-DPdvFuWg.js +0 -12
- package/build/server/chunks/_page.svelte-whmO3TtB.js.map +0 -1
- package/build/server/chunks/client-C5BesWYV.js +0 -33
- package/build/server/chunks/client-C5BesWYV.js.map +0 -1
- package/build/server/chunks/error.svelte-Crm_85Km.js +0 -24
- package/build/server/chunks/error.svelte-Crm_85Km.js.map +0 -1
- package/build/server/chunks/exports-DeY8o0ws.js.map +0 -1
- package/build/server/chunks/hooks.server-DmgUjoZV.js +0 -17
- package/build/server/chunks/hooks.server-DmgUjoZV.js.map +0 -1
- package/build/server/chunks/user-DW-tjDe0.js.map +0 -1
|
@@ -0,0 +1,1324 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, writeFileSync, createWriteStream } from 'node:fs';
|
|
3
|
+
import 'node:http';
|
|
4
|
+
import 'node:https';
|
|
5
|
+
import { Logger, levelText, allLogLevels } from 'logzen';
|
|
6
|
+
import { join, resolve, dirname } from 'node:path/posix';
|
|
7
|
+
import './string-CafUlmcI.js';
|
|
8
|
+
import 'node:child_process';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { styleText } from 'node:util';
|
|
11
|
+
import { _ as _unknown, a as _tuple, b as _array, $ as $ZodUnknown, c as $ZodArray, d as $ZodTuple, p as parse, e as parseAsync, l as literal, o as object, r as record, f as any, s as string, n as number, g as array, h as _enum, u as uuid, i as email, j as boolean, k as custom, m as looseObject, q as partialRecord, t as prettifyError, v as date, w as url } from './schemas-C2VqNPFY.js';
|
|
12
|
+
import { serialize } from 'cookie';
|
|
13
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
14
|
+
import { Kysely, PostgresDialect } from 'kysely';
|
|
15
|
+
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
|
16
|
+
import pg from 'pg';
|
|
17
|
+
import * as webauthn from '@simplewebauthn/server';
|
|
18
|
+
import { verifyRegistrationResponse, generateRegistrationOptions } from '@simplewebauthn/server';
|
|
19
|
+
import { render } from 'svelte/server';
|
|
20
|
+
|
|
21
|
+
function filterObject(object, predicate) {
|
|
22
|
+
const entries = Object.entries(object);
|
|
23
|
+
return Object.fromEntries(entries.filter(([key, value]) => predicate(key, value)));
|
|
24
|
+
}
|
|
25
|
+
function pick(object, ...keys) {
|
|
26
|
+
const picked = {};
|
|
27
|
+
for (const key of keys.flat()) {
|
|
28
|
+
picked[key] = object[key];
|
|
29
|
+
}
|
|
30
|
+
return picked;
|
|
31
|
+
}
|
|
32
|
+
function omit(object, ...keys) {
|
|
33
|
+
return filterObject(object, key => !keys.flat().includes(key));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns whether `value` is not a primitive.
|
|
37
|
+
*
|
|
38
|
+
* This function is only useful for the type check,
|
|
39
|
+
* you can do `Object(v) === v` otherwise.
|
|
40
|
+
*/
|
|
41
|
+
function isObject(value) {
|
|
42
|
+
return Object(value) === value;
|
|
43
|
+
}
|
|
44
|
+
function deepAssign(to, from) {
|
|
45
|
+
const keys = new Set([
|
|
46
|
+
...Object.keys(to),
|
|
47
|
+
...Object.keys(from),
|
|
48
|
+
]);
|
|
49
|
+
for (const key of keys) {
|
|
50
|
+
if (!(key in from))
|
|
51
|
+
continue;
|
|
52
|
+
const value = from[key];
|
|
53
|
+
if (!(key in to)) {
|
|
54
|
+
to[key] = value;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (!isObject(to[key]) && Object(value) !== value) {
|
|
58
|
+
to[key] = value;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (isObject(to[key]) && Object(value) === value) {
|
|
62
|
+
deepAssign(to[key], value);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw new TypeError(!isObject(to[key])
|
|
66
|
+
? 'Can not deeply assign an object to a primitive'
|
|
67
|
+
: 'Can not deeply assign a primitive to an object');
|
|
68
|
+
}
|
|
69
|
+
return to;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class $ZodFunction {
|
|
73
|
+
constructor(def) {
|
|
74
|
+
this._def = def;
|
|
75
|
+
this.def = def;
|
|
76
|
+
}
|
|
77
|
+
implement(func) {
|
|
78
|
+
if (typeof func !== "function") {
|
|
79
|
+
throw new Error("implement() must be called with a function");
|
|
80
|
+
}
|
|
81
|
+
const impl = ((...args) => {
|
|
82
|
+
const parsedArgs = this._def.input ? parse(this._def.input, args, undefined, { callee: impl }) : args;
|
|
83
|
+
if (!Array.isArray(parsedArgs)) {
|
|
84
|
+
throw new Error("Invalid arguments schema: not an array or tuple schema.");
|
|
85
|
+
}
|
|
86
|
+
const output = func(...parsedArgs);
|
|
87
|
+
return this._def.output ? parse(this._def.output, output, undefined, { callee: impl }) : output;
|
|
88
|
+
});
|
|
89
|
+
return impl;
|
|
90
|
+
}
|
|
91
|
+
implementAsync(func) {
|
|
92
|
+
if (typeof func !== "function") {
|
|
93
|
+
throw new Error("implement() must be called with a function");
|
|
94
|
+
}
|
|
95
|
+
const impl = (async (...args) => {
|
|
96
|
+
const parsedArgs = this._def.input ? await parseAsync(this._def.input, args, undefined, { callee: impl }) : args;
|
|
97
|
+
if (!Array.isArray(parsedArgs)) {
|
|
98
|
+
throw new Error("Invalid arguments schema: not an array or tuple schema.");
|
|
99
|
+
}
|
|
100
|
+
const output = await func(...parsedArgs);
|
|
101
|
+
return this._def.output ? parseAsync(this._def.output, output, undefined, { callee: impl }) : output;
|
|
102
|
+
});
|
|
103
|
+
return impl;
|
|
104
|
+
}
|
|
105
|
+
input(...args) {
|
|
106
|
+
const F = this.constructor;
|
|
107
|
+
if (Array.isArray(args[0])) {
|
|
108
|
+
return new F({
|
|
109
|
+
type: "function",
|
|
110
|
+
input: new $ZodTuple({
|
|
111
|
+
type: "tuple",
|
|
112
|
+
items: args[0],
|
|
113
|
+
rest: args[1],
|
|
114
|
+
}),
|
|
115
|
+
output: this._def.output,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return new F({
|
|
119
|
+
type: "function",
|
|
120
|
+
input: args[0],
|
|
121
|
+
output: this._def.output,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
output(output) {
|
|
125
|
+
const F = this.constructor;
|
|
126
|
+
return new F({
|
|
127
|
+
type: "function",
|
|
128
|
+
input: this._def.input,
|
|
129
|
+
output,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function _function(params) {
|
|
134
|
+
return new $ZodFunction({
|
|
135
|
+
type: "function",
|
|
136
|
+
input: Array.isArray(params?.input)
|
|
137
|
+
? _tuple($ZodTuple, params?.input)
|
|
138
|
+
: (params?.input ?? _array($ZodArray, _unknown($ZodUnknown))),
|
|
139
|
+
output: params?.output ?? _unknown($ZodUnknown),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sym$1 = Symbol.for('Axium:state');
|
|
144
|
+
globalThis[sym$1] ||= Object.create({ _errored: false });
|
|
145
|
+
let _doWarnings = false;
|
|
146
|
+
function _duplicateStateWarnings(value) {
|
|
147
|
+
_doWarnings = value;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Prevent duplicate shared state.
|
|
151
|
+
*/
|
|
152
|
+
function _unique(id, value) {
|
|
153
|
+
const state = globalThis[sym$1];
|
|
154
|
+
const _err = new Error();
|
|
155
|
+
Error.captureStackTrace(_err, _unique);
|
|
156
|
+
const stack = _err.stack.slice(6);
|
|
157
|
+
if (!(id in state)) {
|
|
158
|
+
state[id] = { value, stack };
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
if (!state._errored) {
|
|
162
|
+
console.error(styleText('red', 'Duplicate Axium server state! You might have multiple instances of the same module loaded.'));
|
|
163
|
+
state._errored = true;
|
|
164
|
+
}
|
|
165
|
+
_doWarnings && console.warn(styleText('yellow', `Mitigating duplicate state! (${id})\n${stack}\nFrom original\n${state[id].stack}`));
|
|
166
|
+
return state[id].value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const systemDir = '/etc/axium';
|
|
170
|
+
const userDir = join(homedir(), '.axium');
|
|
171
|
+
const dirs = _unique('dirs', [systemDir, userDir]);
|
|
172
|
+
for (let dir = resolve(process.cwd()); dir !== '/'; dir = dirname(dir)) {
|
|
173
|
+
if (fs.existsSync(join(dir, '.axium')))
|
|
174
|
+
dirs.push(join(dir, '.axium'));
|
|
175
|
+
}
|
|
176
|
+
if (process.env.AXIUM_DIR)
|
|
177
|
+
dirs.push(process.env.AXIUM_DIR);
|
|
178
|
+
try {
|
|
179
|
+
fs.mkdirSync(systemDir, { recursive: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Missing permissions
|
|
183
|
+
}
|
|
184
|
+
fs.mkdirSync(userDir, { recursive: true });
|
|
185
|
+
const logger = new Logger({
|
|
186
|
+
hideWarningStack: true,
|
|
187
|
+
noGlobalConsole: true,
|
|
188
|
+
});
|
|
189
|
+
/**
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
const output = {
|
|
193
|
+
constructor: { name: 'Console' },
|
|
194
|
+
error(message) {
|
|
195
|
+
console.error(message.startsWith('\x1b') ? message : styleText('red', message));
|
|
196
|
+
},
|
|
197
|
+
warn(message) {
|
|
198
|
+
console.warn(message.startsWith('\x1b') ? message : styleText('yellow', message));
|
|
199
|
+
},
|
|
200
|
+
info(message) {
|
|
201
|
+
console.info(message.startsWith('\x1b') ? message : styleText('blue', message));
|
|
202
|
+
},
|
|
203
|
+
log(message) {
|
|
204
|
+
console.log(message);
|
|
205
|
+
},
|
|
206
|
+
debug(message) {
|
|
207
|
+
_debugOutput && console.debug(message.startsWith('\x1b') ? message : styleText('gray', message));
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
logger.attach(output);
|
|
211
|
+
let _debugOutput = false;
|
|
212
|
+
/**
|
|
213
|
+
* Enable or disable debug output.
|
|
214
|
+
*/
|
|
215
|
+
function _setDebugOutput(enabled) {
|
|
216
|
+
_debugOutput = enabled;
|
|
217
|
+
}
|
|
218
|
+
function defaultOutput(tag, message = '') {
|
|
219
|
+
switch (tag) {
|
|
220
|
+
case 'debug':
|
|
221
|
+
_debugOutput && output.debug(message);
|
|
222
|
+
break;
|
|
223
|
+
case 'info':
|
|
224
|
+
console.log(message);
|
|
225
|
+
break;
|
|
226
|
+
case 'warn':
|
|
227
|
+
console.warn(styleText('yellow', message));
|
|
228
|
+
break;
|
|
229
|
+
case 'error':
|
|
230
|
+
console.error(styleText('red', message));
|
|
231
|
+
break;
|
|
232
|
+
case 'start':
|
|
233
|
+
process.stdout.write(message + '... ');
|
|
234
|
+
break;
|
|
235
|
+
case 'done':
|
|
236
|
+
console.log('done.');
|
|
237
|
+
break;
|
|
238
|
+
case 'plugin':
|
|
239
|
+
console.log(styleText('whiteBright', 'Running plugin: ' + message));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
let _taggedOutput = defaultOutput;
|
|
243
|
+
// Shortcuts for tagged output
|
|
244
|
+
function done() {
|
|
245
|
+
_taggedOutput?.('done');
|
|
246
|
+
}
|
|
247
|
+
function start(message) {
|
|
248
|
+
_taggedOutput?.('start', message);
|
|
249
|
+
}
|
|
250
|
+
function plugin(name) {
|
|
251
|
+
_taggedOutput?.('plugin', name);
|
|
252
|
+
}
|
|
253
|
+
function debug(message) {
|
|
254
|
+
_taggedOutput?.('debug', message);
|
|
255
|
+
}
|
|
256
|
+
function warn(message) {
|
|
257
|
+
_taggedOutput?.('warn', message);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* This is a factory for handling errors when performing operations.
|
|
261
|
+
* The handler will allow the parent scope to continue if a relation already exists,
|
|
262
|
+
* rather than fatally exiting.
|
|
263
|
+
*/
|
|
264
|
+
function someWarnings(...allowList) {
|
|
265
|
+
return (error) => {
|
|
266
|
+
error = typeof error == 'object' && 'message' in error ? error.message : error;
|
|
267
|
+
for (const [pattern, message = error] of allowList) {
|
|
268
|
+
if (!pattern.test(error))
|
|
269
|
+
continue;
|
|
270
|
+
warn(message);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function zAsyncFunction(schema) {
|
|
278
|
+
return custom((fn) => schema.implementAsync(fn));
|
|
279
|
+
}
|
|
280
|
+
const transports = ['ble', 'cable', 'hybrid', 'internal', 'nfc', 'smart-card', 'usb'];
|
|
281
|
+
const authenticatorAttachment = literal(['platform', 'cross-platform']).optional();
|
|
282
|
+
const PasskeyRegistration = object({
|
|
283
|
+
id: string(),
|
|
284
|
+
rawId: string(),
|
|
285
|
+
response: object({
|
|
286
|
+
clientDataJSON: string(),
|
|
287
|
+
attestationObject: string(),
|
|
288
|
+
authenticatorData: string().optional(),
|
|
289
|
+
transports: array(_enum(transports)).optional(),
|
|
290
|
+
publicKeyAlgorithm: number().optional(),
|
|
291
|
+
publicKey: string().optional(),
|
|
292
|
+
}),
|
|
293
|
+
authenticatorAttachment,
|
|
294
|
+
clientExtensionResults: record(any(), any()),
|
|
295
|
+
type: literal('public-key'),
|
|
296
|
+
});
|
|
297
|
+
/**
|
|
298
|
+
* POSTed to the `/users/:id/login` endpoint.
|
|
299
|
+
*/
|
|
300
|
+
const PasskeyAuthenticationResponse = object({
|
|
301
|
+
id: string(),
|
|
302
|
+
rawId: string(),
|
|
303
|
+
response: object({
|
|
304
|
+
clientDataJSON: string(),
|
|
305
|
+
authenticatorData: string(),
|
|
306
|
+
signature: string(),
|
|
307
|
+
userHandle: string().optional(),
|
|
308
|
+
}),
|
|
309
|
+
authenticatorAttachment,
|
|
310
|
+
clientExtensionResults: record(any(), any()),
|
|
311
|
+
type: literal('public-key'),
|
|
312
|
+
});
|
|
313
|
+
const APIUserRegistration = object({
|
|
314
|
+
name: string().min(1).max(100),
|
|
315
|
+
email: email(),
|
|
316
|
+
userId: uuid(),
|
|
317
|
+
response: PasskeyRegistration,
|
|
318
|
+
});
|
|
319
|
+
const PasskeyChangeable = object({ name: string() }).partial();
|
|
320
|
+
const UserAuthOptions = object({ type: literal(['login', 'action']) });
|
|
321
|
+
const LogoutSessions = object({
|
|
322
|
+
id: array(uuid()).optional(),
|
|
323
|
+
confirm_all: boolean().optional(),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const PluginMetadata = looseObject({
|
|
327
|
+
name: string(),
|
|
328
|
+
version: string(),
|
|
329
|
+
description: string().optional(),
|
|
330
|
+
routes: string().optional(),
|
|
331
|
+
});
|
|
332
|
+
const hookNames = ['db_init', 'remove', 'db_wipe', 'clean'];
|
|
333
|
+
const fn = custom(data => typeof data === 'function');
|
|
334
|
+
const Plugin = PluginMetadata.extend({
|
|
335
|
+
statusText: zAsyncFunction(_function({ input: [], output: string() })),
|
|
336
|
+
hooks: partialRecord(literal(hookNames), fn).optional(),
|
|
337
|
+
});
|
|
338
|
+
const kSpecifier = Symbol('specifier');
|
|
339
|
+
const plugins = _unique('plugins', new Set());
|
|
340
|
+
async function loadPlugin(specifier) {
|
|
341
|
+
try {
|
|
342
|
+
const imported = await import(/* @vite-ignore */ specifier);
|
|
343
|
+
const maybePlugin = 'default' in imported ? imported.default : imported;
|
|
344
|
+
const plugin = Object.assign({ hooks: {}, [kSpecifier]: specifier }, await Plugin.parseAsync(maybePlugin).catch(e => {
|
|
345
|
+
throw prettifyError(e);
|
|
346
|
+
}));
|
|
347
|
+
if (plugin.name.startsWith('#') || plugin.name.includes(' ')) {
|
|
348
|
+
throw 'Invalid plugin name. Plugin names can not start with a hash or contain spaces.';
|
|
349
|
+
}
|
|
350
|
+
plugins.add(plugin);
|
|
351
|
+
output.debug(`Loaded plugin: ${plugin.name} ${plugin.version}`);
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
output.debug(`Failed to load plugin from ${specifier}: ${e ? (e instanceof Error ? e.message : e.toString()) : e}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const ConfigSchema = looseObject({
|
|
359
|
+
allow_new_users: boolean(),
|
|
360
|
+
api: looseObject({
|
|
361
|
+
disable_metadata: boolean(),
|
|
362
|
+
cookie_auth: boolean(),
|
|
363
|
+
})
|
|
364
|
+
.partial(),
|
|
365
|
+
apps: looseObject({
|
|
366
|
+
disabled: array(string()),
|
|
367
|
+
})
|
|
368
|
+
.partial(),
|
|
369
|
+
auth: looseObject({
|
|
370
|
+
origin: string(),
|
|
371
|
+
/** In minutes */
|
|
372
|
+
passkey_probation: number(),
|
|
373
|
+
rp_id: string(),
|
|
374
|
+
rp_name: string(),
|
|
375
|
+
secure_cookies: boolean(),
|
|
376
|
+
/** In minutes */
|
|
377
|
+
verification_timeout: number(),
|
|
378
|
+
/** Whether users can verify emails */
|
|
379
|
+
email_verification: boolean(),
|
|
380
|
+
})
|
|
381
|
+
.partial(),
|
|
382
|
+
db: looseObject({
|
|
383
|
+
host: string(),
|
|
384
|
+
port: number(),
|
|
385
|
+
password: string(),
|
|
386
|
+
user: string(),
|
|
387
|
+
database: string(),
|
|
388
|
+
})
|
|
389
|
+
.partial(),
|
|
390
|
+
debug: boolean(),
|
|
391
|
+
log: looseObject({
|
|
392
|
+
level: _enum(levelText),
|
|
393
|
+
console: boolean(),
|
|
394
|
+
})
|
|
395
|
+
.partial(),
|
|
396
|
+
show_duplicate_state: boolean(),
|
|
397
|
+
web: looseObject({
|
|
398
|
+
assets: string(),
|
|
399
|
+
build: string(),
|
|
400
|
+
disable_cache: boolean(),
|
|
401
|
+
port: number().min(1).max(65535),
|
|
402
|
+
prefix: string(),
|
|
403
|
+
routes: string(),
|
|
404
|
+
secure: boolean(),
|
|
405
|
+
ssl_key: string(),
|
|
406
|
+
ssl_cert: string(),
|
|
407
|
+
template: string(),
|
|
408
|
+
})
|
|
409
|
+
.partial(),
|
|
410
|
+
})
|
|
411
|
+
.partial();
|
|
412
|
+
const configFiles = _unique('configFiles', new Map());
|
|
413
|
+
function plainConfig() {
|
|
414
|
+
return omit(config, Object.keys(configShortcuts));
|
|
415
|
+
}
|
|
416
|
+
const configShortcuts = {
|
|
417
|
+
findPath: findConfigPaths,
|
|
418
|
+
load: loadConfig,
|
|
419
|
+
loadDefaults: loadDefaultConfigs,
|
|
420
|
+
plain: plainConfig,
|
|
421
|
+
save: saveConfig,
|
|
422
|
+
saveTo: saveConfigTo,
|
|
423
|
+
set: setConfig,
|
|
424
|
+
files: configFiles,
|
|
425
|
+
};
|
|
426
|
+
const config = _unique('config', {
|
|
427
|
+
...configShortcuts,
|
|
428
|
+
allow_new_users: true,
|
|
429
|
+
api: {
|
|
430
|
+
disable_metadata: false,
|
|
431
|
+
cookie_auth: true,
|
|
432
|
+
},
|
|
433
|
+
apps: {
|
|
434
|
+
disabled: [],
|
|
435
|
+
},
|
|
436
|
+
auth: {
|
|
437
|
+
origin: 'https://test.localhost',
|
|
438
|
+
passkey_probation: 60,
|
|
439
|
+
rp_id: 'test.localhost',
|
|
440
|
+
rp_name: 'Axium',
|
|
441
|
+
secure_cookies: true,
|
|
442
|
+
verification_timeout: 60,
|
|
443
|
+
email_verification: false,
|
|
444
|
+
},
|
|
445
|
+
db: {
|
|
446
|
+
database: process.env.PGDATABASE || 'axium',
|
|
447
|
+
host: process.env.PGHOST || 'localhost',
|
|
448
|
+
password: process.env.PGPASSWORD || '',
|
|
449
|
+
port: process.env.PGPORT && Number.isSafeInteger(parseInt(process.env.PGPORT)) ? parseInt(process.env.PGPORT) : 5432,
|
|
450
|
+
user: process.env.PGUSER || 'axium',
|
|
451
|
+
},
|
|
452
|
+
debug: false,
|
|
453
|
+
log: {
|
|
454
|
+
console: true,
|
|
455
|
+
level: 'info',
|
|
456
|
+
},
|
|
457
|
+
show_duplicate_state: false,
|
|
458
|
+
web: {
|
|
459
|
+
assets: '',
|
|
460
|
+
build: '../build/handler.js',
|
|
461
|
+
disable_cache: false,
|
|
462
|
+
port: 443,
|
|
463
|
+
prefix: '',
|
|
464
|
+
routes: 'routes',
|
|
465
|
+
secure: true,
|
|
466
|
+
ssl_key: resolve(dirs[0], 'ssl_key.pem'),
|
|
467
|
+
ssl_cert: resolve(dirs[0], 'ssl_cert.pem'),
|
|
468
|
+
template: join(import.meta.dirname, '../web/template.html'),
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
// config from file
|
|
472
|
+
const FileSchema = looseObject({
|
|
473
|
+
...ConfigSchema.shape,
|
|
474
|
+
include: array(string()),
|
|
475
|
+
plugins: array(string()),
|
|
476
|
+
})
|
|
477
|
+
.partial();
|
|
478
|
+
/**
|
|
479
|
+
* Update the current config
|
|
480
|
+
*/
|
|
481
|
+
function setConfig(other) {
|
|
482
|
+
deepAssign(config, other);
|
|
483
|
+
logger.detach(output);
|
|
484
|
+
if (config.log.console)
|
|
485
|
+
logger.attach(output, { output: config.log.level });
|
|
486
|
+
_setDebugOutput(config.debug);
|
|
487
|
+
_duplicateStateWarnings(config.show_duplicate_state);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Load the config from the provided path
|
|
491
|
+
*/
|
|
492
|
+
async function loadConfig(path, options = {}) {
|
|
493
|
+
if (configFiles.has(path))
|
|
494
|
+
return;
|
|
495
|
+
let json;
|
|
496
|
+
try {
|
|
497
|
+
json = JSON.parse(readFileSync(path, 'utf8'));
|
|
498
|
+
}
|
|
499
|
+
catch (e) {
|
|
500
|
+
if (!options.optional)
|
|
501
|
+
throw e;
|
|
502
|
+
output.debug(`Skipping config at ${path} (${e.message})`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let file;
|
|
506
|
+
try {
|
|
507
|
+
file = FileSchema.parse(json);
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
if (!options.loose)
|
|
511
|
+
throw e;
|
|
512
|
+
output.debug(`Loading invalid config from ${path} (${e.message})`);
|
|
513
|
+
file = json;
|
|
514
|
+
}
|
|
515
|
+
configFiles.set(path, file);
|
|
516
|
+
setConfig(file);
|
|
517
|
+
output.debug('Loaded config: ' + path);
|
|
518
|
+
for (const include of file.include ?? [])
|
|
519
|
+
await loadConfig(join(dirname(path), include), { optional: true });
|
|
520
|
+
for (const plugin of file.plugins ?? [])
|
|
521
|
+
await loadPlugin(plugin.startsWith('.') ? resolve(dirname(path), plugin) : plugin);
|
|
522
|
+
}
|
|
523
|
+
async function loadDefaultConfigs() {
|
|
524
|
+
for (const path of findConfigPaths()) {
|
|
525
|
+
if (!existsSync(path)) {
|
|
526
|
+
try {
|
|
527
|
+
writeFileSync(path, '{}');
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
await loadConfig(path, { optional: true });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Update the current config and write the updated config to the appropriate file
|
|
538
|
+
*/
|
|
539
|
+
function saveConfig(changed, global = false) {
|
|
540
|
+
saveConfigTo(findConfigPaths().at(global ? 0 : -1), changed);
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Update the current config and write the updated config to the provided path
|
|
544
|
+
*/
|
|
545
|
+
function saveConfigTo(path, changed) {
|
|
546
|
+
setConfig(changed);
|
|
547
|
+
const config = configFiles.get(path) ?? {};
|
|
548
|
+
Object.assign(config, { ...changed, db: { ...config.db, ...changed.db } });
|
|
549
|
+
output.debug(`Wrote config to ${path}`);
|
|
550
|
+
writeFileSync(path, JSON.stringify(config));
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Find the path to the config file(s)
|
|
554
|
+
* This array should roughly be in the order of most global to most local.
|
|
555
|
+
*/
|
|
556
|
+
function findConfigPaths() {
|
|
557
|
+
const paths = dirs.map(dir => join(dir, 'config.json'));
|
|
558
|
+
if (process.env.AXIUM_CONFIG)
|
|
559
|
+
paths.push(process.env.AXIUM_CONFIG);
|
|
560
|
+
return paths;
|
|
561
|
+
}
|
|
562
|
+
if (process.env.AXIUM_CONFIG)
|
|
563
|
+
await loadConfig(process.env.AXIUM_CONFIG);
|
|
564
|
+
|
|
565
|
+
const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH'];
|
|
566
|
+
|
|
567
|
+
var version = "0.19.0";
|
|
568
|
+
var pkg = {
|
|
569
|
+
version: version};
|
|
570
|
+
|
|
571
|
+
const User = object({
|
|
572
|
+
id: uuid(),
|
|
573
|
+
name: string().min(1, 'Name is required').max(255, 'Name is too long'),
|
|
574
|
+
email: email(),
|
|
575
|
+
emailVerified: date().nullable().optional(),
|
|
576
|
+
image: url().nullable().optional(),
|
|
577
|
+
preferences: record(string(), any()),
|
|
578
|
+
roles: array(string()),
|
|
579
|
+
registeredAt: date(),
|
|
580
|
+
});
|
|
581
|
+
const userPublicFields = ['id', 'image', 'name', 'registeredAt', 'roles'];
|
|
582
|
+
const userProtectedFields = ['email', 'emailVerified', 'preferences'];
|
|
583
|
+
const UserChangeable = User.pick({
|
|
584
|
+
name: true,
|
|
585
|
+
email: true,
|
|
586
|
+
image: true,
|
|
587
|
+
preferences: true,
|
|
588
|
+
}).partial();
|
|
589
|
+
|
|
590
|
+
const sym = Symbol.for('Axium:database');
|
|
591
|
+
let database;
|
|
592
|
+
function connect() {
|
|
593
|
+
if (database)
|
|
594
|
+
return database;
|
|
595
|
+
if (globalThis[sym])
|
|
596
|
+
return (database = globalThis[sym]);
|
|
597
|
+
database = new Kysely({
|
|
598
|
+
dialect: new PostgresDialect({ pool: new pg.Pool(config.db) }),
|
|
599
|
+
});
|
|
600
|
+
globalThis[sym] = database;
|
|
601
|
+
debug('Connected to database!');
|
|
602
|
+
return database;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Select the user with the id from the userId column of a table, placing it in the `user` property.
|
|
606
|
+
*/
|
|
607
|
+
function userFromId(builder) {
|
|
608
|
+
const eb = builder;
|
|
609
|
+
return jsonObjectFrom(eb.selectFrom('users').selectAll().whereRef('id', '=', 'userId'))
|
|
610
|
+
.$notNull()
|
|
611
|
+
.$castTo()
|
|
612
|
+
.as('user');
|
|
613
|
+
}
|
|
614
|
+
/** Shortcut to output a warning if an error is thrown because relation already exists */
|
|
615
|
+
someWarnings([/\w+ "[\w.]+" already exists/, 'already exists.']);
|
|
616
|
+
async function clean(opt) {
|
|
617
|
+
const now = new Date();
|
|
618
|
+
start('Removing expired sessions');
|
|
619
|
+
await database.deleteFrom('sessions').where('sessions.expires', '<', now).execute().then(done);
|
|
620
|
+
start('Removing expired verifications');
|
|
621
|
+
await database.deleteFrom('verifications').where('verifications.expires', '<', now).execute().then(done);
|
|
622
|
+
for (const plugin$1 of plugins) {
|
|
623
|
+
if (!plugin$1.hooks.clean)
|
|
624
|
+
continue;
|
|
625
|
+
plugin(plugin$1.name);
|
|
626
|
+
await plugin$1.hooks.clean(opt);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async function getUser(id) {
|
|
631
|
+
return await database.selectFrom('users').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
|
|
632
|
+
}
|
|
633
|
+
const in30days = () => new Date(Date.now() + 2592000000);
|
|
634
|
+
const in10minutes = () => new Date(Date.now() + 600000);
|
|
635
|
+
async function createSession(userId, elevated = false) {
|
|
636
|
+
const session = {
|
|
637
|
+
id: randomUUID(),
|
|
638
|
+
userId,
|
|
639
|
+
token: randomBytes(64).toString('base64'),
|
|
640
|
+
expires: elevated ? in10minutes() : in30days(),
|
|
641
|
+
elevated,
|
|
642
|
+
created: new Date(),
|
|
643
|
+
};
|
|
644
|
+
await database.insertInto('sessions').values(session).execute();
|
|
645
|
+
return session;
|
|
646
|
+
}
|
|
647
|
+
async function getSessionAndUser(token) {
|
|
648
|
+
const result = await database
|
|
649
|
+
.selectFrom('sessions')
|
|
650
|
+
.selectAll()
|
|
651
|
+
.select(userFromId)
|
|
652
|
+
.where('sessions.token', '=', token)
|
|
653
|
+
.where('sessions.expires', '>', new Date())
|
|
654
|
+
.executeTakeFirstOrThrow();
|
|
655
|
+
if (!result.user)
|
|
656
|
+
throw new Error('Session references non-existing user');
|
|
657
|
+
return result;
|
|
658
|
+
}
|
|
659
|
+
async function getSessions(userId) {
|
|
660
|
+
return await database.selectFrom('sessions').selectAll().where('userId', '=', userId).where('sessions.expires', '>', new Date()).execute();
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Create a verification
|
|
664
|
+
* @param expires How long the token should be valid for in seconds
|
|
665
|
+
*/
|
|
666
|
+
async function createVerification(role, userId, expires) {
|
|
667
|
+
const token = randomBytes(64).toString('base64url');
|
|
668
|
+
const verification = { userId, token, expires: new Date(Date.now() + expires * 1000), role };
|
|
669
|
+
await database.insertInto('verifications').values(verification).executeTakeFirstOrThrow();
|
|
670
|
+
setTimeout(() => {
|
|
671
|
+
void database.deleteFrom('verifications').where('verifications.token', '=', verification.token).execute();
|
|
672
|
+
}, expires * 1000);
|
|
673
|
+
return verification;
|
|
674
|
+
}
|
|
675
|
+
async function useVerification(role, userId, token) {
|
|
676
|
+
const query = database
|
|
677
|
+
.deleteFrom('verifications')
|
|
678
|
+
.where('verifications.token', '=', token)
|
|
679
|
+
.where('verifications.userId', '=', userId)
|
|
680
|
+
.where('verifications.role', '=', role);
|
|
681
|
+
return await query.returningAll().executeTakeFirst();
|
|
682
|
+
}
|
|
683
|
+
async function getPasskey(id) {
|
|
684
|
+
return await database.selectFrom('passkeys').selectAll().where('id', '=', id).executeTakeFirstOrThrow();
|
|
685
|
+
}
|
|
686
|
+
async function createPasskey(passkey) {
|
|
687
|
+
const result = await database.insertInto('passkeys').values(passkey).returningAll().executeTakeFirstOrThrow();
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
async function getPasskeysByUserId(userId) {
|
|
691
|
+
return await database.selectFrom('passkeys').selectAll().where('userId', '=', userId).execute();
|
|
692
|
+
}
|
|
693
|
+
async function checkAuthForUser(event, userId, sensitive = false) {
|
|
694
|
+
const token = getToken(event, sensitive);
|
|
695
|
+
if (!token)
|
|
696
|
+
throw error(401, 'Missing token');
|
|
697
|
+
const session = await getSessionAndUser(token).catch(withError('Invalid or expired session', 401));
|
|
698
|
+
if (session.userId !== userId) {
|
|
699
|
+
if (!session.user?.isAdmin)
|
|
700
|
+
error(403, 'User ID mismatch');
|
|
701
|
+
// Admins are allowed to manage other users.
|
|
702
|
+
const accessor = session.user;
|
|
703
|
+
session.user = await getUser(userId).catch(withError('Target user not found', 404));
|
|
704
|
+
return Object.assign(session, { accessor });
|
|
705
|
+
}
|
|
706
|
+
if (!session.elevated && sensitive)
|
|
707
|
+
error(403, 'This token can not be used for sensitive actions');
|
|
708
|
+
return Object.assign(session, { accessor: session.user });
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function isResponseError(e) {
|
|
712
|
+
return e instanceof Error && e.name === 'ResponseError' && typeof e.status === 'number';
|
|
713
|
+
}
|
|
714
|
+
function error(status, message) {
|
|
715
|
+
const error = Object.assign(new Error(message), { status });
|
|
716
|
+
error.name = 'ResponseError';
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
function isRedirect(e) {
|
|
720
|
+
return typeof e === 'object' && e !== null && 'location' in e && 'status' in e;
|
|
721
|
+
}
|
|
722
|
+
function json(data, init) {
|
|
723
|
+
const response = Response.json(data, init);
|
|
724
|
+
if (!response.headers.has('content-length')) {
|
|
725
|
+
response.headers.set('content-length', JSON.stringify(data).length.toString());
|
|
726
|
+
}
|
|
727
|
+
return response;
|
|
728
|
+
}
|
|
729
|
+
async function parseBody(event, schema) {
|
|
730
|
+
const contentType = event.request.headers.get('content-type');
|
|
731
|
+
if (!contentType || !contentType.includes('application/json'))
|
|
732
|
+
error(415, 'Invalid content type');
|
|
733
|
+
const body = await event.request.json().catch(() => error(415, 'Invalid JSON'));
|
|
734
|
+
try {
|
|
735
|
+
return schema.parse(body);
|
|
736
|
+
}
|
|
737
|
+
catch (e) {
|
|
738
|
+
error(400, prettifyError(e));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function getToken(event, sensitive = false) {
|
|
742
|
+
const header_token = event.request.headers.get('Authorization')?.replace('Bearer ', '');
|
|
743
|
+
if (header_token)
|
|
744
|
+
return header_token;
|
|
745
|
+
if (config.debug || config.api.cookie_auth) {
|
|
746
|
+
return event.cookies.get(sensitive ? 'elevated_token' : 'session_token');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
async function createSessionData(userId, elevated = false) {
|
|
750
|
+
const { token, expires } = await createSession(userId, elevated);
|
|
751
|
+
const response = json({ userId, token: elevated ? '[[redacted:elevated]]' : token }, { status: 201 });
|
|
752
|
+
const cookies = serialize(elevated ? 'elevated_token' : 'session_token', token, {
|
|
753
|
+
httpOnly: true,
|
|
754
|
+
path: '/',
|
|
755
|
+
expires,
|
|
756
|
+
secure: config.auth.secure_cookies,
|
|
757
|
+
sameSite: 'lax',
|
|
758
|
+
});
|
|
759
|
+
response.headers.set('Set-Cookie', cookies);
|
|
760
|
+
return response;
|
|
761
|
+
}
|
|
762
|
+
function stripUser(user, includeProtected = false) {
|
|
763
|
+
return pick(user, ...userPublicFields, ...(includeProtected ? userProtectedFields : []));
|
|
764
|
+
}
|
|
765
|
+
function withError(text, code = 500) {
|
|
766
|
+
return function (e) {
|
|
767
|
+
if (e.name == 'ResponseError')
|
|
768
|
+
throw e;
|
|
769
|
+
error(code, text + (config.debug && e.message ? `: ${e.message}` : ''));
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
async function handleAPIRequest(event, route) {
|
|
773
|
+
const method = event.request.method;
|
|
774
|
+
const _warnings = [];
|
|
775
|
+
if (route.api && !event.request.headers.get('Accept')?.includes('application/json')) {
|
|
776
|
+
_warnings.push('Only application/json is supported');
|
|
777
|
+
event.request.headers.set('Accept', 'application/json');
|
|
778
|
+
}
|
|
779
|
+
for (const [key, type] of Object.entries(route.params || {})) {
|
|
780
|
+
if (!type)
|
|
781
|
+
continue;
|
|
782
|
+
try {
|
|
783
|
+
event.params[key] = type.parse(event.params[key]);
|
|
784
|
+
}
|
|
785
|
+
catch (e) {
|
|
786
|
+
error(400, `Invalid parameter: ${prettifyError(e)}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (typeof route[method] != 'function')
|
|
790
|
+
error(405, `Method ${method} not allowed for ${route.path}`);
|
|
791
|
+
const result = await route[method](event);
|
|
792
|
+
if (result instanceof Response)
|
|
793
|
+
return result;
|
|
794
|
+
result._warnings ||= [];
|
|
795
|
+
result._warnings.push(..._warnings);
|
|
796
|
+
return json(result);
|
|
797
|
+
}
|
|
798
|
+
function handleResponseError(e) {
|
|
799
|
+
if (isResponseError(e))
|
|
800
|
+
return json({ message: e.message }, { status: e.status });
|
|
801
|
+
if (isRedirect(e))
|
|
802
|
+
return Response.redirect(e.location, e.status);
|
|
803
|
+
console.error(e);
|
|
804
|
+
return json({ message: 'Internal Error' + (config.debug ? ': ' + e.message : '') }, { status: 500 });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const apps = _unique('apps', new Map());
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* @internal
|
|
811
|
+
*/
|
|
812
|
+
const routes = _unique('routes', new Map());
|
|
813
|
+
/**
|
|
814
|
+
* @category Plugin API
|
|
815
|
+
*/
|
|
816
|
+
function addRoute(opt) {
|
|
817
|
+
const route = { ...opt, server: !('page' in opt) };
|
|
818
|
+
if (!route.path.startsWith('/')) {
|
|
819
|
+
throw new Error(`Route path must start with a slash: ${route.path}`);
|
|
820
|
+
}
|
|
821
|
+
if (route.path.startsWith('/api/'))
|
|
822
|
+
route.api = true;
|
|
823
|
+
if (route.api && !route.server)
|
|
824
|
+
throw new Error(`API routes cannot have a client page: ${route.path}`);
|
|
825
|
+
routes.set(route.path, route);
|
|
826
|
+
output.debug('Added route: ' + route.path);
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Resolve a request URL into a route.
|
|
830
|
+
* This handles parsing of parameters in the URL.
|
|
831
|
+
*/
|
|
832
|
+
function resolveRoute(event) {
|
|
833
|
+
const { pathname } = event.url;
|
|
834
|
+
if (routes.has(pathname) && !pathname.split('/').some(p => p.startsWith(':')))
|
|
835
|
+
return routes.get(pathname);
|
|
836
|
+
// Otherwise we must have a parameterized route
|
|
837
|
+
_routes: for (const route of routes.values()) {
|
|
838
|
+
const params = {};
|
|
839
|
+
// Split the path and route into parts, zipped together
|
|
840
|
+
const pathParts = pathname.split('/').filter(Boolean);
|
|
841
|
+
// Skips routes in disabled apps
|
|
842
|
+
if (apps.has(pathParts[0]) && config.apps.disabled.includes(pathParts[0]))
|
|
843
|
+
continue;
|
|
844
|
+
for (const routePart of route.path.split('/').filter(Boolean)) {
|
|
845
|
+
const pathPart = pathParts.shift();
|
|
846
|
+
if (!pathPart)
|
|
847
|
+
continue _routes;
|
|
848
|
+
if (pathPart == routePart)
|
|
849
|
+
continue;
|
|
850
|
+
if (!routePart.startsWith(':'))
|
|
851
|
+
continue _routes;
|
|
852
|
+
params[routePart.slice(1)] = pathPart;
|
|
853
|
+
}
|
|
854
|
+
// we didn't find a match, since an exact match would have been found already
|
|
855
|
+
if (pathParts.length || !Object.keys(params).length)
|
|
856
|
+
continue;
|
|
857
|
+
event.params = params;
|
|
858
|
+
return route;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
addRoute({
|
|
863
|
+
path: '/api/metadata',
|
|
864
|
+
async GET() {
|
|
865
|
+
if (config.api.disable_metadata)
|
|
866
|
+
error(401, 'API metadata is disabled');
|
|
867
|
+
return {
|
|
868
|
+
version: pkg.version,
|
|
869
|
+
routes: Object.fromEntries(routes
|
|
870
|
+
.entries()
|
|
871
|
+
.filter(([path]) => path.startsWith('/api/'))
|
|
872
|
+
.map(([path, route]) => [
|
|
873
|
+
path,
|
|
874
|
+
{
|
|
875
|
+
params: Object.fromEntries(Object.entries(route.params || {}).map(([key, type]) => [key, type ? type.def.type : null])),
|
|
876
|
+
methods: requestMethods.filter(m => m in route),
|
|
877
|
+
},
|
|
878
|
+
])),
|
|
879
|
+
plugins: Object.fromEntries(plugins.values().map(plugin => [plugin.name, plugin.version])),
|
|
880
|
+
};
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
addRoute({
|
|
885
|
+
path: '/api/passkeys/:id',
|
|
886
|
+
params: {
|
|
887
|
+
id: string(),
|
|
888
|
+
},
|
|
889
|
+
async GET(event) {
|
|
890
|
+
const passkey = await getPasskey(event.params.id);
|
|
891
|
+
await checkAuthForUser(event, passkey.userId);
|
|
892
|
+
return omit(passkey, 'counter', 'publicKey');
|
|
893
|
+
},
|
|
894
|
+
async PATCH(event) {
|
|
895
|
+
const body = await parseBody(event, PasskeyChangeable);
|
|
896
|
+
const passkey = await getPasskey(event.params.id);
|
|
897
|
+
await checkAuthForUser(event, passkey.userId);
|
|
898
|
+
const result = await database
|
|
899
|
+
.updateTable('passkeys')
|
|
900
|
+
.set(body)
|
|
901
|
+
.where('id', '=', passkey.id)
|
|
902
|
+
.returningAll()
|
|
903
|
+
.executeTakeFirstOrThrow()
|
|
904
|
+
.catch(withError('Could not update passkey'));
|
|
905
|
+
return omit(result, 'counter', 'publicKey');
|
|
906
|
+
},
|
|
907
|
+
async DELETE(event) {
|
|
908
|
+
const passkey = await getPasskey(event.params.id);
|
|
909
|
+
await checkAuthForUser(event, passkey.userId);
|
|
910
|
+
const { count } = await database
|
|
911
|
+
.selectFrom('passkeys')
|
|
912
|
+
.select(database.fn.countAll().as('count'))
|
|
913
|
+
.where('userId', '=', passkey.userId)
|
|
914
|
+
.executeTakeFirstOrThrow();
|
|
915
|
+
if (Number(count) <= 1)
|
|
916
|
+
error(409, 'At least one passkey is required');
|
|
917
|
+
const result = await database
|
|
918
|
+
.deleteFrom('passkeys')
|
|
919
|
+
.where('id', '=', passkey.id)
|
|
920
|
+
.returningAll()
|
|
921
|
+
.executeTakeFirstOrThrow()
|
|
922
|
+
.catch(withError('Could not delete passkey'));
|
|
923
|
+
return omit(result, 'counter', 'publicKey');
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Map of user ID => challenge
|
|
928
|
+
const registrations$1 = new Map();
|
|
929
|
+
async function OPTIONS(event) {
|
|
930
|
+
if (!config.allow_new_users)
|
|
931
|
+
error(409, 'New user registration is disabled');
|
|
932
|
+
const { name, email: email$1 } = await parseBody(event, object({ name: string().optional(), email: email().optional() }));
|
|
933
|
+
const userId = randomUUID();
|
|
934
|
+
const user = await getUser(userId).catch(() => null);
|
|
935
|
+
if (user)
|
|
936
|
+
error(409, 'Generated UUID is already in use, please retry.');
|
|
937
|
+
const options = await generateRegistrationOptions({
|
|
938
|
+
rpName: config.auth.rp_name,
|
|
939
|
+
rpID: config.auth.rp_id,
|
|
940
|
+
userName: email$1 ?? userId,
|
|
941
|
+
userDisplayName: name,
|
|
942
|
+
attestationType: 'none',
|
|
943
|
+
excludeCredentials: [],
|
|
944
|
+
authenticatorSelection: {
|
|
945
|
+
residentKey: 'preferred',
|
|
946
|
+
userVerification: 'preferred',
|
|
947
|
+
authenticatorAttachment: 'platform',
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
registrations$1.set(userId, options.challenge);
|
|
951
|
+
return { userId, options };
|
|
952
|
+
}
|
|
953
|
+
async function POST(event) {
|
|
954
|
+
if (!config.allow_new_users)
|
|
955
|
+
error(409, 'New user registration is disabled');
|
|
956
|
+
const { userId, email, name, response } = await parseBody(event, APIUserRegistration);
|
|
957
|
+
const existing = await database.selectFrom('users').selectAll().where('email', '=', email.toLowerCase()).executeTakeFirst();
|
|
958
|
+
if (existing)
|
|
959
|
+
error(409, 'Email already in use');
|
|
960
|
+
const expectedChallenge = registrations$1.get(userId);
|
|
961
|
+
if (!expectedChallenge)
|
|
962
|
+
error(404, 'No registration challenge found for this user');
|
|
963
|
+
registrations$1.delete(userId);
|
|
964
|
+
const { verified, registrationInfo } = await verifyRegistrationResponse({
|
|
965
|
+
response,
|
|
966
|
+
expectedChallenge,
|
|
967
|
+
expectedOrigin: config.auth.origin,
|
|
968
|
+
}).catch(() => error(400, 'Verification failed'));
|
|
969
|
+
if (!verified || !registrationInfo)
|
|
970
|
+
error(401, 'Verification failed');
|
|
971
|
+
await database
|
|
972
|
+
.insertInto('users')
|
|
973
|
+
.values({ id: userId, name, email: email.toLowerCase() })
|
|
974
|
+
.executeTakeFirstOrThrow()
|
|
975
|
+
.catch(withError('Failed to create user'));
|
|
976
|
+
await createPasskey({
|
|
977
|
+
transports: [],
|
|
978
|
+
...registrationInfo.credential,
|
|
979
|
+
userId,
|
|
980
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
981
|
+
backedUp: registrationInfo.credentialBackedUp,
|
|
982
|
+
}).catch(withError('Failed to create passkey', 500));
|
|
983
|
+
return await createSessionData(userId);
|
|
984
|
+
}
|
|
985
|
+
addRoute({
|
|
986
|
+
path: '/api/register',
|
|
987
|
+
params: {},
|
|
988
|
+
OPTIONS,
|
|
989
|
+
POST,
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
addRoute({
|
|
993
|
+
path: '/api/session',
|
|
994
|
+
async GET(event) {
|
|
995
|
+
const token = getToken(event);
|
|
996
|
+
if (!token)
|
|
997
|
+
error(401, 'Missing token');
|
|
998
|
+
const result = await getSessionAndUser(token).catch(withError('Invalid session', 400));
|
|
999
|
+
return {
|
|
1000
|
+
...omit(result, 'token'),
|
|
1001
|
+
user: stripUser(result.user, true),
|
|
1002
|
+
};
|
|
1003
|
+
},
|
|
1004
|
+
async DELETE(event) {
|
|
1005
|
+
const token = getToken(event);
|
|
1006
|
+
if (!token)
|
|
1007
|
+
error(401, 'Missing token');
|
|
1008
|
+
const result = await database
|
|
1009
|
+
.deleteFrom('sessions')
|
|
1010
|
+
.where('sessions.token', '=', token)
|
|
1011
|
+
.returningAll()
|
|
1012
|
+
.executeTakeFirstOrThrow()
|
|
1013
|
+
.catch((e) => (e.message == 'no result' ? error(404, 'Session does not exist') : error(400, 'Invalid session')));
|
|
1014
|
+
return omit(result, 'token');
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
const challenges = new Map();
|
|
1019
|
+
const params = { id: uuid() };
|
|
1020
|
+
/**
|
|
1021
|
+
* Resolve a user's UUID using their email (in the future this might also include handles)
|
|
1022
|
+
*/
|
|
1023
|
+
addRoute({
|
|
1024
|
+
path: '/api/user_id',
|
|
1025
|
+
async POST(event) {
|
|
1026
|
+
const { value } = await parseBody(event, object({ using: literal('email'), value: email() }));
|
|
1027
|
+
const { id } = await database
|
|
1028
|
+
.selectFrom('users')
|
|
1029
|
+
.select('id')
|
|
1030
|
+
.where('email', '=', value)
|
|
1031
|
+
.executeTakeFirstOrThrow()
|
|
1032
|
+
.catch(withError('User not found', 404));
|
|
1033
|
+
return { id };
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
addRoute({
|
|
1037
|
+
path: '/api/users/:id',
|
|
1038
|
+
params,
|
|
1039
|
+
async GET(event) {
|
|
1040
|
+
const userId = event.params.id;
|
|
1041
|
+
const auth = await checkAuthForUser(event, userId).catch(() => null);
|
|
1042
|
+
const user = auth?.user || (await getUser(userId).catch(withError('User does not exist', 404)));
|
|
1043
|
+
return stripUser(user, !!auth);
|
|
1044
|
+
},
|
|
1045
|
+
async PATCH(event) {
|
|
1046
|
+
const userId = event.params.id;
|
|
1047
|
+
const body = await parseBody(event, UserChangeable);
|
|
1048
|
+
await checkAuthForUser(event, userId);
|
|
1049
|
+
if ('email' in body)
|
|
1050
|
+
body.emailVerified = null;
|
|
1051
|
+
const result = await database
|
|
1052
|
+
.updateTable('users')
|
|
1053
|
+
.set(body)
|
|
1054
|
+
.where('id', '=', userId)
|
|
1055
|
+
.returningAll()
|
|
1056
|
+
.executeTakeFirstOrThrow()
|
|
1057
|
+
.catch(withError('Failed to update user'));
|
|
1058
|
+
return stripUser(result, true);
|
|
1059
|
+
},
|
|
1060
|
+
async DELETE(event) {
|
|
1061
|
+
const userId = event.params.id;
|
|
1062
|
+
await checkAuthForUser(event, userId, true);
|
|
1063
|
+
const result = await database
|
|
1064
|
+
.deleteFrom('users')
|
|
1065
|
+
.where('id', '=', userId)
|
|
1066
|
+
.returningAll()
|
|
1067
|
+
.executeTakeFirstOrThrow()
|
|
1068
|
+
.catch(withError('Failed to delete user'));
|
|
1069
|
+
return result;
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
addRoute({
|
|
1073
|
+
path: '/api/users/:id/full',
|
|
1074
|
+
params,
|
|
1075
|
+
async GET(event) {
|
|
1076
|
+
const userId = event.params.id;
|
|
1077
|
+
const { user } = await checkAuthForUser(event, userId);
|
|
1078
|
+
const sessions = await getSessions(userId);
|
|
1079
|
+
return {
|
|
1080
|
+
...stripUser(user, true),
|
|
1081
|
+
sessions: sessions.map(s => omit(s, 'token')),
|
|
1082
|
+
};
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
addRoute({
|
|
1086
|
+
path: '/api/users/:id/auth',
|
|
1087
|
+
params,
|
|
1088
|
+
async OPTIONS(event) {
|
|
1089
|
+
const userId = event.params.id;
|
|
1090
|
+
const { type } = await parseBody(event, UserAuthOptions);
|
|
1091
|
+
await getUser(userId).catch(withError('User does not exist', 404));
|
|
1092
|
+
const passkeys = await getPasskeysByUserId(userId);
|
|
1093
|
+
if (!passkeys)
|
|
1094
|
+
error(409, 'No passkeys exists for this user');
|
|
1095
|
+
const options = await webauthn.generateAuthenticationOptions({
|
|
1096
|
+
rpID: config.auth.rp_id,
|
|
1097
|
+
allowCredentials: passkeys.map(passkey => pick(passkey, 'id', 'transports')),
|
|
1098
|
+
});
|
|
1099
|
+
challenges.set(userId, { data: options.challenge, type });
|
|
1100
|
+
return options;
|
|
1101
|
+
},
|
|
1102
|
+
async POST(event) {
|
|
1103
|
+
const userId = event.params.id;
|
|
1104
|
+
const response = await parseBody(event, PasskeyAuthenticationResponse);
|
|
1105
|
+
const auth = challenges.get(userId);
|
|
1106
|
+
if (!auth)
|
|
1107
|
+
error(404, 'No challenge');
|
|
1108
|
+
const { data: expectedChallenge, type } = auth;
|
|
1109
|
+
challenges.delete(userId);
|
|
1110
|
+
const passkey = await getPasskey(response.id).catch(withError('Passkey does not exist', 404));
|
|
1111
|
+
if (passkey.userId !== userId)
|
|
1112
|
+
error(403, 'Passkey does not belong to this user');
|
|
1113
|
+
const { verified } = await webauthn
|
|
1114
|
+
.verifyAuthenticationResponse({
|
|
1115
|
+
response,
|
|
1116
|
+
credential: passkey,
|
|
1117
|
+
expectedChallenge,
|
|
1118
|
+
expectedOrigin: config.auth.origin,
|
|
1119
|
+
expectedRPID: config.auth.rp_id,
|
|
1120
|
+
})
|
|
1121
|
+
.catch(withError('Verification failed', 400));
|
|
1122
|
+
if (!verified)
|
|
1123
|
+
error(401, 'Verification failed');
|
|
1124
|
+
switch (type) {
|
|
1125
|
+
case 'login':
|
|
1126
|
+
return await createSessionData(userId);
|
|
1127
|
+
case 'action':
|
|
1128
|
+
if ((Date.now() - passkey.createdAt.getTime()) / 60_000 < config.auth.passkey_probation)
|
|
1129
|
+
error(403, 'You can not authorize sensitive actions with a newly created passkey');
|
|
1130
|
+
return await createSessionData(userId, true);
|
|
1131
|
+
}
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
// Map of user ID => challenge
|
|
1135
|
+
const registrations = new Map();
|
|
1136
|
+
addRoute({
|
|
1137
|
+
path: '/api/users/:id/passkeys',
|
|
1138
|
+
params,
|
|
1139
|
+
/**
|
|
1140
|
+
* Get passkey registration options for a user.
|
|
1141
|
+
*/
|
|
1142
|
+
async OPTIONS(event) {
|
|
1143
|
+
const userId = event.params.id;
|
|
1144
|
+
const existing = await getPasskeysByUserId(userId);
|
|
1145
|
+
const { user } = await checkAuthForUser(event, userId);
|
|
1146
|
+
const options = await webauthn.generateRegistrationOptions({
|
|
1147
|
+
rpName: config.auth.rp_name,
|
|
1148
|
+
rpID: config.auth.rp_id,
|
|
1149
|
+
userName: userId,
|
|
1150
|
+
userDisplayName: user.email,
|
|
1151
|
+
attestationType: 'none',
|
|
1152
|
+
excludeCredentials: existing.map(passkey => pick(passkey, 'id', 'transports')),
|
|
1153
|
+
authenticatorSelection: {
|
|
1154
|
+
residentKey: 'preferred',
|
|
1155
|
+
userVerification: 'preferred',
|
|
1156
|
+
authenticatorAttachment: 'platform',
|
|
1157
|
+
},
|
|
1158
|
+
});
|
|
1159
|
+
registrations.set(userId, options.challenge);
|
|
1160
|
+
return options;
|
|
1161
|
+
},
|
|
1162
|
+
/**
|
|
1163
|
+
* Get passkeys for a user.
|
|
1164
|
+
*/
|
|
1165
|
+
async GET(event) {
|
|
1166
|
+
const userId = event.params.id;
|
|
1167
|
+
await checkAuthForUser(event, userId);
|
|
1168
|
+
const passkeys = await getPasskeysByUserId(userId);
|
|
1169
|
+
return passkeys.map(p => omit(p, 'publicKey', 'counter'));
|
|
1170
|
+
},
|
|
1171
|
+
/**
|
|
1172
|
+
* Register a new passkey for an existing user.
|
|
1173
|
+
*/
|
|
1174
|
+
async PUT(event) {
|
|
1175
|
+
const userId = event.params.id;
|
|
1176
|
+
const response = await parseBody(event, PasskeyRegistration);
|
|
1177
|
+
await checkAuthForUser(event, userId);
|
|
1178
|
+
const expectedChallenge = registrations.get(userId);
|
|
1179
|
+
if (!expectedChallenge)
|
|
1180
|
+
error(404, 'No registration challenge found for this user');
|
|
1181
|
+
registrations.delete(userId);
|
|
1182
|
+
const { verified, registrationInfo } = await webauthn
|
|
1183
|
+
.verifyRegistrationResponse({
|
|
1184
|
+
response,
|
|
1185
|
+
expectedChallenge,
|
|
1186
|
+
expectedOrigin: config.auth.origin,
|
|
1187
|
+
})
|
|
1188
|
+
.catch(withError('Verification failed', 400));
|
|
1189
|
+
if (!verified || !registrationInfo)
|
|
1190
|
+
error(401, 'Verification failed');
|
|
1191
|
+
const passkey = await createPasskey({
|
|
1192
|
+
transports: [],
|
|
1193
|
+
...registrationInfo.credential,
|
|
1194
|
+
userId,
|
|
1195
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
1196
|
+
backedUp: registrationInfo.credentialBackedUp,
|
|
1197
|
+
}).catch(withError('Failed to create passkey'));
|
|
1198
|
+
return omit(passkey, 'publicKey', 'counter');
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
addRoute({
|
|
1202
|
+
path: '/api/users/:id/sessions',
|
|
1203
|
+
params,
|
|
1204
|
+
async GET(event) {
|
|
1205
|
+
const userId = event.params.id;
|
|
1206
|
+
await checkAuthForUser(event, userId);
|
|
1207
|
+
return (await getSessions(userId).catch(e => error(503, 'Failed to get sessions' + (config.debug ? ': ' + e : '')))).map(s => omit(s, 'token'));
|
|
1208
|
+
},
|
|
1209
|
+
async DELETE(event) {
|
|
1210
|
+
const userId = event.params.id;
|
|
1211
|
+
const body = await parseBody(event, LogoutSessions);
|
|
1212
|
+
await checkAuthForUser(event, userId, body.confirm_all);
|
|
1213
|
+
if (!body.confirm_all && !Array.isArray(body.id))
|
|
1214
|
+
error(400, 'Invalid request body');
|
|
1215
|
+
const query = body.confirm_all ? database.deleteFrom('sessions') : database.deleteFrom('sessions').where('sessions.id', 'in', body.id);
|
|
1216
|
+
const result = await query
|
|
1217
|
+
.where('sessions.userId', '=', userId)
|
|
1218
|
+
.returningAll()
|
|
1219
|
+
.execute()
|
|
1220
|
+
.catch(withError('Failed to delete one or more sessions'));
|
|
1221
|
+
return result.map(s => omit(s, 'token'));
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
addRoute({
|
|
1225
|
+
path: '/api/users/:id/verify_email',
|
|
1226
|
+
params,
|
|
1227
|
+
async OPTIONS(event) {
|
|
1228
|
+
const userId = event.params.id;
|
|
1229
|
+
if (!config.auth.email_verification)
|
|
1230
|
+
return { enabled: false };
|
|
1231
|
+
await checkAuthForUser(event, userId);
|
|
1232
|
+
if (!config.auth.email_verification)
|
|
1233
|
+
return { enabled: false };
|
|
1234
|
+
return { enabled: true };
|
|
1235
|
+
},
|
|
1236
|
+
async GET(event) {
|
|
1237
|
+
const userId = event.params.id;
|
|
1238
|
+
const { user } = await checkAuthForUser(event, userId);
|
|
1239
|
+
if (user.emailVerified)
|
|
1240
|
+
error(409, 'Email already verified');
|
|
1241
|
+
const verification = await createVerification('verify_email', userId, config.auth.verification_timeout * 60);
|
|
1242
|
+
return omit(verification, 'token', 'role');
|
|
1243
|
+
},
|
|
1244
|
+
async POST(event) {
|
|
1245
|
+
const userId = event.params.id;
|
|
1246
|
+
const { token } = await parseBody(event, object({ token: string() }));
|
|
1247
|
+
const { user } = await checkAuthForUser(event, userId);
|
|
1248
|
+
if (user.emailVerified)
|
|
1249
|
+
error(409, 'Email already verified');
|
|
1250
|
+
await useVerification('verify_email', userId, token).catch(withError('Invalid or expired verification token', 400));
|
|
1251
|
+
return {};
|
|
1252
|
+
},
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Perform initial setup for when the server is serving web pages.
|
|
1257
|
+
*/
|
|
1258
|
+
async function init() {
|
|
1259
|
+
logger.attach(createWriteStream(join(dirs.at(-1), 'server.log')), { output: allLogLevels });
|
|
1260
|
+
await loadDefaultConfigs();
|
|
1261
|
+
connect();
|
|
1262
|
+
await clean({});
|
|
1263
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
1264
|
+
process.on('beforeExit', () => database.destroy());
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
let template = null;
|
|
1268
|
+
function fillSvelteKitTemplate({ head, body }, env = {}, nonce = '') {
|
|
1269
|
+
template ||= readFileSync(config.web.template, 'utf-8');
|
|
1270
|
+
return (template
|
|
1271
|
+
.replaceAll('%sveltekit.head%', head)
|
|
1272
|
+
.replaceAll('%sveltekit.body%', body)
|
|
1273
|
+
.replaceAll('%sveltekit.assets%', config.web.assets)
|
|
1274
|
+
// Unused for now.
|
|
1275
|
+
.replaceAll('%sveltekit.nonce%', nonce)
|
|
1276
|
+
.replace(/%sveltekit\.env\.([^%]+)%/g, (_match, key) => env[key] ?? ''));
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* @internal
|
|
1280
|
+
*/
|
|
1281
|
+
async function handleSvelteKit({ event, resolve, }) {
|
|
1282
|
+
const route = resolveRoute(event);
|
|
1283
|
+
if (!route && event.url.pathname === '/' && config.debug)
|
|
1284
|
+
return new Response(null, { status: 303, headers: { Location: '/_axium/default' } });
|
|
1285
|
+
if (config.debug)
|
|
1286
|
+
console.log(styleText('blueBright', event.request.method.padEnd(7)), route ? route.path : event.url.pathname);
|
|
1287
|
+
if (!route)
|
|
1288
|
+
return await resolve(event).catch(handleResponseError);
|
|
1289
|
+
if (route.server == true) {
|
|
1290
|
+
if (route.api)
|
|
1291
|
+
return await handleAPIRequest(event, route).catch(handleResponseError);
|
|
1292
|
+
const run = route[event.request.method];
|
|
1293
|
+
if (typeof run !== 'function') {
|
|
1294
|
+
error(405, `Method ${event.request.method} not allowed for ${route.path}`);
|
|
1295
|
+
}
|
|
1296
|
+
try {
|
|
1297
|
+
const result = await run(event);
|
|
1298
|
+
if (result instanceof Response)
|
|
1299
|
+
return result;
|
|
1300
|
+
return json(result);
|
|
1301
|
+
}
|
|
1302
|
+
catch (e) {
|
|
1303
|
+
return handleResponseError(e);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
await route.load?.(event);
|
|
1307
|
+
const body = fillSvelteKitTemplate(render(route.page));
|
|
1308
|
+
return new Response(body, {
|
|
1309
|
+
headers: config.web.disable_cache
|
|
1310
|
+
? {
|
|
1311
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
1312
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
1313
|
+
Pragma: 'no-cache',
|
|
1314
|
+
Expires: '0',
|
|
1315
|
+
}
|
|
1316
|
+
: {},
|
|
1317
|
+
status: 200,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
await init();
|
|
1322
|
+
|
|
1323
|
+
export { handleSvelteKit as handle };
|
|
1324
|
+
//# sourceMappingURL=hooks.server-PyYsuy1-.js.map
|