@server/next 0.41.0 → 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 +9 -15
  2. package/index.js +331 -228
  3. package/package.json +11 -15
  4. package/readme.md +6 -5
package/index.d.ts CHANGED
@@ -182,10 +182,9 @@ type KVStore = {
182
182
  type Provider = "email" | "github" | "google" | "microsoft" | "discord" | "facebook" | "apple";
183
183
  type Strategy = "cookie" | "jwt" | "token";
184
184
  type AuthSession = {
185
- id: string;
186
- provider: Provider;
187
- strategy: Strategy;
188
185
  user: string;
186
+ provider: Provider;
187
+ created: string;
189
188
  };
190
189
  type AuthUser<T = Record<string, any>> = T & {
191
190
  id: string | number;
@@ -200,22 +199,22 @@ type ProfileUser = {
200
199
  type AuthOption = `${Strategy}:${Provider}` | {
201
200
  strategy: Strategy;
202
201
  providers?: Provider | Provider[];
203
- session?: StoreSource;
204
- store?: StoreSource;
202
+ users?: StoreSource;
205
203
  redirect?: string;
206
204
  onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
207
205
  onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
208
206
  onUser?: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
207
+ onToken?: (user: AuthUser, ctx: Context) => ProfileUser | Promise<ProfileUser>;
209
208
  onLogout?: (ctx: Context) => unknown;
210
209
  };
211
210
  type AuthSettings = {
212
211
  providers: Provider[];
213
212
  strategy: Strategy;
214
- store: KVStore;
215
- session: KVStore;
213
+ users: KVStore;
216
214
  onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
217
215
  onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
218
216
  onUser: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
217
+ onToken: (user: AuthUser, ctx: Context) => ProfileUser | Promise<ProfileUser>;
219
218
  onLogout?: (ctx: Context) => unknown;
220
219
  redirect: string;
221
220
  };
@@ -254,10 +253,7 @@ type Options = {
254
253
  secret?: string;
255
254
  public?: string | Bucket;
256
255
  uploads?: string | Bucket | UploadOptions;
257
- store?: StoreSource;
258
- session?: StoreSource | {
259
- store: StoreSource;
260
- };
256
+ sessions?: StoreSource;
261
257
  cors?: CorsOptions;
262
258
  auth?: AuthOption;
263
259
  openapi?: any;
@@ -276,10 +272,8 @@ type Settings = {
276
272
  uploads?: ({
277
273
  bucket: Bucket;
278
274
  } & LimitOptions) | null;
279
- store?: KVStore;
280
- session?: {
281
- store: KVStore;
282
- };
275
+ sessions: KVStore;
276
+ sessionsDefault?: boolean;
283
277
  cors?: CorsSettings;
284
278
  auth?: AuthSettings;
285
279
  openapi?: any;
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: {
@@ -729,13 +726,20 @@ function clientIp(headers2, opts = {}) {
729
726
 
730
727
  // src/helpers/store.ts
731
728
  import kv from "polystore";
732
- function toStore(source) {
729
+ function isStore(source) {
733
730
  const store = source;
734
- if (store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function") {
735
- return store;
736
- }
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;
737
737
  return kv(source);
738
738
  }
739
+ function toStoreExpiring(source, expires) {
740
+ if (isStore(source)) return source;
741
+ return kv(source).expires(expires);
742
+ }
739
743
 
740
744
  // src/helpers/disposition.ts
741
745
  var encodeExt = (name) => encodeURIComponent(name).replace(
@@ -994,50 +998,108 @@ async function verifyJwt(token, secret) {
994
998
  return payload;
995
999
  }
996
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
+
997
1048
  // src/auth/finishLogin.ts
998
- async function finishLogin(ctx, input) {
1049
+ async function finishLogin(ctx, input, opts = {}) {
999
1050
  const settings = ctx.options.auth;
1000
- const { strategy, onLogin, onUser } = settings;
1051
+ const { strategy, onLogin, onUser, onToken } = settings;
1001
1052
  const key = String(input.key);
1002
1053
  const auth2 = {
1003
- id: createId(),
1004
- strategy,
1005
- provider: input.provider,
1006
1054
  user: key,
1007
- email: input.email,
1008
- time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
1055
+ provider: input.provider,
1056
+ created: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
1009
1057
  };
1010
1058
  const loginUser = {
1011
1059
  ...input.user,
1012
1060
  provider: input.provider,
1013
1061
  strategy
1014
1062
  };
1015
- const existingUser = await settings.store.get(key) ?? null;
1063
+ const existingUser = await settings.users.get(key) ?? null;
1016
1064
  const user = onLogin ? await onLogin(loginUser, existingUser, ctx) : { ...existingUser ?? {}, ...loginUser };
1017
1065
  assertUser(user, "onLogin");
1018
- await settings.store.set(key, user);
1019
- if (!strategy.includes("jwt")) {
1020
- await settings.session.set(auth2.id, auth2, { expires: "1w" });
1021
- }
1066
+ await settings.users.set(key, user);
1022
1067
  if (strategy.includes("jwt")) {
1023
- const token = await signJwt(auth2, ctx.options.secret, 7 * 24 * 60 * 60);
1024
- 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);
1025
1075
  assertUser(exposed, "onUser");
1026
1076
  return status(201).json({ ...exposed, token });
1027
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) });
1028
1084
  if (strategy.includes("token")) {
1029
1085
  const exposed = await onUser(user, ctx);
1030
1086
  assertUser(exposed, "onUser");
1031
- return status(201).json({ ...exposed, token: auth2.id });
1087
+ return status(201).json({ ...exposed, token: id });
1032
1088
  }
1033
1089
  if (strategy.includes("cookie")) {
1034
- return cookies("authentication", {
1035
- value: auth2.id,
1090
+ const reply = cookies("session", {
1091
+ value: id,
1036
1092
  path: "/",
1037
1093
  httpOnly: true,
1038
1094
  secure: ctx.platform.production,
1039
1095
  sameSite: "Lax"
1040
- }).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);
1041
1103
  }
1042
1104
  throw new Error("Unknown auth type");
1043
1105
  }
@@ -1123,78 +1185,111 @@ var login = (ctx) => {
1123
1185
  });
1124
1186
  return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
1125
1187
  };
1126
- var callback = async (ctx) => {
1127
- const body = ctx.body || {};
1128
- 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);
1129
1196
  const tokenRes = await fetch(TOKEN, {
1130
1197
  method: "POST",
1131
1198
  headers: {
1132
1199
  accept: "application/json",
1133
1200
  "content-type": "application/x-www-form-urlencoded"
1134
1201
  },
1135
- body: new URLSearchParams({
1136
- client_id: env.APPLE_ID,
1137
- client_secret: await clientSecret(),
1138
- code: body.code,
1139
- grant_type: "authorization_code",
1140
- redirect_uri: `${ctx.url.origin}/auth/callback/apple`
1141
- })
1202
+ body: params
1142
1203
  });
1143
1204
  if (!tokenRes.ok) throw new Error("apple: token exchange failed");
1144
1205
  const token = await tokenRes.json();
1145
1206
  const claims = b64urlJson(token.id_token.split(".")[1]);
1146
1207
  let name;
1147
- if (body.user) {
1148
- const parsed = JSON.parse(body.user).name;
1208
+ if (user) {
1209
+ const parsed = JSON.parse(user).name;
1149
1210
  if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
1150
1211
  }
1151
- const raw = { ...claims, name };
1212
+ return { ...claims, name };
1213
+ };
1214
+ var finish = async (ctx, raw, opts) => {
1152
1215
  const { onProfile } = ctx.options.auth;
1153
1216
  const profile = onProfile ? await onProfile(raw, "apple") : { id: raw.sub, name: raw.name, email: raw.email };
1154
1217
  assertUser(profile, "onProfile");
1155
- const res = await finishLogin(ctx, {
1156
- provider: "apple",
1157
- key: profile.id,
1158
- email: profile.email,
1159
- user: profile
1160
- });
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);
1161
1235
  res.headers.append("set-cookie", clearState());
1162
1236
  return res;
1163
1237
  };
1164
- 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 };
1165
1245
 
1166
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
+ });
1167
1254
  function oauthProvider(config2) {
1168
1255
  const KEY = config2.name.toUpperCase();
1169
1256
  const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
1170
- const login3 = (ctx) => {
1171
- const { state, cookie } = startState(ctx);
1172
- const params = new URLSearchParams({
1257
+ const authorizeUrl2 = (params) => {
1258
+ const search = new URLSearchParams({
1173
1259
  client_id: env[`${KEY}_ID`],
1174
- redirect_uri: callbackUrl(ctx),
1175
1260
  response_type: "code",
1176
- scope: config2.scope,
1177
- state
1261
+ scope: config2.scope
1178
1262
  });
1179
- return cookies("oauth_state", cookie).redirect(
1180
- `${config2.authorizeUrl}?${params}`
1181
- );
1263
+ for (const [key, value] of Object.entries(params)) {
1264
+ if (value) search.set(key, value);
1265
+ }
1266
+ return `${config2.authorizeUrl}?${search}`;
1182
1267
  };
1183
- const callback3 = async (ctx) => {
1184
- 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
+ }
1185
1286
  const tokenRes = await fetch(config2.tokenUrl, {
1186
1287
  method: "POST",
1187
1288
  headers: {
1188
1289
  accept: "application/json",
1189
1290
  "content-type": "application/x-www-form-urlencoded"
1190
1291
  },
1191
- body: new URLSearchParams({
1192
- client_id: env[`${KEY}_ID`],
1193
- client_secret: env[`${KEY}_SECRET`],
1194
- code: ctx.url.query.code,
1195
- grant_type: "authorization_code",
1196
- redirect_uri: callbackUrl(ctx)
1197
- })
1292
+ body
1198
1293
  });
1199
1294
  if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
1200
1295
  const token = await tokenRes.json();
@@ -1205,20 +1300,39 @@ function oauthProvider(config2) {
1205
1300
  }
1206
1301
  });
1207
1302
  if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
1208
- const raw = await profileRes.json();
1303
+ return profileRes.json();
1304
+ };
1305
+ const finish3 = async (ctx, raw, opts) => {
1209
1306
  const { onProfile } = ctx.options.auth;
1210
1307
  const profile = onProfile ? await onProfile(raw, config2.name) : config2.profile(raw);
1211
1308
  assertUser(profile, "onProfile");
1212
- const res = await finishLogin(ctx, {
1213
- provider: config2.name,
1214
- key: profile.id,
1215
- email: profile.email,
1216
- 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)
1217
1324
  });
1325
+ const res = await finish3(ctx, raw);
1218
1326
  res.headers.append("set-cookie", clearState());
1219
1327
  return res;
1220
1328
  };
1221
- 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 };
1222
1336
  }
