@server/next 0.40.4 → 0.42.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/index.d.ts +150 -103
- package/index.js +433 -284
- package/package.json +15 -16
- package/readme.md +6 -5
package/index.js
CHANGED
|
@@ -43,20 +43,17 @@ ServerError_default.extend({
|
|
|
43
43
|
status: 400,
|
|
44
44
|
message: "The route param '{param}' tries to climb the path ('{value}'). If this route legitimately receives paths, set security: { traversalProtection: false }"
|
|
45
45
|
},
|
|
46
|
-
NO_STORE: "You need a 'store' to write 'ctx.session'",
|
|
47
|
-
NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
|
|
48
|
-
NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
|
|
49
46
|
AUTH_ARGON_NEEDED: "Argon2 is needed for the auth module, please install it with 'npm i argon2'",
|
|
50
47
|
AUTH_INVALID_TOKEN: { status: 401, message: "Invalid Authorization token" },
|
|
51
|
-
|
|
48
|
+
AUTH_NO_CODE: {
|
|
49
|
+
status: 400,
|
|
50
|
+
message: "Missing the OAuth 'code' in the request body"
|
|
51
|
+
},
|
|
52
|
+
SESSION_JWT: "The `jwt` strategy is stateless, so there is no `ctx.session` (tried '{key}'). Use the `token` strategy for server-side sessions, or `cookie` for browsers",
|
|
52
53
|
AUTH_INVALID_HEADER: {
|
|
53
54
|
status: 401,
|
|
54
55
|
message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
|
|
55
56
|
},
|
|
56
|
-
AUTH_INVALID_STRATEGY: {
|
|
57
|
-
status: 401,
|
|
58
|
-
message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
|
|
59
|
-
},
|
|
60
57
|
AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
|
|
61
58
|
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.providers'",
|
|
62
59
|
AUTH_INVALID_PROVIDER: {
|
|
@@ -220,7 +217,7 @@ function human(bytes) {
|
|
|
220
217
|
return `${rounded}${UNITS[i]}`;
|
|
221
218
|
}
|
|
222
219
|
var tooLarge = (max) => new StatusError(
|
|
223
|
-
`Request body exceeds the ${human(max)} limit. Raise it with
|
|
220
|
+
`Request body exceeds the ${human(max)} limit. Raise it with security: { maxBody: '10mb' }, or maxBody: false to disable it.`,
|
|
224
221
|
413
|
|
225
222
|
);
|
|
226
223
|
|
|
@@ -577,11 +574,9 @@ var sources = /* @__PURE__ */ new WeakMap();
|
|
|
577
574
|
function setBodySource(ctx, source) {
|
|
578
575
|
sources.set(ctx, source);
|
|
579
576
|
}
|
|
580
|
-
async function resolveBody(ctx,
|
|
577
|
+
async function resolveBody(ctx, mode = "parse", max = resolveMax(void 0)) {
|
|
581
578
|
const source = sources.get(ctx);
|
|
582
579
|
if (!source) return void 0;
|
|
583
|
-
const mode = typeof body === "string" ? body : body?.mode ?? "parse";
|
|
584
|
-
const max = resolveMax(typeof body === "object" ? body?.max : void 0);
|
|
585
580
|
const contentType = String(ctx.headers["content-type"] || "");
|
|
586
581
|
const isMultipart = /multipart\/form-data/i.test(contentType);
|
|
587
582
|
const declared = Number(ctx.headers["content-length"]);
|
|
@@ -731,13 +726,20 @@ function clientIp(headers2, opts = {}) {
|
|
|
731
726
|
|
|
732
727
|
// src/helpers/store.ts
|
|
733
728
|
import kv from "polystore";
|
|
734
|
-
function
|
|
729
|
+
function isStore(source) {
|
|
735
730
|
const store = source;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
731
|
+
return Boolean(
|
|
732
|
+
store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function"
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
function toStore(source) {
|
|
736
|
+
if (isStore(source)) return source;
|
|
739
737
|
return kv(source);
|
|
740
738
|
}
|
|
739
|
+
function toStoreExpiring(source, expires) {
|
|
740
|
+
if (isStore(source)) return source;
|
|
741
|
+
return kv(source).expires(expires);
|
|
742
|
+
}
|
|
741
743
|
|
|
742
744
|
// src/helpers/disposition.ts
|
|
743
745
|
var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
@@ -996,50 +998,108 @@ async function verifyJwt(token, secret) {
|
|
|
996
998
|
return payload;
|
|
997
999
|
}
|
|
998
1000
|
|
|
1001
|
+
// src/auth/findSessionId.ts
|
|
1002
|
+
var validateToken = (authorization) => {
|
|
1003
|
+
const [type2, id] = authorization.trim().split(" ");
|
|
1004
|
+
if (type2?.toLowerCase() !== "bearer") {
|
|
1005
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
1006
|
+
}
|
|
1007
|
+
if (id?.length !== 16) {
|
|
1008
|
+
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1009
|
+
}
|
|
1010
|
+
return id;
|
|
1011
|
+
};
|
|
1012
|
+
function findSessionId(ctx) {
|
|
1013
|
+
const strategy = ctx.options.auth?.strategy;
|
|
1014
|
+
if (strategy?.includes("token") && ctx.headers.authorization) {
|
|
1015
|
+
return validateToken(ctx.headers.authorization);
|
|
1016
|
+
}
|
|
1017
|
+
return ctx.cookies.session || void 0;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/middle/session.ts
|
|
1021
|
+
var loaded = /* @__PURE__ */ new WeakMap();
|
|
1022
|
+
function jwtSession() {
|
|
1023
|
+
const target = {};
|
|
1024
|
+
return new Proxy(target, {
|
|
1025
|
+
get(target2, key) {
|
|
1026
|
+
if (typeof key === "symbol" || key === "then") return target2[key];
|
|
1027
|
+
throw ServerError_default.SESSION_JWT({ key: String(key) });
|
|
1028
|
+
},
|
|
1029
|
+
set(target2, key, value) {
|
|
1030
|
+
if (typeof key === "symbol") {
|
|
1031
|
+
target2[key] = value;
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
throw ServerError_default.SESSION_JWT({ key: String(key) });
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
async function session(ctx) {
|
|
1039
|
+
if (ctx.options.auth?.strategy.includes("jwt")) {
|
|
1040
|
+
ctx.session = jwtSession();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const id = findSessionId(ctx);
|
|
1044
|
+
ctx.session = id && await ctx.options.sessions.get(id) || {};
|
|
1045
|
+
loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
|
|
1046
|
+
}
|
|
1047
|
+
|
|
999
1048
|
// src/auth/finishLogin.ts
|
|
1000
|
-
async function finishLogin(ctx, input) {
|
|
1049
|
+
async function finishLogin(ctx, input, opts = {}) {
|
|
1001
1050
|
const settings = ctx.options.auth;
|
|
1002
|
-
const { strategy, onLogin, onUser } = settings;
|
|
1051
|
+
const { strategy, onLogin, onUser, onToken } = settings;
|
|
1003
1052
|
const key = String(input.key);
|
|
1004
1053
|
const auth2 = {
|
|
1005
|
-
id: createId(),
|
|
1006
|
-
strategy,
|
|
1007
|
-
provider: input.provider,
|
|
1008
1054
|
user: key,
|
|
1009
|
-
|
|
1010
|
-
|
|
1055
|
+
provider: input.provider,
|
|
1056
|
+
created: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
1011
1057
|
};
|
|
1012
1058
|
const loginUser = {
|
|
1013
1059
|
...input.user,
|
|
1014
1060
|
provider: input.provider,
|
|
1015
1061
|
strategy
|
|
1016
1062
|
};
|
|
1017
|
-
const existingUser = await settings.
|
|
1063
|
+
const existingUser = await settings.users.get(key) ?? null;
|
|
1018
1064
|
const user = onLogin ? await onLogin(loginUser, existingUser, ctx) : { ...existingUser ?? {}, ...loginUser };
|
|
1019
1065
|
assertUser(user, "onLogin");
|
|
1020
|
-
await settings.
|
|
1021
|
-
if (!strategy.includes("jwt")) {
|
|
1022
|
-
await settings.session.set(auth2.id, auth2, { expires: "1w" });
|
|
1023
|
-
}
|
|
1066
|
+
await settings.users.set(key, user);
|
|
1024
1067
|
if (strategy.includes("jwt")) {
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1068
|
+
const payload = {
|
|
1069
|
+
...await onToken(user, ctx),
|
|
1070
|
+
provider: input.provider
|
|
1071
|
+
};
|
|
1072
|
+
assertUser(payload, "onToken");
|
|
1073
|
+
const token = await signJwt(payload, ctx.options.secret, 7 * 24 * 60 * 60);
|
|
1074
|
+
const exposed = await onUser(payload, ctx);
|
|
1027
1075
|
assertUser(exposed, "onUser");
|
|
1028
1076
|
return status(201).json({ ...exposed, token });
|
|
1029
1077
|
}
|
|
1078
|
+
const prev = loaded.get(ctx);
|
|
1079
|
+
if (prev?.id) await ctx.options.sessions.del(prev.id);
|
|
1080
|
+
const id = createId();
|
|
1081
|
+
Object.assign(ctx.session, auth2);
|
|
1082
|
+
await ctx.options.sessions.set(id, ctx.session);
|
|
1083
|
+
loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
|
|
1030
1084
|
if (strategy.includes("token")) {
|
|
1031
1085
|
const exposed = await onUser(user, ctx);
|
|
1032
1086
|
assertUser(exposed, "onUser");
|
|
1033
|
-
return status(201).json({ ...exposed, token:
|
|
1087
|
+
return status(201).json({ ...exposed, token: id });
|
|
1034
1088
|
}
|
|
1035
1089
|
if (strategy.includes("cookie")) {
|
|
1036
|
-
|
|
1037
|
-
value:
|
|
1090
|
+
const reply = cookies("session", {
|
|
1091
|
+
value: id,
|
|
1038
1092
|
path: "/",
|
|
1039
1093
|
httpOnly: true,
|
|
1040
1094
|
secure: ctx.platform.production,
|
|
1041
1095
|
sameSite: "Lax"
|
|
1042
|
-
})
|
|
1096
|
+
});
|
|
1097
|
+
if (opts.json) {
|
|
1098
|
+
const exposed = await onUser(user, ctx);
|
|
1099
|
+
assertUser(exposed, "onUser");
|
|
1100
|
+
return reply.status(201).json(exposed);
|
|
1101
|
+
}
|
|
1102
|
+
return reply.redirect(settings.redirect);
|
|
1043
1103
|
}
|
|
1044
1104
|
throw new Error("Unknown auth type");
|
|
1045
1105
|
}
|
|
@@ -1125,78 +1185,111 @@ var login = (ctx) => {
|
|
|
1125
1185
|
});
|
|
1126
1186
|
return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
|
|
1127
1187
|
};
|
|
1128
|
-
var
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1188
|
+
var exchange = async (code, redirectUri, user) => {
|
|
1189
|
+
const params = new URLSearchParams({
|
|
1190
|
+
client_id: env.APPLE_ID,
|
|
1191
|
+
client_secret: await clientSecret(),
|
|
1192
|
+
code: code ?? "",
|
|
1193
|
+
grant_type: "authorization_code"
|
|
1194
|
+
});
|
|
1195
|
+
if (redirectUri) params.set("redirect_uri", redirectUri);
|
|
1131
1196
|
const tokenRes = await fetch(TOKEN, {
|
|
1132
1197
|
method: "POST",
|
|
1133
1198
|
headers: {
|
|
1134
1199
|
accept: "application/json",
|
|
1135
1200
|
"content-type": "application/x-www-form-urlencoded"
|
|
1136
1201
|
},
|
|
1137
|
-
body:
|
|
1138
|
-
client_id: env.APPLE_ID,
|
|
1139
|
-
client_secret: await clientSecret(),
|
|
1140
|
-
code: body.code,
|
|
1141
|
-
grant_type: "authorization_code",
|
|
1142
|
-
redirect_uri: `${ctx.url.origin}/auth/callback/apple`
|
|
1143
|
-
})
|
|
1202
|
+
body: params
|
|
1144
1203
|
});
|
|
1145
1204
|
if (!tokenRes.ok) throw new Error("apple: token exchange failed");
|
|
1146
1205
|
const token = await tokenRes.json();
|
|
1147
1206
|
const claims = b64urlJson(token.id_token.split(".")[1]);
|
|
1148
1207
|
let name;
|
|
1149
|
-
if (
|
|
1150
|
-
const parsed = JSON.parse(
|
|
1208
|
+
if (user) {
|
|
1209
|
+
const parsed = JSON.parse(user).name;
|
|
1151
1210
|
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
1152
1211
|
}
|
|
1153
|
-
|
|
1212
|
+
return { ...claims, name };
|
|
1213
|
+
};
|
|
1214
|
+
var finish = async (ctx, raw, opts) => {
|
|
1154
1215
|
const { onProfile } = ctx.options.auth;
|
|
1155
1216
|
const profile = onProfile ? await onProfile(raw, "apple") : { id: raw.sub, name: raw.name, email: raw.email };
|
|
1156
1217
|
assertUser(profile, "onProfile");
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1218
|
+
return finishLogin(
|
|
1219
|
+
ctx,
|
|
1220
|
+
{
|
|
1221
|
+
provider: "apple",
|
|
1222
|
+
key: profile.id,
|
|
1223
|
+
email: profile.email,
|
|
1224
|
+
user: profile
|
|
1225
|
+
},
|
|
1226
|
+
opts
|
|
1227
|
+
);
|
|
1228
|
+
};
|
|
1229
|
+
var callback = async (ctx) => {
|
|
1230
|
+
const body = ctx.body || {};
|
|
1231
|
+
checkState(ctx, body.state);
|
|
1232
|
+
const url = `${ctx.url.origin}/auth/callback/apple`;
|
|
1233
|
+
const raw = await exchange(body.code, url, body.user);
|
|
1234
|
+
const res = await finish(ctx, raw);
|
|
1163
1235
|
res.headers.append("set-cookie", clearState());
|
|
1164
1236
|
return res;
|
|
1165
1237
|
};
|
|
1166
|
-
var
|
|
1238
|
+
var verify = async (ctx) => {
|
|
1239
|
+
const { code, redirect_uri, user } = ctx.body ?? {};
|
|
1240
|
+
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1241
|
+
const raw = await exchange(code, redirect_uri, user);
|
|
1242
|
+
return finish(ctx, raw, { json: true });
|
|
1243
|
+
};
|
|
1244
|
+
var apple_default = { login, callback, verify };
|
|
1167
1245
|
|
|
1168
1246
|
// src/auth/providers/oauth.ts
|
|
1247
|
+
var wantsJson = (ctx) => String(ctx.headers.accept || "").includes("application/json");
|
|
1248
|
+
var clientParams = (source) => ({
|
|
1249
|
+
redirect_uri: source.redirect_uri,
|
|
1250
|
+
state: source.state,
|
|
1251
|
+
code_challenge: source.code_challenge,
|
|
1252
|
+
code_challenge_method: source.code_challenge ? "S256" : void 0
|
|
1253
|
+
});
|
|
1169
1254
|
function oauthProvider(config2) {
|
|
1170
1255
|
const KEY = config2.name.toUpperCase();
|
|
1171
1256
|
const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
|
|
1172
|
-
const
|
|
1173
|
-
const
|
|
1174
|
-
const params = new URLSearchParams({
|
|
1257
|
+
const authorizeUrl2 = (params) => {
|
|
1258
|
+
const search = new URLSearchParams({
|
|
1175
1259
|
client_id: env[`${KEY}_ID`],
|
|
1176
|
-
redirect_uri: callbackUrl(ctx),
|
|
1177
1260
|
response_type: "code",
|
|
1178
|
-
scope: config2.scope
|
|
1179
|
-
state
|
|
1261
|
+
scope: config2.scope
|
|
1180
1262
|
});
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1263
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1264
|
+
if (value) search.set(key, value);
|
|
1265
|
+
}
|
|
1266
|
+
return `${config2.authorizeUrl}?${search}`;
|
|
1184
1267
|
};
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1268
|
+
const login3 = (ctx) => {
|
|
1269
|
+
if (wantsJson(ctx)) {
|
|
1270
|
+
return json({ url: authorizeUrl2(clientParams(ctx.url.query)) });
|
|
1271
|
+
}
|
|
1272
|
+
const { state, cookie } = startState(ctx);
|
|
1273
|
+
const url = authorizeUrl2({ redirect_uri: callbackUrl(ctx), state });
|
|
1274
|
+
return cookies("oauth_state", cookie).redirect(url);
|
|
1275
|
+
};
|
|
1276
|
+
const exchange2 = async (ctx, code, extra) => {
|
|
1277
|
+
const body = new URLSearchParams({
|
|
1278
|
+
client_id: env[`${KEY}_ID`],
|
|
1279
|
+
client_secret: env[`${KEY}_SECRET`],
|
|
1280
|
+
code,
|
|
1281
|
+
grant_type: "authorization_code"
|
|
1282
|
+
});
|
|
1283
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
1284
|
+
if (value) body.set(key, value);
|
|
1285
|
+
}
|
|
1187
1286
|
const tokenRes = await fetch(config2.tokenUrl, {
|
|
1188
1287
|
method: "POST",
|
|
1189
1288
|
headers: {
|
|
1190
1289
|
accept: "application/json",
|
|
1191
1290
|
"content-type": "application/x-www-form-urlencoded"
|
|
1192
1291
|
},
|
|
1193
|
-
body
|
|
1194
|
-
client_id: env[`${KEY}_ID`],
|
|
1195
|
-
client_secret: env[`${KEY}_SECRET`],
|
|
1196
|
-
code: ctx.url.query.code,
|
|
1197
|
-
grant_type: "authorization_code",
|
|
1198
|
-
redirect_uri: callbackUrl(ctx)
|
|
1199
|
-
})
|
|
1292
|
+
body
|
|
1200
1293
|
});
|
|
1201
1294
|
if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
|
|
1202
1295
|
const token = await tokenRes.json();
|
|
@@ -1207,20 +1300,39 @@ function oauthProvider(config2) {
|
|
|
1207
1300
|
}
|
|
1208
1301
|
});
|
|
1209
1302
|
if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
|
|
1210
|
-
|
|
1303
|
+
return profileRes.json();
|
|
1304
|
+
};
|
|
1305
|
+
const finish3 = async (ctx, raw, opts) => {
|
|
1211
1306
|
const { onProfile } = ctx.options.auth;
|
|
1212
1307
|
const profile = onProfile ? await onProfile(raw, config2.name) : config2.profile(raw);
|
|
1213
1308
|
assertUser(profile, "onProfile");
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1309
|
+
return finishLogin(
|
|
1310
|
+
ctx,
|
|
1311
|
+
{
|
|
1312
|
+
provider: config2.name,
|
|
1313
|
+
key: profile.id,
|
|
1314
|
+
email: profile.email,
|
|
1315
|
+
user: profile
|
|
1316
|
+
},
|
|
1317
|
+
opts
|
|
1318
|
+
);
|
|
1319
|
+
};
|
|
1320
|
+
const callback3 = async (ctx) => {
|
|
1321
|
+
checkState(ctx, ctx.url.query.state);
|
|
1322
|
+
const raw = await exchange2(ctx, ctx.url.query.code, {
|
|
1323
|
+
redirect_uri: callbackUrl(ctx)
|
|
1219
1324
|
});
|
|
1325
|
+
const res = await finish3(ctx, raw);
|
|
1220
1326
|
res.headers.append("set-cookie", clearState());
|
|
1221
1327
|
return res;
|
|
1222
1328
|
};
|
|
1223
|
-
|
|
1329
|
+
const verify4 = async (ctx) => {
|
|
1330
|
+
const { code, redirect_uri, code_verifier } = ctx.body ?? {};
|
|
1331
|
+
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1332
|
+
const raw = await exchange2(ctx, code, { redirect_uri, code_verifier });
|
|
1333
|
+
return finish3(ctx, raw, { json: true });
|
|
1334
|
+
};
|
|
1335
|
+
return { login: login3, callback: callback3, verify: verify4 };
|
|
1224
1336
|
}
|
|
1225
1337
|
|
|
1226
1338
|
// src/auth/providers/discord.ts
|
|
@@ -1252,10 +1364,10 @@ async function emailLogin(ctx) {
|
|
|
1252
1364
|
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
1253
1365
|
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
1254
1366
|
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
1255
|
-
const
|
|
1256
|
-
if (!await
|
|
1257
|
-
const user = await
|
|
1258
|
-
const isValid = await
|
|
1367
|
+
const users = ctx.options.auth.users;
|
|
1368
|
+
if (!await users.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
1369
|
+
const user = await users.get(email);
|
|
1370
|
+
const isValid = await verify2(password, user.password);
|
|
1259
1371
|
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
1260
1372
|
return finishLogin(ctx, {
|
|
1261
1373
|
provider: "email",
|
|
@@ -1270,8 +1382,8 @@ async function emailRegister(ctx) {
|
|
|
1270
1382
|
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
1271
1383
|
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
1272
1384
|
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
1273
|
-
const
|
|
1274
|
-
if (await
|
|
1385
|
+
const users = ctx.options.auth.users;
|
|
1386
|
+
if (await users.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
1275
1387
|
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
1276
1388
|
const user = {
|
|
1277
1389
|
id: createId(email),
|
|
@@ -1293,12 +1405,12 @@ async function emailResetPassword() {
|
|
|
1293
1405
|
}
|
|
1294
1406
|
async function emailUpdatePassword(ctx) {
|
|
1295
1407
|
const passwords = ctx.body;
|
|
1296
|
-
const fullUser = await ctx.options.auth.
|
|
1408
|
+
const fullUser = await ctx.options.auth.users.get(ctx.user.email);
|
|
1297
1409
|
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
1298
|
-
const isValid = await
|
|
1410
|
+
const isValid = await verify2(passwords.previous, fullUser.password);
|
|
1299
1411
|
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
1300
1412
|
fullUser.password = await hash2(passwords.updated);
|
|
1301
|
-
await updateUser(fullUser, ctx.user, ctx.options.auth.
|
|
1413
|
+
await updateUser(fullUser, ctx.user, ctx.options.auth.users);
|
|
1302
1414
|
return 200;
|
|
1303
1415
|
}
|
|
1304
1416
|
var email_default = {
|
|
@@ -1324,7 +1436,8 @@ var facebook_default = oauthProvider({
|
|
|
1324
1436
|
});
|
|
1325
1437
|
|
|
1326
1438
|
// src/auth/providers/github.ts
|
|
1327
|
-
var
|
|
1439
|
+
var AUTHORIZE2 = "https://github.com/login/oauth/authorize";
|
|
1440
|
+
var oauth = async (code, extra) => {
|
|
1328
1441
|
const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
|
|
1329
1442
|
headers2.accept = "application/json";
|
|
1330
1443
|
headers2["content-type"] = "application/json";
|
|
@@ -1332,13 +1445,17 @@ var oauth = async (code) => {
|
|
|
1332
1445
|
if (!res2.ok) throw new Error("Invalid request");
|
|
1333
1446
|
return res2.json();
|
|
1334
1447
|
};
|
|
1448
|
+
const params = {
|
|
1449
|
+
client_id: env.GITHUB_ID,
|
|
1450
|
+
client_secret: env.GITHUB_SECRET,
|
|
1451
|
+
code
|
|
1452
|
+
};
|
|
1453
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
1454
|
+
if (value) params[key] = value;
|
|
1455
|
+
}
|
|
1335
1456
|
const res = await fch("https://github.com/login/oauth/access_token", {
|
|
1336
1457
|
method: "post",
|
|
1337
|
-
body: JSON.stringify(
|
|
1338
|
-
client_id: env.GITHUB_ID,
|
|
1339
|
-
client_secret: env.GITHUB_SECRET,
|
|
1340
|
-
code
|
|
1341
|
-
})
|
|
1458
|
+
body: JSON.stringify(params)
|
|
1342
1459
|
});
|
|
1343
1460
|
return (path) => {
|
|
1344
1461
|
return fch(`https://api.github.com${path}`, {
|
|
@@ -1346,19 +1463,25 @@ var oauth = async (code) => {
|
|
|
1346
1463
|
});
|
|
1347
1464
|
};
|
|
1348
1465
|
};
|
|
1349
|
-
var
|
|
1350
|
-
const
|
|
1351
|
-
const params = new URLSearchParams({
|
|
1466
|
+
var authorizeUrl = (params) => {
|
|
1467
|
+
const search = new URLSearchParams({
|
|
1352
1468
|
client_id: env.GITHUB_ID,
|
|
1353
|
-
scope: "user:email"
|
|
1354
|
-
state
|
|
1469
|
+
scope: "user:email"
|
|
1355
1470
|
});
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1471
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1472
|
+
if (value) search.set(key, value);
|
|
1473
|
+
}
|
|
1474
|
+
return `${AUTHORIZE2}?${search}`;
|
|
1475
|
+
};
|
|
1476
|
+
var login2 = (ctx) => {
|
|
1477
|
+
if (wantsJson(ctx)) {
|
|
1478
|
+
return json({ url: authorizeUrl(clientParams(ctx.url.query)) });
|
|
1479
|
+
}
|
|
1480
|
+
const { state, cookie } = startState(ctx);
|
|
1481
|
+
return cookies("oauth_state", cookie).redirect(authorizeUrl({ state }));
|
|
1359
1482
|
};
|
|
1360
|
-
var getUserProfile = async (code) => {
|
|
1361
|
-
const api = await oauth(code);
|
|
1483
|
+
var getUserProfile = async (code, extra = {}) => {
|
|
1484
|
+
const api = await oauth(code, extra);
|
|
1362
1485
|
const [profile, emails] = await Promise.all([
|
|
1363
1486
|
api("/user"),
|
|
1364
1487
|
api("/user/emails")
|
|
@@ -1374,22 +1497,35 @@ var defaultProfile = (raw) => ({
|
|
|
1374
1497
|
location: raw.location,
|
|
1375
1498
|
created: raw.created_at
|
|
1376
1499
|
});
|
|
1377
|
-
var
|
|
1378
|
-
checkState(ctx, ctx.url.query.state);
|
|
1379
|
-
const raw = await getUserProfile(ctx.url.query.code);
|
|
1500
|
+
var finish2 = async (ctx, raw, opts) => {
|
|
1380
1501
|
const { onProfile } = ctx.options.auth;
|
|
1381
1502
|
const profile = onProfile ? await onProfile(raw, "github") : defaultProfile(raw);
|
|
1382
1503
|
assertUser(profile, "onProfile");
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1504
|
+
return finishLogin(
|
|
1505
|
+
ctx,
|
|
1506
|
+
{
|
|
1507
|
+
provider: "github",
|
|
1508
|
+
key: profile.id,
|
|
1509
|
+
email: profile.email,
|
|
1510
|
+
user: profile
|
|
1511
|
+
},
|
|
1512
|
+
opts
|
|
1513
|
+
);
|
|
1514
|
+
};
|
|
1515
|
+
var callback2 = async (ctx) => {
|
|
1516
|
+
checkState(ctx, ctx.url.query.state);
|
|
1517
|
+
const raw = await getUserProfile(ctx.url.query.code);
|
|
1518
|
+
const res = await finish2(ctx, raw);
|
|
1389
1519
|
res.headers.append("set-cookie", clearState());
|
|
1390
1520
|
return res;
|
|
1391
1521
|
};
|
|
1392
|
-
var
|
|
1522
|
+
var verify3 = async (ctx) => {
|
|
1523
|
+
const { code, redirect_uri, code_verifier } = ctx.body ?? {};
|
|
1524
|
+
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1525
|
+
const raw = await getUserProfile(code, { redirect_uri, code_verifier });
|
|
1526
|
+
return finish2(ctx, raw, { json: true });
|
|
1527
|
+
};
|
|
1528
|
+
var github_default = { login: login2, callback: callback2, verify: verify3 };
|
|
1393
1529
|
|
|
1394
1530
|
// src/auth/providers/google.ts
|
|
1395
1531
|
var google_default = oauthProvider({
|
|
@@ -1439,7 +1575,7 @@ function defaultOnUser(fullUser) {
|
|
|
1439
1575
|
return user;
|
|
1440
1576
|
}
|
|
1441
1577
|
var available = Object.keys(providers_default);
|
|
1442
|
-
function parseAuthOptions(auth2
|
|
1578
|
+
function parseAuthOptions(auth2) {
|
|
1443
1579
|
if (!auth2) return null;
|
|
1444
1580
|
if (typeof auth2 === "string") {
|
|
1445
1581
|
const [strategy2, provider] = auth2.split(":");
|
|
@@ -1462,15 +1598,8 @@ function parseAuthOptions(auth2, all) {
|
|
|
1462
1598
|
const redirect2 = auth2.redirect || defaultRedirect;
|
|
1463
1599
|
const { onProfile, onLogin, onLogout } = auth2;
|
|
1464
1600
|
const onUser = auth2.onUser || defaultOnUser;
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
}
|
|
1468
|
-
if (!auth2.session && !all.store) {
|
|
1469
|
-
throw new Error("Need a sessionStore store for Auth");
|
|
1470
|
-
}
|
|
1471
|
-
const store = all.store ? toStore(all.store) : null;
|
|
1472
|
-
const authStore = auth2.store ? toStore(auth2.store) : store.prefix("user:");
|
|
1473
|
-
const sessionStore = auth2.session ? toStore(auth2.session) : store.prefix("auth:");
|
|
1601
|
+
const onToken = auth2.onToken || defaultOnUser;
|
|
1602
|
+
const users = auth2.users ? toStore(auth2.users) : null;
|
|
1474
1603
|
return {
|
|
1475
1604
|
strategy,
|
|
1476
1605
|
providers: list,
|
|
@@ -1478,9 +1607,9 @@ function parseAuthOptions(auth2, all) {
|
|
|
1478
1607
|
onProfile,
|
|
1479
1608
|
onLogin,
|
|
1480
1609
|
onUser,
|
|
1610
|
+
onToken,
|
|
1481
1611
|
onLogout,
|
|
1482
|
-
|
|
1483
|
-
session: sessionStore
|
|
1612
|
+
users
|
|
1484
1613
|
};
|
|
1485
1614
|
}
|
|
1486
1615
|
|
|
@@ -1631,6 +1760,9 @@ function resolveSecurity(security) {
|
|
|
1631
1760
|
return {
|
|
1632
1761
|
trustProxy: o.trustProxy ?? true,
|
|
1633
1762
|
traversalProtection: off ? false : o.traversalProtection !== false,
|
|
1763
|
+
// Cap on the bytes buffered per request (see bodyLimit). `false` (or
|
|
1764
|
+
// turning security off entirely) resolves to Infinity, meaning no limit.
|
|
1765
|
+
maxBody: off ? INF : resolveMax(o.maxBody),
|
|
1634
1766
|
headers: headers2,
|
|
1635
1767
|
hsts: off ? null : val(o.hsts, "max-age=15552000; includeSubDomains")
|
|
1636
1768
|
};
|
|
@@ -1661,6 +1793,19 @@ function applySecurity(res, ctx) {
|
|
|
1661
1793
|
// src/helpers/config.ts
|
|
1662
1794
|
function config(options = {}) {
|
|
1663
1795
|
const env2 = globalThis.env;
|
|
1796
|
+
const opts = options;
|
|
1797
|
+
if (typeof opts.body === "string") {
|
|
1798
|
+
throw new Error(
|
|
1799
|
+
`The root \`body: '${opts.body}'\` option is now \`parser: '${opts.body}'\`.`
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
for (const key of ["body", "query", "params", "response"]) {
|
|
1803
|
+
if (opts[key] !== void 0) {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`\`${key}\` is a route option, not a root one; pass it per route, like .post('/', { ${key} }, handler).`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1664
1809
|
const raw = options.log ?? env2.LOG_LEVEL;
|
|
1665
1810
|
const level = raw === true ? "info" : raw === false ? void 0 : raw;
|
|
1666
1811
|
const log = createLogger(level);
|
|
@@ -1670,10 +1815,14 @@ function config(options = {}) {
|
|
|
1670
1815
|
log,
|
|
1671
1816
|
// How request bodies are read: parsed into ctx.body by default; `raw` keeps
|
|
1672
1817
|
// the Buffer, `stream` hands the handler the unread web ReadableStream.
|
|
1673
|
-
|
|
1818
|
+
parser: options.parser ?? "parse",
|
|
1674
1819
|
// Secure-by-default response headers + trustProxy for ctx.ip. `false` turns
|
|
1675
1820
|
// the added headers off; see resolveSecurity for the defaults.
|
|
1676
|
-
security: resolveSecurity(options.security)
|
|
1821
|
+
security: resolveSecurity(options.security),
|
|
1822
|
+
// Sessions: one record per device, exposed as ctx.session. Anything
|
|
1823
|
+
// polystore accepts works; raw sources (a Map, a Redis client) get a 1w
|
|
1824
|
+
// expiry, a built store is honored as-is, prefix and expiry included.
|
|
1825
|
+
sessions: toStoreExpiring(options.sessions ?? /* @__PURE__ */ new Map(), "1w")
|
|
1677
1826
|
};
|
|
1678
1827
|
if (options.cache !== void 0) settings.cache = options.cache;
|
|
1679
1828
|
options.cors = options.cors || env2.CORS || null;
|
|
@@ -1727,16 +1876,26 @@ function config(options = {}) {
|
|
|
1727
1876
|
}
|
|
1728
1877
|
const favicon2 = options.favicon || env2.FAVICON;
|
|
1729
1878
|
if (favicon2) settings.favicon = favicon2;
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
settings.session = { store: toStore(store) };
|
|
1734
|
-
}
|
|
1735
|
-
if (settings.store && !options.session) {
|
|
1736
|
-
settings.session = { store: settings.store.prefix("session:") };
|
|
1737
|
-
}
|
|
1879
|
+
const production = env2.NODE_ENV === "production";
|
|
1880
|
+
const defaulted = options.sessions == null;
|
|
1881
|
+
settings.sessionsDefault = defaulted;
|
|
1738
1882
|
if (options.auth || env2.AUTH) {
|
|
1739
|
-
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null
|
|
1883
|
+
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null);
|
|
1884
|
+
}
|
|
1885
|
+
if (settings.auth) {
|
|
1886
|
+
if (!settings.auth.users) {
|
|
1887
|
+
if (production) {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
"Auth in production needs a persistent `users` store, like auth: { ..., users: kv(redis).prefix('users:') }."
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
settings.auth.users = toStore(/* @__PURE__ */ new Map());
|
|
1893
|
+
}
|
|
1894
|
+
if (production && defaulted && !settings.auth.strategy.includes("jwt")) {
|
|
1895
|
+
throw new Error(
|
|
1896
|
+
"Auth in production needs a persistent `sessions` store, like sessions: kv(redis).prefix('session:')."
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1740
1899
|
}
|
|
1741
1900
|
if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
|
|
1742
1901
|
console.warn(
|
|
@@ -1760,7 +1919,7 @@ function config(options = {}) {
|
|
|
1760
1919
|
}
|
|
1761
1920
|
if (settings.public) log.message("public", loc(options.public));
|
|
1762
1921
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
1763
|
-
if (
|
|
1922
|
+
if (options.sessions) log.message("sessions", "enabled");
|
|
1764
1923
|
if (settings.cors) {
|
|
1765
1924
|
const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
|
|
1766
1925
|
log.message("cors", origin);
|
|
@@ -1808,7 +1967,7 @@ function applyCors(res, ctx) {
|
|
|
1808
1967
|
|
|
1809
1968
|
// src/helpers/createWebsocket.ts
|
|
1810
1969
|
function createWebsocket(sockets, handlers) {
|
|
1811
|
-
const
|
|
1970
|
+
const run2 = (event, socket, body) => {
|
|
1812
1971
|
const routes = handlers.socket?.filter((r2) => r2.path === event) ?? [];
|
|
1813
1972
|
const user = socket.user ?? socket.data?.user;
|
|
1814
1973
|
for (const route of routes) {
|
|
@@ -1818,14 +1977,14 @@ function createWebsocket(sockets, handlers) {
|
|
|
1818
1977
|
}
|
|
1819
1978
|
};
|
|
1820
1979
|
return {
|
|
1821
|
-
message: (socket, body) =>
|
|
1980
|
+
message: (socket, body) => run2("message", socket, body),
|
|
1822
1981
|
open: (socket) => {
|
|
1823
1982
|
sockets.push(socket);
|
|
1824
|
-
|
|
1983
|
+
run2("open", socket);
|
|
1825
1984
|
},
|
|
1826
1985
|
close: (socket) => {
|
|
1827
1986
|
sockets.splice(sockets.indexOf(socket), 1);
|
|
1828
|
-
|
|
1987
|
+
run2("close", socket);
|
|
1829
1988
|
}
|
|
1830
1989
|
};
|
|
1831
1990
|
}
|
|
@@ -1871,6 +2030,14 @@ function getMachine() {
|
|
|
1871
2030
|
}
|
|
1872
2031
|
|
|
1873
2032
|
// src/parseResponse.ts
|
|
2033
|
+
var warned = false;
|
|
2034
|
+
var warnDefault = () => {
|
|
2035
|
+
if (warned) return;
|
|
2036
|
+
warned = true;
|
|
2037
|
+
console.warn(
|
|
2038
|
+
"[server:sessions] Using the default in-memory session store in production: sessions are lost on restart and not shared across instances. Configure one with sessions: kv(redis).prefix('session:')."
|
|
2039
|
+
);
|
|
2040
|
+
};
|
|
1874
2041
|
async function parseResponse(out, ctx) {
|
|
1875
2042
|
if (!out && typeof out !== "string") return null;
|
|
1876
2043
|
if (typeof out === "function") {
|
|
@@ -1938,11 +2105,14 @@ async function parseResponse(out, ctx) {
|
|
|
1938
2105
|
if (ctx.time?.times?.length > 1) {
|
|
1939
2106
|
out.headers.set("Server-Timing", ctx.time.headers());
|
|
1940
2107
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2108
|
+
const prev = loaded.get(ctx);
|
|
2109
|
+
const jwt = ctx.options.auth?.strategy.includes("jwt");
|
|
2110
|
+
const data = jwt ? "{}" : JSON.stringify(ctx.session ?? {});
|
|
2111
|
+
if (!jwt && data !== (prev?.data ?? "{}")) {
|
|
2112
|
+
if (ctx.options.sessionsDefault && ctx.platform.production) {
|
|
2113
|
+
warnDefault();
|
|
1944
2114
|
}
|
|
1945
|
-
let id =
|
|
2115
|
+
let id = prev?.id;
|
|
1946
2116
|
if (!id) {
|
|
1947
2117
|
id = createId();
|
|
1948
2118
|
out.headers.append(
|
|
@@ -1956,7 +2126,7 @@ async function parseResponse(out, ctx) {
|
|
|
1956
2126
|
})
|
|
1957
2127
|
);
|
|
1958
2128
|
}
|
|
1959
|
-
ctx.options.
|
|
2129
|
+
ctx.options.sessions.set(id, ctx.session);
|
|
1960
2130
|
}
|
|
1961
2131
|
if (ctx?.res?.headers) {
|
|
1962
2132
|
for (const key in ctx.res.headers) {
|
|
@@ -2009,36 +2179,48 @@ function pathPattern(pattern, path) {
|
|
|
2009
2179
|
return null;
|
|
2010
2180
|
}
|
|
2011
2181
|
|
|
2012
|
-
// src/
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
if (
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (typeof schema?.body?.parse === "function") {
|
|
2022
|
-
base = "body";
|
|
2023
|
-
schema.body.parse(ctx.body || {});
|
|
2024
|
-
}
|
|
2025
|
-
if (typeof schema?.query === "function") {
|
|
2026
|
-
base = "query";
|
|
2027
|
-
schema.query(ctx.url.query || {});
|
|
2028
|
-
}
|
|
2029
|
-
if (typeof schema?.query?.parse === "function") {
|
|
2030
|
-
base = "query";
|
|
2031
|
-
schema.query.parse(ctx.url.query || {});
|
|
2032
|
-
}
|
|
2033
|
-
} catch (error) {
|
|
2034
|
-
if (error.name === "ZodError" || error.constructor.name === "ZodError") {
|
|
2035
|
-
const message = error.issues.map(
|
|
2036
|
-
({ path, message: message2 }) => `[${base}.${path.join(".")}]: ${message2}`
|
|
2037
|
-
).sort().join("\n");
|
|
2038
|
-
throw new StatusError(message, 422);
|
|
2182
|
+
// src/errors/ValidationError.ts
|
|
2183
|
+
var ValidationError = class extends StatusError {
|
|
2184
|
+
source;
|
|
2185
|
+
issues;
|
|
2186
|
+
constructor(source, issues) {
|
|
2187
|
+
if (source === "response") {
|
|
2188
|
+
super("Server Error", 500);
|
|
2189
|
+
} else {
|
|
2190
|
+
super(`Invalid request ${source}`, 422);
|
|
2039
2191
|
}
|
|
2040
|
-
|
|
2192
|
+
this.source = source;
|
|
2193
|
+
this.issues = issues;
|
|
2041
2194
|
}
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
// src/helpers/validate.ts
|
|
2198
|
+
async function run(schema, value, source) {
|
|
2199
|
+
const result = await schema["~standard"].validate(value);
|
|
2200
|
+
if (result.issues) throw new ValidationError(source, result.issues);
|
|
2201
|
+
return result.value;
|
|
2202
|
+
}
|
|
2203
|
+
async function validateRequest(ctx, options) {
|
|
2204
|
+
if (options.body) {
|
|
2205
|
+
ctx.body = await run(options.body, ctx.body ?? {}, "body");
|
|
2206
|
+
}
|
|
2207
|
+
if (options.query) {
|
|
2208
|
+
const query = await run(options.query, ctx.url.query || {}, "query");
|
|
2209
|
+
replace2(ctx.url.query, query);
|
|
2210
|
+
}
|
|
2211
|
+
if (options.params) {
|
|
2212
|
+
const params = await run(options.params, ctx.url.params || {}, "params");
|
|
2213
|
+
replace2(ctx.url.params, params);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
async function validateResponse(out, options) {
|
|
2217
|
+
if (!options.response) return out;
|
|
2218
|
+
if (out?.constructor !== Object && !Array.isArray(out)) return out;
|
|
2219
|
+
return await run(options.response, out, "response");
|
|
2220
|
+
}
|
|
2221
|
+
function replace2(target, values) {
|
|
2222
|
+
for (const key of Object.keys(target)) delete target[key];
|
|
2223
|
+
Object.assign(target, values);
|
|
2042
2224
|
}
|
|
2043
2225
|
|
|
2044
2226
|
// src/helpers/handleRequest.ts
|
|
@@ -2063,20 +2245,28 @@ async function getResponse(app, ctx) {
|
|
|
2063
2245
|
ctx.options = { ...app.settings, ...route.options };
|
|
2064
2246
|
}
|
|
2065
2247
|
checkTraversal(params, ctx);
|
|
2066
|
-
ctx.body = await resolveBody(
|
|
2248
|
+
ctx.body = await resolveBody(
|
|
2249
|
+
ctx,
|
|
2250
|
+
ctx.options.parser,
|
|
2251
|
+
ctx.options.security.maxBody
|
|
2252
|
+
);
|
|
2253
|
+
await validateRequest(ctx, route.options);
|
|
2067
2254
|
for (const cb of route.fns) {
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
}
|
|
2255
|
+
const res = await cb(ctx);
|
|
2256
|
+
const out = await parseResponse(
|
|
2257
|
+
await validateResponse(res, route.options),
|
|
2258
|
+
ctx
|
|
2259
|
+
);
|
|
2260
|
+
if (out) return out;
|
|
2075
2261
|
}
|
|
2076
2262
|
break;
|
|
2077
2263
|
}
|
|
2078
2264
|
if (!matched) {
|
|
2079
|
-
ctx.body = await resolveBody(
|
|
2265
|
+
ctx.body = await resolveBody(
|
|
2266
|
+
ctx,
|
|
2267
|
+
ctx.options.parser,
|
|
2268
|
+
ctx.options.security.maxBody
|
|
2269
|
+
);
|
|
2080
2270
|
for (const mw of app.middleware) {
|
|
2081
2271
|
const out = await parseResponse(await mw(ctx), ctx);
|
|
2082
2272
|
if (out) return out;
|
|
@@ -2218,7 +2408,7 @@ function timingSafeEqual(a, b) {
|
|
|
2218
2408
|
}
|
|
2219
2409
|
return mismatch === 0;
|
|
2220
2410
|
}
|
|
2221
|
-
async function
|
|
2411
|
+
async function verify2(password, hash3) {
|
|
2222
2412
|
if ("Bun" in globalThis) {
|
|
2223
2413
|
return Bun.password.verify(password, hash3, "argon2id");
|
|
2224
2414
|
}
|
|
@@ -2252,94 +2442,72 @@ async function verify(password, hash3) {
|
|
|
2252
2442
|
});
|
|
2253
2443
|
}
|
|
2254
2444
|
|
|
2255
|
-
// src/auth/
|
|
2256
|
-
|
|
2257
|
-
const
|
|
2258
|
-
if (
|
|
2445
|
+
// src/auth/getUser.ts
|
|
2446
|
+
async function getJwtUser(ctx) {
|
|
2447
|
+
const header = ctx.headers.authorization;
|
|
2448
|
+
if (!header) return;
|
|
2449
|
+
const [type2, token] = header.trim().split(" ");
|
|
2450
|
+
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
2259
2451
|
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2260
2452
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
return authorization;
|
|
2271
|
-
};
|
|
2272
|
-
function findSessionId(ctx) {
|
|
2273
|
-
const strategy = ctx.options.auth.strategy;
|
|
2274
|
-
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
2275
|
-
if (strategy.includes("token")) {
|
|
2276
|
-
if (!ctx.headers.authorization) return;
|
|
2277
|
-
return validateToken(ctx.headers.authorization);
|
|
2278
|
-
}
|
|
2279
|
-
if (strategy.includes("cookie")) {
|
|
2280
|
-
if (!ctx.cookies.authentication) return;
|
|
2281
|
-
return validateCookie(ctx.cookies.authentication);
|
|
2453
|
+
const payload = await verifyJwt(token, ctx.options.secret);
|
|
2454
|
+
if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2455
|
+
const { iat, exp, ...claims } = payload;
|
|
2456
|
+
if (!claims.id || !claims.email) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2457
|
+
if (!ctx.options.auth.providers.includes(claims.provider)) {
|
|
2458
|
+
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
2459
|
+
provider: claims.provider,
|
|
2460
|
+
valid: ctx.options.auth.providers
|
|
2461
|
+
});
|
|
2282
2462
|
}
|
|
2283
|
-
|
|
2463
|
+
const exposed = await ctx.options.auth.onUser(claims, ctx);
|
|
2464
|
+
assertUser(exposed, "onUser");
|
|
2465
|
+
return exposed;
|
|
2284
2466
|
}
|
|
2285
|
-
|
|
2286
|
-
// src/auth/getUser.ts
|
|
2287
2467
|
async function getAuthSession(ctx) {
|
|
2288
|
-
|
|
2289
|
-
if (
|
|
2290
|
-
const
|
|
2291
|
-
if (!
|
|
2292
|
-
|
|
2293
|
-
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
2294
|
-
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2295
|
-
}
|
|
2296
|
-
const payload = await verifyJwt(token, ctx.options.secret);
|
|
2297
|
-
if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2298
|
-
return payload;
|
|
2468
|
+
let session2 = ctx.session;
|
|
2469
|
+
if (!session2) {
|
|
2470
|
+
const id = findSessionId(ctx);
|
|
2471
|
+
if (!id) return;
|
|
2472
|
+
session2 = await ctx.options.sessions.get(id) ?? void 0;
|
|
2299
2473
|
}
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
return ctx.options.auth.session.get(id);
|
|
2474
|
+
if (!session2?.user) return;
|
|
2475
|
+
return session2;
|
|
2303
2476
|
}
|
|
2304
2477
|
async function getUser(ctx) {
|
|
2305
2478
|
if (!ctx.options.auth) return;
|
|
2306
2479
|
const options = ctx.options.auth;
|
|
2480
|
+
if (options.strategy.includes("jwt")) return getJwtUser(ctx);
|
|
2307
2481
|
const auth2 = await getAuthSession(ctx);
|
|
2308
2482
|
if (!auth2) return;
|
|
2309
|
-
if (options.strategy !== auth2.strategy) {
|
|
2310
|
-
throw ServerError_default.AUTH_INVALID_STRATEGY({
|
|
2311
|
-
strategy: auth2.strategy || "undefined",
|
|
2312
|
-
valid: options.strategy
|
|
2313
|
-
});
|
|
2314
|
-
}
|
|
2315
2483
|
if (!options.providers.includes(auth2.provider)) {
|
|
2316
2484
|
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
2317
2485
|
provider: auth2.provider,
|
|
2318
2486
|
valid: options.providers
|
|
2319
2487
|
});
|
|
2320
2488
|
}
|
|
2321
|
-
const user = await
|
|
2489
|
+
const user = await options.users.get(auth2.user);
|
|
2322
2490
|
if (!user) throw ServerError_default.AUTH_NO_USER();
|
|
2323
|
-
|
|
2324
|
-
user.provider = auth2.provider;
|
|
2325
|
-
const exposed = await ctx.options.auth.onUser(user, ctx);
|
|
2491
|
+
const exposed = await options.onUser(user, ctx);
|
|
2326
2492
|
assertUser(exposed, "onUser");
|
|
2327
2493
|
return exposed;
|
|
2328
2494
|
}
|
|
2329
2495
|
|
|
2330
2496
|
// src/auth/logout.ts
|
|
2331
2497
|
async function logout(ctx) {
|
|
2332
|
-
const { strategy } = ctx.
|
|
2333
|
-
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
2498
|
+
const { strategy } = ctx.options.auth;
|
|
2334
2499
|
if (!strategy.includes("jwt")) {
|
|
2335
|
-
|
|
2500
|
+
const prev = loaded.get(ctx);
|
|
2501
|
+
if (prev?.id) await ctx.options.sessions.del(prev.id);
|
|
2502
|
+
ctx.session = {};
|
|
2503
|
+
loaded.set(ctx, { id: void 0, data: "{}" });
|
|
2336
2504
|
}
|
|
2337
2505
|
if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
|
|
2338
2506
|
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
2339
2507
|
return { token: null };
|
|
2340
2508
|
}
|
|
2341
2509
|
if (strategy.includes("cookie")) {
|
|
2342
|
-
return cookies({
|
|
2510
|
+
return cookies({ session: null }).redirect("/");
|
|
2343
2511
|
}
|
|
2344
2512
|
throw new Error("Unknown auth type");
|
|
2345
2513
|
}
|
|
@@ -2365,6 +2533,7 @@ function auth(app) {
|
|
|
2365
2533
|
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
2366
2534
|
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
2367
2535
|
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
2536
|
+
app.post(`/auth/verify/${name}`, providers_default[name].verify);
|
|
2368
2537
|
}
|
|
2369
2538
|
if (enabled.includes("apple")) {
|
|
2370
2539
|
const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
|
|
@@ -2373,6 +2542,7 @@ function auth(app) {
|
|
|
2373
2542
|
}
|
|
2374
2543
|
app.get("/auth/login/apple", providers_default.apple.login);
|
|
2375
2544
|
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
2545
|
+
app.post("/auth/verify/apple", providers_default.apple.verify);
|
|
2376
2546
|
}
|
|
2377
2547
|
if (enabled.includes("email")) {
|
|
2378
2548
|
app.post("/auth/register/email", providers_default.email.register);
|
|
@@ -2541,8 +2711,8 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2541
2711
|
for (const route of routes) {
|
|
2542
2712
|
const path = route.path;
|
|
2543
2713
|
const fn = route.fns.find((p) => typeof p === "function");
|
|
2544
|
-
const meta = route.
|
|
2545
|
-
const config2 = getConfig(route.options);
|
|
2714
|
+
const meta = route.options ?? {};
|
|
2715
|
+
const config2 = getConfig(route.options?.schema);
|
|
2546
2716
|
if (typeof path !== "string" || path === "*" || path === "/docs" || !fn) {
|
|
2547
2717
|
continue;
|
|
2548
2718
|
}
|
|
@@ -2595,7 +2765,7 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2595
2765
|
paths[normalizedPath][method] = {
|
|
2596
2766
|
tags: config2.tags,
|
|
2597
2767
|
summary: config2.title || getTag("@title", fn) || `${method.toUpperCase()} ${normalizedPath}`,
|
|
2598
|
-
description: getTitle(fn) || getDescription(fn),
|
|
2768
|
+
description: config2.description || getTitle(fn) || getDescription(fn),
|
|
2599
2769
|
requestBody,
|
|
2600
2770
|
parameters,
|
|
2601
2771
|
responses
|
|
@@ -2647,39 +2817,6 @@ function preflight(ctx) {
|
|
|
2647
2817
|
return 204;
|
|
2648
2818
|
}
|
|
2649
2819
|
|
|
2650
|
-
// src/middle/NoSession.ts
|
|
2651
|
-
var NoSession = class {
|
|
2652
|
-
};
|
|
2653
|
-
function createNoSession() {
|
|
2654
|
-
return new Proxy(NoSession, {
|
|
2655
|
-
get(target, key) {
|
|
2656
|
-
if (target[key]) return target[key];
|
|
2657
|
-
if (key === "then") return target[key];
|
|
2658
|
-
if (typeof key === "symbol") return target[key];
|
|
2659
|
-
throw ServerError_default.NO_STORE_READ({ key: String(key) });
|
|
2660
|
-
},
|
|
2661
|
-
set(target, key, value) {
|
|
2662
|
-
if (target[key] || key === "then" || typeof key === "symbol") {
|
|
2663
|
-
target[key] = value;
|
|
2664
|
-
return true;
|
|
2665
|
-
}
|
|
2666
|
-
throw ServerError_default.NO_STORE_WRITE({ key: String(key) });
|
|
2667
|
-
}
|
|
2668
|
-
});
|
|
2669
|
-
}
|
|
2670
|
-
|
|
2671
|
-
// src/middle/session.ts
|
|
2672
|
-
async function session(ctx) {
|
|
2673
|
-
const store = ctx.options.session?.store;
|
|
2674
|
-
if (!store) {
|
|
2675
|
-
ctx.session = createNoSession();
|
|
2676
|
-
return;
|
|
2677
|
-
}
|
|
2678
|
-
if (ctx.cookies.session) {
|
|
2679
|
-
ctx.session = await store.get(ctx.cookies.session) || {};
|
|
2680
|
-
}
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
2820
|
// src/middle/timer.ts
|
|
2684
2821
|
var createTime = () => {
|
|
2685
2822
|
const times2 = [["init", performance.now()]];
|
|
@@ -3052,6 +3189,14 @@ var Netlify = async (app, request, context) => {
|
|
|
3052
3189
|
};
|
|
3053
3190
|
|
|
3054
3191
|
// src/router.ts
|
|
3192
|
+
function checkParserConflict(options, globalParser) {
|
|
3193
|
+
const parser = options.parser ?? globalParser ?? "parse";
|
|
3194
|
+
if (options.body && parser !== "parse") {
|
|
3195
|
+
throw new Error(
|
|
3196
|
+
`A \`parser: '${parser}'\` route never parses the body, so its \`body\` schema cannot run. Remove one, or set \`parser: 'parse'\` on the route.`
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3055
3200
|
var Router = class _Router {
|
|
3056
3201
|
// Cross-cutting middleware added with .use(); they run on every request
|
|
3057
3202
|
middleware = [];
|
|
@@ -3085,6 +3230,7 @@ var Router = class _Router {
|
|
|
3085
3230
|
if (rest[0] != null && typeof rest[0] !== "function") {
|
|
3086
3231
|
options = rest.shift();
|
|
3087
3232
|
}
|
|
3233
|
+
checkParserConflict(options, this.settings?.parser);
|
|
3088
3234
|
const base = method === "socket" ? [] : this.middleware;
|
|
3089
3235
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
3090
3236
|
this.handlers[method].push({ path, options, fns });
|
|
@@ -3119,6 +3265,7 @@ var Router = class _Router {
|
|
|
3119
3265
|
if (arg instanceof _Router) {
|
|
3120
3266
|
for (const m of Object.keys(arg.handlers)) {
|
|
3121
3267
|
for (const route of arg.handlers[m]) {
|
|
3268
|
+
checkParserConflict(route.options, this.settings?.parser);
|
|
3122
3269
|
const base = m === "socket" ? [] : this.middleware;
|
|
3123
3270
|
this.handlers[m].push({
|
|
3124
3271
|
path: route.path,
|
|
@@ -3208,16 +3355,17 @@ var Server = class extends Router {
|
|
|
3208
3355
|
} else if (this.platform.runtime === "bun") {
|
|
3209
3356
|
this.settings.log.start(`http://localhost:${this.settings.port}/`);
|
|
3210
3357
|
}
|
|
3211
|
-
this
|
|
3212
|
-
|
|
3213
|
-
this.use(
|
|
3214
|
-
|
|
3215
|
-
this.
|
|
3358
|
+
const app = this;
|
|
3359
|
+
app.use(timer);
|
|
3360
|
+
if (this.settings.cors) app.use(preflight);
|
|
3361
|
+
app.use(assets);
|
|
3362
|
+
if (this.settings.favicon) app.get("/favicon.ico", favicon);
|
|
3363
|
+
app.use(session);
|
|
3216
3364
|
if (this.settings.auth) {
|
|
3217
|
-
auth(
|
|
3365
|
+
auth(app);
|
|
3218
3366
|
}
|
|
3219
3367
|
if (this.settings.openapi) {
|
|
3220
|
-
|
|
3368
|
+
app.get(this.settings.openapi.path || "/docs", openapi_default);
|
|
3221
3369
|
}
|
|
3222
3370
|
}
|
|
3223
3371
|
self() {
|
|
@@ -3252,6 +3400,7 @@ function server(options) {
|
|
|
3252
3400
|
export {
|
|
3253
3401
|
Server,
|
|
3254
3402
|
ServerError_default as ServerError,
|
|
3403
|
+
ValidationError,
|
|
3255
3404
|
default3 as bucket,
|
|
3256
3405
|
cache,
|
|
3257
3406
|
cookies,
|