@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.
Files changed (4) hide show
  1. package/index.d.ts +150 -103
  2. package/index.js +433 -284
  3. package/package.json +15 -16
  4. 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
- AUTH_INVALID_COOKIE: { status: 401, message: "Invalid Authorization cookie" },
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 body: { max: '10mb' } on the route or server, or set max: false to disable.`,
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, body) {
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 toStore(source) {
729
+ function isStore(source) {
735
730
  const store = source;
736
- if (store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function") {
737
- return store;
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
- email: input.email,
1010
- time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
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.store.get(key) ?? null;
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.store.set(key, user);
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 token = await signJwt(auth2, ctx.options.secret, 7 * 24 * 60 * 60);
1026
- const exposed = await onUser(user, ctx);
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: auth2.id });
1087
+ return status(201).json({ ...exposed, token: id });
1034
1088
  }
1035
1089
  if (strategy.includes("cookie")) {
1036
- return cookies("authentication", {
1037
- value: auth2.id,
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
- }).redirect(settings.redirect);
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 callback = async (ctx) => {
1129
- const body = ctx.body || {};
1130
- checkState(ctx, body.state);
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: new URLSearchParams({
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 (body.user) {
1150
- const parsed = JSON.parse(body.user).name;
1208
+ if (user) {
1209
+ const parsed = JSON.parse(user).name;
1151
1210
  if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
1152
1211
  }
1153
- const raw = { ...claims, name };
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
- const res = await finishLogin(ctx, {
1158
- provider: "apple",
1159
- key: profile.id,
1160
- email: profile.email,
1161
- user: profile
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 apple_default = { login, callback };
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 login3 = (ctx) => {
1173
- const { state, cookie } = startState(ctx);
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
- return cookies("oauth_state", cookie).redirect(
1182
- `${config2.authorizeUrl}?${params}`
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 callback3 = async (ctx) => {
1186
- checkState(ctx, ctx.url.query.state);
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: new URLSearchParams({
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
- const raw = await profileRes.json();
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
- const res = await finishLogin(ctx, {
1215
- provider: config2.name,
1216
- key: profile.id,
1217
- email: profile.email,
1218
- user: profile
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
- return { login: login3, callback: callback3 };
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 store = ctx.options.auth.store;
1256
- if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
1257
- const user = await store.get(email);
1258
- const isValid = await verify(password, user.password);
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 store = ctx.options.auth.store;
1274
- if (await store.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
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.store.get(ctx.user.email);
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 verify(passwords.previous, fullUser.password);
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.store);
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 oauth = async (code) => {
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 login2 = (ctx) => {
1350
- const { state, cookie } = startState(ctx);
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
- return cookies("oauth_state", cookie).redirect(
1357
- `https://github.com/login/oauth/authorize?${params}`
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 callback2 = async (ctx) => {
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
- const res = await finishLogin(ctx, {
1384
- provider: "github",
1385
- key: profile.id,
1386
- email: profile.email,
1387
- user: profile
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 github_default = { login: login2, callback: callback2 };
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, all) {
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
- if (!auth2.store && !all.store) {
1466
- throw new Error("Need a userStore store for Auth");
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
- store: authStore,
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
- body: options.body ?? "parse",
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
- settings.store = options.store ? toStore(options.store) : null;
1731
- if (options.session) {
1732
- const store = typeof options.session === "object" && "store" in options.session ? options.session.store : options.session;
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, options);
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 (settings.session) log.message("session", "enabled");
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 run = (event, socket, body) => {
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) => run("message", socket, body),
1980
+ message: (socket, body) => run2("message", socket, body),
1822
1981
  open: (socket) => {
1823
1982
  sockets.push(socket);
1824
- run("open", socket);
1983
+ run2("open", socket);
1825
1984
  },
1826
1985
  close: (socket) => {
1827
1986
  sockets.splice(sockets.indexOf(socket), 1);
1828
- run("close", socket);
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
- if (Object.keys(ctx.session || {}).length) {
1942
- if (!ctx.options.session?.store) {
1943
- throw ServerError_default.NO_STORE();
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 = ctx.cookies.session;
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.session.store.set(id, ctx.session);
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/helpers/validate.ts
2013
- function validate(ctx, schema) {
2014
- if (!schema || typeof schema !== "object") return;
2015
- let base;
2016
- try {
2017
- if (typeof schema?.body === "function") {
2018
- base = "body";
2019
- schema.body(ctx.body || {});
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
- throw error;
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(ctx, ctx.options.body);
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
- if (typeof cb === "function") {
2069
- const res = await cb(ctx);
2070
- const out = await parseResponse(res, ctx);
2071
- if (out) return out;
2072
- } else {
2073
- validate(ctx, cb);
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(ctx, ctx.options.body);
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 verify(password, hash3) {
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/findSessionId.ts
2256
- var validateToken = (authorization) => {
2257
- const [type2, id] = authorization.trim().split(" ");
2258
- if (type2?.toLowerCase() !== "bearer") {
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
- if (id?.length !== 16) {
2262
- throw ServerError_default.AUTH_INVALID_TOKEN();
2263
- }
2264
- return id;
2265
- };
2266
- var validateCookie = (authorization) => {
2267
- if (authorization.length !== 16) {
2268
- throw ServerError_default.AUTH_INVALID_COOKIE();
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
- throw new Error(`Invalid auth type "${strategy}"`);
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
- const strategy = ctx.options.auth.strategy;
2289
- if (strategy.includes("jwt")) {
2290
- const header = ctx.headers.authorization;
2291
- if (!header) return;
2292
- const [type2, token] = header.trim().split(" ");
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
- const id = findSessionId(ctx);
2301
- if (!id) return;
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 ctx.options.auth.store.get(auth2.user);
2489
+ const user = await options.users.get(auth2.user);
2322
2490
  if (!user) throw ServerError_default.AUTH_NO_USER();
2323
- user.strategy = auth2.strategy;
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.user;
2333
- if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
2498
+ const { strategy } = ctx.options.auth;
2334
2499
  if (!strategy.includes("jwt")) {
2335
- await ctx.options.auth.session.del(findSessionId(ctx));
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({ authentication: null }).redirect("/");
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.fns.find((p) => typeof p === "object");
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.use(timer);
3212
- if (this.settings.cors) this.use(preflight);
3213
- this.use(assets);
3214
- if (this.settings.favicon) this.get("/favicon.ico", favicon);
3215
- this.use(session);
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(this);
3365
+ auth(app);
3218
3366
  }
3219
3367
  if (this.settings.openapi) {
3220
- this.get(this.settings.openapi.path || "/docs", openapi_default);
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,