1223
1337
 
1224
1338
  // src/auth/providers/discord.ts
@@ -1250,10 +1364,10 @@ async function emailLogin(ctx) {
1250
1364
  if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
1251
1365
  if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
1252
1366
  if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
1253
- const store = ctx.options.auth.store;
1254
- if (!await store.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
1255
- const user = await store.get(email);
1256
- 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);
1257
1371
  if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
1258
1372
  return finishLogin(ctx, {
1259
1373
  provider: "email",
@@ -1268,8 +1382,8 @@ async function emailRegister(ctx) {
1268
1382
  if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
1269
1383
  if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
1270
1384
  if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
1271
- const store = ctx.options.auth.store;
1272
- 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();
1273
1387
  const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
1274
1388
  const user = {
1275
1389
  id: createId(email),
@@ -1291,12 +1405,12 @@ async function emailResetPassword() {
1291
1405
  }
1292
1406
  async function emailUpdatePassword(ctx) {
1293
1407
  const passwords = ctx.body;
1294
- const fullUser = await ctx.options.auth.store.get(ctx.user.email);
1408
+ const fullUser = await ctx.options.auth.users.get(ctx.user.email);
1295
1409
  if (!fullUser) throw ServerError_default.AUTH_NO_USER();
1296
- const isValid = await verify(passwords.previous, fullUser.password);
1410
+ const isValid = await verify2(passwords.previous, fullUser.password);
1297
1411
  if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
1298
1412
  fullUser.password = await hash2(passwords.updated);
1299
- await updateUser(fullUser, ctx.user, ctx.options.auth.store);
1413
+ await updateUser(fullUser, ctx.user, ctx.options.auth.users);
1300
1414
  return 200;
1301
1415
  }
1302
1416
  var email_default = {
@@ -1322,7 +1436,8 @@ var facebook_default = oauthProvider({
1322
1436
  });
1323
1437
 
1324
1438
  // src/auth/providers/github.ts
1325
- var oauth = async (code) => {
1439
+ var AUTHORIZE2 = "https://github.com/login/oauth/authorize";
1440
+ var oauth = async (code, extra) => {
1326
1441
  const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
1327
1442
  headers2.accept = "application/json";
1328
1443
  headers2["content-type"] = "application/json";
@@ -1330,13 +1445,17 @@ var oauth = async (code) => {
1330
1445
  if (!res2.ok) throw new Error("Invalid request");
1331
1446
  return res2.json();
1332
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
+ }
1333
1456
  const res = await fch("https://github.com/login/oauth/access_token", {
1334
1457
  method: "post",
1335
- body: JSON.stringify({
1336
- client_id: env.GITHUB_ID,
1337
- client_secret: env.GITHUB_SECRET,
1338
- code
1339
- })
1458
+ body: JSON.stringify(params)
1340
1459
  });
1341
1460
  return (path) => {
1342
1461
  return fch(`https://api.github.com${path}`, {
@@ -1344,19 +1463,25 @@ var oauth = async (code) => {
1344
1463
  });
1345
1464
  };
1346
1465
  };
1347
- var login2 = (ctx) => {
1348
- const { state, cookie } = startState(ctx);
1349
- const params = new URLSearchParams({
1466
+ var authorizeUrl = (params) => {
1467
+ const search = new URLSearchParams({
1350
1468
  client_id: env.GITHUB_ID,
1351
- scope: "user:email",
1352
- state
1469
+ scope: "user:email"
1353
1470
  });
1354
- return cookies("oauth_state", cookie).redirect(
1355
- `https://github.com/login/oauth/authorize?${params}`
1356
- );
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 }));
1357
1482
  };
1358
- var getUserProfile = async (code) => {
1359
- const api = await oauth(code);
1483
+ var getUserProfile = async (code, extra = {}) => {
1484
+ const api = await oauth(code, extra);
1360
1485
  const [profile, emails] = await Promise.all([
1361
1486
  api("/user"),
1362
1487
  api("/user/emails")
@@ -1372,22 +1497,35 @@ var defaultProfile = (raw) => ({
1372
1497
  location: raw.location,
1373
1498
  created: raw.created_at
1374
1499
  });
1375
- var callback2 = async (ctx) => {
1376
- checkState(ctx, ctx.url.query.state);
1377
- const raw = await getUserProfile(ctx.url.query.code);
1500
+ var finish2 = async (ctx, raw, opts) => {
1378
1501
  const { onProfile } = ctx.options.auth;
1379
1502
  const profile = onProfile ? await onProfile(raw, "github") : defaultProfile(raw);
1380
1503
  assertUser(profile, "onProfile");
1381
- const res = await finishLogin(ctx, {
1382
- provider: "github",
1383
- key: profile.id,
1384
- email: profile.email,
1385
- user: profile
1386
- });
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);
1387
1519
  res.headers.append("set-cookie", clearState());
1388
1520
  return res;
1389
1521
  };
1390
- 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 };
1391
1529
 
1392
1530
  // src/auth/providers/google.ts
1393
1531
  var google_default = oauthProvider({
@@ -1437,7 +1575,7 @@ function defaultOnUser(fullUser) {
1437
1575
  return user;
1438
1576
  }
1439
1577
  var available = Object.keys(providers_default);
1440
- function parseAuthOptions(auth2, all) {
1578
+ function parseAuthOptions(auth2) {
1441
1579
  if (!auth2) return null;
1442
1580
  if (typeof auth2 === "string") {
1443
1581
  const [strategy2, provider] = auth2.split(":");
@@ -1460,15 +1598,8 @@ function parseAuthOptions(auth2, all) {
1460
1598
  const redirect2 = auth2.redirect || defaultRedirect;
1461
1599
  const { onProfile, onLogin, onLogout } = auth2;
1462
1600
  const onUser = auth2.onUser || defaultOnUser;
1463
- if (!auth2.store && !all.store) {
1464
- throw new Error("Need a userStore store for Auth");
1465
- }
1466
- if (!auth2.session && !all.store) {
1467
- throw new Error("Need a sessionStore store for Auth");
1468
- }
1469
- const store = all.store ? toStore(all.store) : null;
1470
- const authStore = auth2.store ? toStore(auth2.store) : store.prefix("user:");
1471
- 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;
1472
1603
  return {
1473
1604
  strategy,
1474
1605
  providers: list,
@@ -1476,9 +1607,9 @@ function parseAuthOptions(auth2, all) {
1476
1607
  onProfile,
1477
1608
  onLogin,
1478
1609
  onUser,
1610
+ onToken,
1479
1611
  onLogout,
1480
- store: authStore,
1481
- session: sessionStore
1612
+ users
1482
1613
  };
1483
1614
  }
1484
1615
 
@@ -1687,7 +1818,11 @@ function config(options = {}) {
1687
1818
  parser: options.parser ?? "parse",
1688
1819
  // Secure-by-default response headers + trustProxy for ctx.ip. `false` turns
1689
1820
  // the added headers off; see resolveSecurity for the defaults.
1690
- 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")
1691
1826
  };
1692
1827
  if (options.cache !== void 0) settings.cache = options.cache;
1693
1828
  options.cors = options.cors || env2.CORS || null;
@@ -1741,16 +1876,26 @@ function config(options = {}) {
1741
1876
  }
1742
1877
  const favicon2 = options.favicon || env2.FAVICON;
1743
1878
  if (favicon2) settings.favicon = favicon2;
1744
- settings.store = options.store ? toStore(options.store) : null;
1745
- if (options.session) {
1746
- const store = typeof options.session === "object" && "store" in options.session ? options.session.store : options.session;
1747
- settings.session = { store: toStore(store) };
1748
- }
1749
- if (settings.store && !options.session) {
1750
- settings.session = { store: settings.store.prefix("session:") };
1751
- }
1879
+ const production = env2.NODE_ENV === "production";
1880
+ const defaulted = options.sessions == null;
1881
+ settings.sessionsDefault = defaulted;
1752
1882
  if (options.auth || env2.AUTH) {
1753
- 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
+ }
1754
1899
  }
1755
1900
  if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
1756
1901
  console.warn(
@@ -1774,7 +1919,7 @@ function config(options = {}) {
1774
1919
  }
1775
1920
  if (settings.public) log.message("public", loc(options.public));
1776
1921
  if (settings.uploads) log.message("uploads", loc(options.uploads));
1777
- if (settings.session) log.message("session", "enabled");
1922
+ if (options.sessions) log.message("sessions", "enabled");
1778
1923
  if (settings.cors) {
1779
1924
  const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
1780
1925
  log.message("cors", origin);
@@ -1885,6 +2030,14 @@ function getMachine() {
1885
2030
  }
1886
2031
 
1887
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
+ };
1888
2041
  async function parseResponse(out, ctx) {
1889
2042
  if (!out && typeof out !== "string") return null;
1890
2043
  if (typeof out === "function") {
@@ -1952,11 +2105,14 @@ async function parseResponse(out, ctx) {
1952
2105
  if (ctx.time?.times?.length > 1) {
1953
2106
  out.headers.set("Server-Timing", ctx.time.headers());
1954
2107
  }
1955
- if (Object.keys(ctx.session || {}).length) {
1956
- if (!ctx.options.session?.store) {
1957
- 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();
1958
2114
  }
1959
- let id = ctx.cookies.session;
2115
+ let id = prev?.id;
1960
2116
  if (!id) {
1961
2117
  id = createId();
1962
2118
  out.headers.append(
@@ -1970,7 +2126,7 @@ async function parseResponse(out, ctx) {
1970
2126
  })
1971
2127
  );
1972
2128
  }
1973
- ctx.options.session.store.set(id, ctx.session);
2129
+ ctx.options.sessions.set(id, ctx.session);
1974
2130
  }
1975
2131
  if (ctx?.res?.headers) {
1976
2132
  for (const key in ctx.res.headers) {
@@ -2252,7 +2408,7 @@ function timingSafeEqual(a, b) {
2252
2408
  }
2253
2409
  return mismatch === 0;
2254
2410
  }
2255
- async function verify(password, hash3) {
2411
+ async function verify2(password, hash3) {
2256
2412
  if ("Bun" in globalThis) {
2257
2413
  return Bun.password.verify(password, hash3, "argon2id");
2258
2414
  }
@@ -2286,94 +2442,72 @@ async function verify(password, hash3) {
2286
2442
  });
2287
2443
  }
2288
2444
 
2289
- // src/auth/findSessionId.ts
2290
- var validateToken = (authorization) => {
2291
- const [type2, id] = authorization.trim().split(" ");
2292
- 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) {
2293
2451
  throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
2294
2452
  }
2295
- if (id?.length !== 16) {
2296
- throw ServerError_default.AUTH_INVALID_TOKEN();
2297
- }
2298
- return id;
2299
- };
2300
- var validateCookie = (authorization) => {
2301
- if (authorization.length !== 16) {
2302
- throw ServerError_default.AUTH_INVALID_COOKIE();
2303
- }
2304
- return authorization;
2305
- };
2306
- function findSessionId(ctx) {
2307
- const strategy = ctx.options.auth.strategy;
2308
- if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
2309
- if (strategy.includes("token")) {
2310
- if (!ctx.headers.authorization) return;
2311
- return validateToken(ctx.headers.authorization);
2312
- }
2313
- if (strategy.includes("cookie")) {
2314
- if (!ctx.cookies.authentication) return;
2315
- 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
+ });
2316
2462
  }
2317
- throw new Error(`Invalid auth type "${strategy}"`);
2463
+ const exposed = await ctx.options.auth.onUser(claims, ctx);
2464
+ assertUser(exposed, "onUser");
2465
+ return exposed;
2318
2466
  }
2319
-
2320
- // src/auth/getUser.ts
2321
2467
  async function getAuthSession(ctx) {
2322
- const strategy = ctx.options.auth.strategy;
2323
- if (strategy.includes("jwt")) {
2324
- const header = ctx.headers.authorization;
2325
- if (!header) return;
2326
- const [type2, token] = header.trim().split(" ");
2327
- if (type2?.toLowerCase() !== "bearer" || !token) {
2328
- throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
2329
- }
2330
- const payload = await verifyJwt(token, ctx.options.secret);
2331
- if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
2332
- 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;
2333
2473
  }
2334
- const id = findSessionId(ctx);
2335
- if (!id) return;
2336
- return ctx.options.auth.session.get(id);
2474
+ if (!session2?.user) return;
2475
+ return session2;
2337
2476
  }
2338
2477
  async function getUser(ctx) {
2339
2478
  if (!ctx.options.auth) return;
2340
2479
  const options = ctx.options.auth;
2480
+ if (options.strategy.includes("jwt")) return getJwtUser(ctx);
2341
2481
  const auth2 = await getAuthSession(ctx);
2342
2482
  if (!auth2) return;
2343
- if (options.strategy !== auth2.strategy) {
2344
- throw ServerError_default.AUTH_INVALID_STRATEGY({
2345
- strategy: auth2.strategy || "undefined",
2346
- valid: options.strategy
2347
- });
2348
- }
2349
2483
  if (!options.providers.includes(auth2.provider)) {
2350
2484
  throw ServerError_default.AUTH_INVALID_PROVIDER({
2351
2485
  provider: auth2.provider,
2352
2486
  valid: options.providers
2353
2487
  });
2354
2488
  }
2355
- const user = await ctx.options.auth.store.get(auth2.user);
2489
+ const user = await options.users.get(auth2.user);
2356
2490
  if (!user) throw ServerError_default.AUTH_NO_USER();
2357
- user.strategy = auth2.strategy;
2358
- user.provider = auth2.provider;
2359
- const exposed = await ctx.options.auth.onUser(user, ctx);
2491
+ const exposed = await options.onUser(user, ctx);
2360
2492
  assertUser(exposed, "onUser");
2361
2493
  return exposed;
2362
2494
  }
2363
2495
 
2364
2496
  // src/auth/logout.ts
2365
2497
  async function logout(ctx) {
2366
- const { strategy } = ctx.user;
2367
- if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
2498
+ const { strategy } = ctx.options.auth;
2368
2499
  if (!strategy.includes("jwt")) {
2369
- 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: "{}" });
2370
2504
  }
2371
2505
  if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
2372
2506
  if (strategy.includes("token") || strategy.includes("jwt")) {
2373
2507
  return { token: null };
2374
2508
  }
2375
2509
  if (strategy.includes("cookie")) {
2376
- return cookies({ authentication: null }).redirect("/");
2510
+ return cookies({ session: null }).redirect("/");
2377
2511
  }
2378
2512
  throw new Error("Unknown auth type");
2379
2513
  }
@@ -2399,6 +2533,7 @@ function auth(app) {
2399
2533
  if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
2400
2534
  app.get(`/auth/login/${name}`, providers_default[name].login);
2401
2535
  app.get(`/auth/callback/${name}`, providers_default[name].callback);
2536
+ app.post(`/auth/verify/${name}`, providers_default[name].verify);
2402
2537
  }
2403
2538
  if (enabled.includes("apple")) {
2404
2539
  const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
@@ -2407,6 +2542,7 @@ function auth(app) {
2407
2542
  }
2408
2543
  app.get("/auth/login/apple", providers_default.apple.login);
2409
2544
  app.post("/auth/callback/apple", providers_default.apple.callback);
2545
+ app.post("/auth/verify/apple", providers_default.apple.verify);
2410
2546
  }
2411
2547
  if (enabled.includes("email")) {
2412
2548
  app.post("/auth/register/email", providers_default.email.register);
@@ -2681,39 +2817,6 @@ function preflight(ctx) {
2681
2817
  return 204;
2682
2818
  }
2683
2819
 
2684
- // src/middle/NoSession.ts
2685
- var NoSession = class {
2686
- };
2687
- function createNoSession() {
2688
- return new Proxy(NoSession, {
2689
- get(target, key) {
2690
- if (target[key]) return target[key];
2691
- if (key === "then") return target[key];
2692
- if (typeof key === "symbol") return target[key];
2693
- throw ServerError_default.NO_STORE_READ({ key: String(key) });
2694
- },
2695
- set(target, key, value) {
2696
- if (target[key] || key === "then" || typeof key === "symbol") {
2697
- target[key] = value;
2698
- return true;
2699
- }
2700
- throw ServerError_default.NO_STORE_WRITE({ key: String(key) });
2701
- }
2702
- });
2703
- }
2704
-
2705
- // src/middle/session.ts
2706
- async function session(ctx) {
2707
- const store = ctx.options.session?.store;
2708
- if (!store) {
2709
- ctx.session = createNoSession();
2710
- return;
2711
- }
2712
- if (ctx.cookies.session) {
2713
- ctx.session = await store.get(ctx.cookies.session) || {};
2714
- }
2715
- }
2716
-
2717
2820
  // src/middle/timer.ts
2718
2821
  var createTime = () => {
2719
2822
  const times2 = [["init", performance.now()]];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "A fully-fledged web server with routing, file uploads, sessions, static files, schema validation, websockets, testing, etc.",
5
5
  "homepage": "https://server-js.com/",
6
6
  "repository": "github:franciscop/server-next",
@@ -47,25 +47,21 @@
47
47
  "Github": "https://github.com/franciscop/server-next"
48
48
  },
49
49
  "documentation": {
50
- "Documentation": [
51
- "docs/0. Documentation.md",
52
- "docs/1. Getting Started.md"
53
- ],
54
- "Guides": "docs/2. Guides.md",
55
- "Options": "docs/3. Options.md",
56
- "Router": "docs/4. Router.md",
57
- "Context": "docs/5. Context.md",
58
- "Reply": "docs/6. Reply.md",
59
- "Authentication": "docs/7. Authentication.md",
60
- "Testing": "docs/8. Testing.md",
61
- "Platforms": "docs/9. Platforms.md",
62
- "FAQ": "docs/A. FAQ.md"
50
+ "Documentation": "docs/0. Documentation.md",
51
+ "Options": "docs/1. Options.md",
52
+ "Router": "docs/2. Router.md",
53
+ "Context": "docs/3. Context.md",
54
+ "Reply": "docs/4. Reply.md",
55
+ "Authentication": "docs/5. Authentication.md",
56
+ "Testing": "docs/6. Testing.md",
57
+ "Platforms": "docs/7. Platforms.md",
58
+ "FAQ": "docs/8. FAQ.md"
63
59
  },
64
60
  "tutorials": "docs/tutorials"
65
61
  },
66
62
  "dependencies": {
67
63
  "bucket": "^0.7.1",
68
- "polystore": "^0.24.0"
64
+ "polystore": "^0.26.0"
69
65
  },
70
66
  "devDependencies": {
71
67
  "@types/bun": "^1.3.0",
package/readme.md CHANGED
@@ -9,21 +9,22 @@ npm install @server/next
9
9
  ```js
10
10
  import server from '@server/next';
11
11
 
12
- export default server({ store: new Map(), uploads: './uploads' })
12
+ export default server({ uploads: './uploads' })
13
13
  .get('/', () => 'Hello world')
14
14
  .get('/users/:id', (ctx) => db.users.find(ctx.url.params.id))
15
15
  .post('/avatar', (ctx) => ctx.body.avatar.path);
16
16
  ```
17
17
 
18
- Key-value stores and file storage come included, so `store` takes a plain `Map` and `uploads` takes a folder path. For Redis, S3 and the rest, `kv` and `bucket` are exported too:
18
+ Key-value stores and file storage come included, so `sessions` work out of the box and `uploads` takes a folder path. For Redis, S3 and the rest, pass the client straight in:
19
19
 
20
20
  ```js
21
- import server, { kv, bucket } from '@server/next';
21
+ import server, { bucket } from '@server/next';
22
+ import { createClient } from 'redis';
22
23
 
23
- const store = kv(createClient({ url }).connect());
24
+ const sessions = createClient({ url });
24
25
  const uploads = bucket.S3('my-bucket', { id, key });
25
26
 
26
- export default server({ store, uploads });
27
+ export default server({ sessions, uploads });
27
28
  ```
28
29
 
29
30
  See the [full documentation](https://serverjs.io/documentation).