@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.
- package/index.d.ts +9 -15
- package/index.js +331 -228
- package/package.json +11 -15
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
|
729
|
+
function isStore(source) {
|
|
733
730
|
const store = source;
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1024
|
-
|
|
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:
|
|
1087
|
+
return status(201).json({ ...exposed, token: id });
|
|
1032
1088
|
}
|
|
1033
1089
|
if (strategy.includes("cookie")) {
|
|
1034
|
-
|
|
1035
|
-
value:
|
|
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
|
-
})
|
|
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
|
|
1127
|
-
const
|
|
1128
|
-
|
|
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:
|
|
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 (
|
|
1148
|
-
const parsed = JSON.parse(
|
|
1208
|
+
if (user) {
|
|
1209
|
+
const parsed = JSON.parse(user).name;
|
|
1149
1210
|
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
1150
1211
|
}
|
|
1151
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
|
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
|
|
1171
|
-
const
|
|
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
|
-
|
|
1180
|
-
|
|
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
|
|
1184
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
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
|
|
1254
|
-
if (!await
|
|
1255
|
-
const user = await
|
|
1256
|
-
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);
|
|
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
|
|
1272
|
-
if (await
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
1348
|
-
const
|
|
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
|
-
|
|
1355
|
-
|
|
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
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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/
|
|
2290
|
-
|
|
2291
|
-
const
|
|
2292
|
-
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) {
|
|
2293
2451
|
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2294
2452
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2323
|
-
if (
|
|
2324
|
-
const
|
|
2325
|
-
if (!
|
|
2326
|
-
|
|
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
|
-
|
|
2335
|
-
|
|
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
|
|
2489
|
+
const user = await options.users.get(auth2.user);
|
|
2356
2490
|
if (!user) throw ServerError_default.AUTH_NO_USER();
|
|
2357
|
-
|
|
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.
|
|
2367
|
-
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
2498
|
+
const { strategy } = ctx.options.auth;
|
|
2368
2499
|
if (!strategy.includes("jwt")) {
|
|
2369
|
-
|
|
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({
|
|
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.
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
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.
|
|
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({
|
|
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 `
|
|
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, {
|
|
21
|
+
import server, { bucket } from '@server/next';
|
|
22
|
+
import { createClient } from 'redis';
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
+
const sessions = createClient({ url });
|
|
24
25
|
const uploads = bucket.S3('my-bucket', { id, key });
|
|
25
26
|
|
|
26
|
-
export default server({
|
|
27
|
+
export default server({ sessions, uploads });
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
See the [full documentation](https://serverjs.io/documentation).
|