@server/next 0.41.0 → 0.43.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 +13 -16
- package/index.js +362 -248
- package/package.json +11 -15
- package/readme.md +6 -5
package/index.d.ts
CHANGED
|
@@ -103,10 +103,13 @@ type RouteOptions = {
|
|
|
103
103
|
params?: StandardSchemaV1<any, any>;
|
|
104
104
|
response?: StandardSchemaV1<any, any>;
|
|
105
105
|
cache?: CacheOption;
|
|
106
|
+
uploads?: string | Bucket | UploadOptions | false;
|
|
106
107
|
};
|
|
107
108
|
type Route = {
|
|
108
109
|
path: string;
|
|
109
|
-
options: RouteOptions
|
|
110
|
+
options: Omit<RouteOptions, "uploads"> & {
|
|
111
|
+
uploads?: Settings["uploads"];
|
|
112
|
+
};
|
|
110
113
|
fns: Middleware[];
|
|
111
114
|
};
|
|
112
115
|
type Cookie = {
|
|
@@ -182,10 +185,9 @@ type KVStore = {
|
|
|
182
185
|
type Provider = "email" | "github" | "google" | "microsoft" | "discord" | "facebook" | "apple";
|
|
183
186
|
type Strategy = "cookie" | "jwt" | "token";
|
|
184
187
|
type AuthSession = {
|
|
185
|
-
id: string;
|
|
186
|
-
provider: Provider;
|
|
187
|
-
strategy: Strategy;
|
|
188
188
|
user: string;
|
|
189
|
+
provider: Provider;
|
|
190
|
+
created: string;
|
|
189
191
|
};
|
|
190
192
|
type AuthUser<T = Record<string, any>> = T & {
|
|
191
193
|
id: string | number;
|
|
@@ -200,22 +202,22 @@ type ProfileUser = {
|
|
|
200
202
|
type AuthOption = `${Strategy}:${Provider}` | {
|
|
201
203
|
strategy: Strategy;
|
|
202
204
|
providers?: Provider | Provider[];
|
|
203
|
-
|
|
204
|
-
store?: StoreSource;
|
|
205
|
+
users?: StoreSource;
|
|
205
206
|
redirect?: string;
|
|
206
207
|
onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
|
|
207
208
|
onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
208
209
|
onUser?: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
|
|
210
|
+
onToken?: (user: AuthUser, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
209
211
|
onLogout?: (ctx: Context) => unknown;
|
|
210
212
|
};
|
|
211
213
|
type AuthSettings = {
|
|
212
214
|
providers: Provider[];
|
|
213
215
|
strategy: Strategy;
|
|
214
|
-
|
|
215
|
-
session: KVStore;
|
|
216
|
+
users: KVStore;
|
|
216
217
|
onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
|
|
217
218
|
onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
218
219
|
onUser: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
|
|
220
|
+
onToken: (user: AuthUser, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
219
221
|
onLogout?: (ctx: Context) => unknown;
|
|
220
222
|
redirect: string;
|
|
221
223
|
};
|
|
@@ -254,10 +256,7 @@ type Options = {
|
|
|
254
256
|
secret?: string;
|
|
255
257
|
public?: string | Bucket;
|
|
256
258
|
uploads?: string | Bucket | UploadOptions;
|
|
257
|
-
|
|
258
|
-
session?: StoreSource | {
|
|
259
|
-
store: StoreSource;
|
|
260
|
-
};
|
|
259
|
+
sessions?: StoreSource;
|
|
261
260
|
cors?: CorsOptions;
|
|
262
261
|
auth?: AuthOption;
|
|
263
262
|
openapi?: any;
|
|
@@ -276,10 +275,8 @@ type Settings = {
|
|
|
276
275
|
uploads?: ({
|
|
277
276
|
bucket: Bucket;
|
|
278
277
|
} & LimitOptions) | null;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
store: KVStore;
|
|
282
|
-
};
|
|
278
|
+
sessions: KVStore;
|
|
279
|
+
sessionsDefault?: boolean;
|
|
283
280
|
cors?: CorsSettings;
|
|
284
281
|
auth?: AuthSettings;
|
|
285
282
|
openapi?: any;
|
package/index.js
CHANGED
|
@@ -43,20 +43,18 @@ 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",
|
|
53
|
+
SESSION_GUEST: "No `ctx.session` for this request (tried '{key}'): the `token` strategy carries the session in the Authorization header, and this request has none. Sign in first, or use the `cookie` strategy for guest sessions",
|
|
52
54
|
AUTH_INVALID_HEADER: {
|
|
53
55
|
status: 401,
|
|
54
56
|
message: "Invalid authorization header {type}, must send 'Bearer {TOKEN}' (with space)"
|
|
55
57
|
},
|
|
56
|
-
AUTH_INVALID_STRATEGY: {
|
|
57
|
-
status: 401,
|
|
58
|
-
message: "Invalid Authorization type '{strategy}', valid one is '{valid}'"
|
|
59
|
-
},
|
|
60
58
|
AUTH_INVALID_STATE: { status: 403, message: "Invalid OAuth state" },
|
|
61
59
|
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.providers'",
|
|
62
60
|
AUTH_INVALID_PROVIDER: {
|
|
@@ -107,6 +105,17 @@ var StatusError = class extends Error {
|
|
|
107
105
|
}
|
|
108
106
|
};
|
|
109
107
|
|
|
108
|
+
// src/helpers/bucket.ts
|
|
109
|
+
import FileSystem from "bucket/fs";
|
|
110
|
+
function bucket(root) {
|
|
111
|
+
if (!root) return null;
|
|
112
|
+
if (typeof root === "string") return FileSystem(root);
|
|
113
|
+
if (typeof root.file === "function") return root;
|
|
114
|
+
throw new Error(
|
|
115
|
+
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
// src/helpers/createId.ts
|
|
111
120
|
var alphabet = "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
112
121
|
var random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
@@ -149,6 +158,16 @@ function createId(source, size = 16) {
|
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
// src/helpers/upload.ts
|
|
161
|
+
function resolveUploads(up) {
|
|
162
|
+
if (!up) return null;
|
|
163
|
+
if (typeof up === "object" && "bucket" in up) {
|
|
164
|
+
const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
|
|
165
|
+
if (maxSize != null) parseBytes(maxSize);
|
|
166
|
+
if (minSize != null) parseBytes(minSize);
|
|
167
|
+
return { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
|
|
168
|
+
}
|
|
169
|
+
return { bucket: bucket(up) };
|
|
170
|
+
}
|
|
152
171
|
function parseBytes(value) {
|
|
153
172
|
if (typeof value === "number") return value;
|
|
154
173
|
const units = {
|
|
@@ -729,13 +748,20 @@ function clientIp(headers2, opts = {}) {
|
|
|
729
748
|
|
|
730
749
|
// src/helpers/store.ts
|
|
731
750
|
import kv from "polystore";
|
|
732
|
-
function
|
|
751
|
+
function isStore(source) {
|
|
733
752
|
const store = source;
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
753
|
+
return Boolean(
|
|
754
|
+
store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function"
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
function toStore(source) {
|
|
758
|
+
if (isStore(source)) return source;
|
|
737
759
|
return kv(source);
|
|
738
760
|
}
|
|
761
|
+
function toStoreExpiring(source, expires) {
|
|
762
|
+
if (isStore(source)) return source;
|
|
763
|
+
return kv(source).expires(expires);
|
|
764
|
+
}
|
|
739
765
|
|
|
740
766
|
// src/helpers/disposition.ts
|
|
741
767
|
var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
@@ -994,50 +1020,117 @@ async function verifyJwt(token, secret) {
|
|
|
994
1020
|
return payload;
|
|
995
1021
|
}
|
|
996
1022
|
|
|
1023
|
+
// src/auth/findSessionId.ts
|
|
1024
|
+
var validateToken = (authorization) => {
|
|
1025
|
+
const [type2, id] = authorization.trim().split(" ");
|
|
1026
|
+
if (type2?.toLowerCase() !== "bearer") {
|
|
1027
|
+
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
1028
|
+
}
|
|
1029
|
+
if (id?.length !== 16) {
|
|
1030
|
+
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
1031
|
+
}
|
|
1032
|
+
return id;
|
|
1033
|
+
};
|
|
1034
|
+
function findSessionId(ctx) {
|
|
1035
|
+
if (ctx.options.auth?.strategy.includes("token")) {
|
|
1036
|
+
if (!ctx.headers.authorization) return;
|
|
1037
|
+
return validateToken(ctx.headers.authorization);
|
|
1038
|
+
}
|
|
1039
|
+
return ctx.cookies.session || void 0;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/middle/session.ts
|
|
1043
|
+
var loaded = /* @__PURE__ */ new WeakMap();
|
|
1044
|
+
function noSession(error) {
|
|
1045
|
+
const target = {};
|
|
1046
|
+
return new Proxy(target, {
|
|
1047
|
+
get(target2, key) {
|
|
1048
|
+
if (typeof key === "symbol" || key === "then") return target2[key];
|
|
1049
|
+
throw error(String(key));
|
|
1050
|
+
},
|
|
1051
|
+
set(target2, key, value) {
|
|
1052
|
+
if (typeof key === "symbol") {
|
|
1053
|
+
target2[key] = value;
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
throw error(String(key));
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
async function session(ctx) {
|
|
1061
|
+
const strategy = ctx.options.auth?.strategy;
|
|
1062
|
+
if (strategy?.includes("jwt")) {
|
|
1063
|
+
ctx.session = noSession((key) => ServerError_default.SESSION_JWT({ key }));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const id = findSessionId(ctx);
|
|
1067
|
+
if (!id && strategy?.includes("token")) {
|
|
1068
|
+
ctx.session = noSession((key) => ServerError_default.SESSION_GUEST({ key }));
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
ctx.session = id && await ctx.options.sessions.get(id) || {};
|
|
1072
|
+
loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
997
1075
|
// src/auth/finishLogin.ts
|
|
998
|
-
async function finishLogin(ctx, input) {
|
|
1076
|
+
async function finishLogin(ctx, input, opts = {}) {
|
|
999
1077
|
const settings = ctx.options.auth;
|
|
1000
|
-
const { strategy, onLogin, onUser } = settings;
|
|
1078
|
+
const { strategy, onLogin, onUser, onToken } = settings;
|
|
1001
1079
|
const key = String(input.key);
|
|
1080
|
+
if (!strategy.includes("jwt") && !loaded.has(ctx)) {
|
|
1081
|
+
ctx.session = {};
|
|
1082
|
+
loaded.set(ctx, { id: void 0, data: "{}" });
|
|
1083
|
+
}
|
|
1002
1084
|
const auth2 = {
|
|
1003
|
-
id: createId(),
|
|
1004
|
-
strategy,
|
|
1005
|
-
provider: input.provider,
|
|
1006
1085
|
user: key,
|
|
1007
|
-
|
|
1008
|
-
|
|
1086
|
+
provider: input.provider,
|
|
1087
|
+
created: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
1009
1088
|
};
|
|
1010
1089
|
const loginUser = {
|
|
1011
1090
|
...input.user,
|
|
1012
1091
|
provider: input.provider,
|
|
1013
1092
|
strategy
|
|
1014
1093
|
};
|
|
1015
|
-
const existingUser = await settings.
|
|
1094
|
+
const existingUser = await settings.users.get(key) ?? null;
|
|
1016
1095
|
const user = onLogin ? await onLogin(loginUser, existingUser, ctx) : { ...existingUser ?? {}, ...loginUser };
|
|
1017
1096
|
assertUser(user, "onLogin");
|
|
1018
|
-
await settings.
|
|
1019
|
-
if (!strategy.includes("jwt")) {
|
|
1020
|
-
await settings.session.set(auth2.id, auth2, { expires: "1w" });
|
|
1021
|
-
}
|
|
1097
|
+
await settings.users.set(key, user);
|
|
1022
1098
|
if (strategy.includes("jwt")) {
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1099
|
+
const payload = {
|
|
1100
|
+
...await onToken(user, ctx),
|
|
1101
|
+
provider: input.provider
|
|
1102
|
+
};
|
|
1103
|
+
assertUser(payload, "onToken");
|
|
1104
|
+
const token = await signJwt(payload, ctx.options.secret, 7 * 24 * 60 * 60);
|
|
1105
|
+
const exposed = await onUser(payload, ctx);
|
|
1025
1106
|
assertUser(exposed, "onUser");
|
|
1026
1107
|
return status(201).json({ ...exposed, token });
|
|
1027
1108
|
}
|
|
1109
|
+
const prev = loaded.get(ctx);
|
|
1110
|
+
if (prev?.id) await ctx.options.sessions.del(prev.id);
|
|
1111
|
+
const id = createId();
|
|
1112
|
+
Object.assign(ctx.session, auth2);
|
|
1113
|
+
await ctx.options.sessions.set(id, ctx.session);
|
|
1114
|
+
loaded.set(ctx, { id, data: JSON.stringify(ctx.session) });
|
|
1028
1115
|
if (strategy.includes("token")) {
|
|
1029
1116
|
const exposed = await onUser(user, ctx);
|
|
1030
1117
|
assertUser(exposed, "onUser");
|
|
1031
|
-
return status(201).json({ ...exposed, token:
|
|
1118
|
+
return status(201).json({ ...exposed, token: id });
|
|
1032
1119
|
}
|
|
1033
1120
|
if (strategy.includes("cookie")) {
|
|
1034
|
-
|
|
1035
|
-
value:
|
|
1121
|
+
const reply = cookies("session", {
|
|
1122
|
+
value: id,
|
|
1036
1123
|
path: "/",
|
|
1037
1124
|
httpOnly: true,
|
|
1038
1125
|
secure: ctx.platform.production,
|
|
1039
1126
|
sameSite: "Lax"
|
|
1040
|
-
})
|
|
1127
|
+
});
|
|
1128
|
+
if (opts.json) {
|
|
1129
|
+
const exposed = await onUser(user, ctx);
|
|
1130
|
+
assertUser(exposed, "onUser");
|
|
1131
|
+
return reply.status(201).json(exposed);
|
|
1132
|
+
}
|
|
1133
|
+
return reply.redirect(settings.redirect);
|
|
1041
1134
|
}
|
|
1042
1135
|
throw new Error("Unknown auth type");
|
|
1043
1136
|
}
|
|
@@ -1123,78 +1216,111 @@ var login = (ctx) => {
|
|
|
1123
1216
|
});
|
|
1124
1217
|
return cookies("oauth_state", cookie).redirect(`${AUTHORIZE}?${params}`);
|
|
1125
1218
|
};
|
|
1126
|
-
var
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1219
|
+
var exchange = async (code, redirectUri, user) => {
|
|
1220
|
+
const params = new URLSearchParams({
|
|
1221
|
+
client_id: env.APPLE_ID,
|
|
1222
|
+
client_secret: await clientSecret(),
|
|
1223
|
+
code: code ?? "",
|
|
1224
|
+
grant_type: "authorization_code"
|
|
1225
|
+
});
|
|
1226
|
+
if (redirectUri) params.set("redirect_uri", redirectUri);
|
|
1129
1227
|
const tokenRes = await fetch(TOKEN, {
|
|
1130
1228
|
method: "POST",
|
|
1131
1229
|
headers: {
|
|
1132
1230
|
accept: "application/json",
|
|
1133
1231
|
"content-type": "application/x-www-form-urlencoded"
|
|
1134
1232
|
},
|
|
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
|
-
})
|
|
1233
|
+
body: params
|
|
1142
1234
|
});
|
|
1143
1235
|
if (!tokenRes.ok) throw new Error("apple: token exchange failed");
|
|
1144
1236
|
const token = await tokenRes.json();
|
|
1145
1237
|
const claims = b64urlJson(token.id_token.split(".")[1]);
|
|
1146
1238
|
let name;
|
|
1147
|
-
if (
|
|
1148
|
-
const parsed = JSON.parse(
|
|
1239
|
+
if (user) {
|
|
1240
|
+
const parsed = JSON.parse(user).name;
|
|
1149
1241
|
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
1150
1242
|
}
|
|
1151
|
-
|
|
1243
|
+
return { ...claims, name };
|
|
1244
|
+
};
|
|
1245
|
+
var finish = async (ctx, raw, opts) => {
|
|
1152
1246
|
const { onProfile } = ctx.options.auth;
|
|
1153
1247
|
const profile = onProfile ? await onProfile(raw, "apple") : { id: raw.sub, name: raw.name, email: raw.email };
|
|
1154
1248
|
assertUser(profile, "onProfile");
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1249
|
+
return finishLogin(
|
|
1250
|
+
ctx,
|
|
1251
|
+
{
|
|
1252
|
+
provider: "apple",
|
|
1253
|
+
key: profile.id,
|
|
1254
|
+
email: profile.email,
|
|
1255
|
+
user: profile
|
|
1256
|
+
},
|
|
1257
|
+
opts
|
|
1258
|
+
);
|
|
1259
|
+
};
|
|
1260
|
+
var callback = async (ctx) => {
|
|
1261
|
+
const body = ctx.body || {};
|
|
1262
|
+
checkState(ctx, body.state);
|
|
1263
|
+
const url = `${ctx.url.origin}/auth/callback/apple`;
|
|
1264
|
+
const raw = await exchange(body.code, url, body.user);
|
|
1265
|
+
const res = await finish(ctx, raw);
|
|
1161
1266
|
res.headers.append("set-cookie", clearState());
|
|
1162
1267
|
return res;
|
|
1163
1268
|
};
|
|
1164
|
-
var
|
|
1269
|
+
var verify = async (ctx) => {
|
|
1270
|
+
const { code, redirect_uri, user } = ctx.body ?? {};
|
|
1271
|
+
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1272
|
+
const raw = await exchange(code, redirect_uri, user);
|
|
1273
|
+
return finish(ctx, raw, { json: true });
|
|
1274
|
+
};
|
|
1275
|
+
var apple_default = { login, callback, verify };
|
|
1165
1276
|
|
|
1166
1277
|
// src/auth/providers/oauth.ts
|
|
1278
|
+
var wantsJson = (ctx) => String(ctx.headers.accept || "").includes("application/json");
|
|
1279
|
+
var clientParams = (source) => ({
|
|
1280
|
+
redirect_uri: source.redirect_uri,
|
|
1281
|
+
state: source.state,
|
|
1282
|
+
code_challenge: source.code_challenge,
|
|
1283
|
+
code_challenge_method: source.code_challenge ? "S256" : void 0
|
|
1284
|
+
});
|
|
1167
1285
|
function oauthProvider(config2) {
|
|
1168
1286
|
const KEY = config2.name.toUpperCase();
|
|
1169
1287
|
const callbackUrl = (ctx) => `${ctx.url.origin}/auth/callback/${config2.name}`;
|
|
1170
|
-
const
|
|
1171
|
-
const
|
|
1172
|
-
const params = new URLSearchParams({
|
|
1288
|
+
const authorizeUrl2 = (params) => {
|
|
1289
|
+
const search = new URLSearchParams({
|
|
1173
1290
|
client_id: env[`${KEY}_ID`],
|
|
1174
|
-
redirect_uri: callbackUrl(ctx),
|
|
1175
1291
|
response_type: "code",
|
|
1176
|
-
scope: config2.scope
|
|
1177
|
-
state
|
|
1292
|
+
scope: config2.scope
|
|
1178
1293
|
});
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1294
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1295
|
+
if (value) search.set(key, value);
|
|
1296
|
+
}
|
|
1297
|
+
return `${config2.authorizeUrl}?${search}`;
|
|
1182
1298
|
};
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1299
|
+
const login3 = (ctx) => {
|
|
1300
|
+
if (wantsJson(ctx)) {
|
|
1301
|
+
return json({ url: authorizeUrl2(clientParams(ctx.url.query)) });
|
|
1302
|
+
}
|
|
1303
|
+
const { state, cookie } = startState(ctx);
|
|
1304
|
+
const url = authorizeUrl2({ redirect_uri: callbackUrl(ctx), state });
|
|
1305
|
+
return cookies("oauth_state", cookie).redirect(url);
|
|
1306
|
+
};
|
|
1307
|
+
const exchange2 = async (ctx, code, extra) => {
|
|
1308
|
+
const body = new URLSearchParams({
|
|
1309
|
+
client_id: env[`${KEY}_ID`],
|
|
1310
|
+
client_secret: env[`${KEY}_SECRET`],
|
|
1311
|
+
code,
|
|
1312
|
+
grant_type: "authorization_code"
|
|
1313
|
+
});
|
|
1314
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
1315
|
+
if (value) body.set(key, value);
|
|
1316
|
+
}
|
|
1185
1317
|
const tokenRes = await fetch(config2.tokenUrl, {
|
|
1186
1318
|
method: "POST",
|
|
1187
1319
|
headers: {
|
|
1188
1320
|
accept: "application/json",
|
|
1189
1321
|
"content-type": "application/x-www-form-urlencoded"
|
|
1190
1322
|
},
|
|
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
|
-
})
|
|
1323
|
+
body
|
|
1198
1324
|
});
|
|
1199
1325
|
if (!tokenRes.ok) throw new Error(`${config2.name}: token exchange failed`);
|
|
1200
1326
|
const token = await tokenRes.json();
|
|
@@ -1205,20 +1331,39 @@ function oauthProvider(config2) {
|
|
|
1205
1331
|
}
|
|
1206
1332
|
});
|
|
1207
1333
|
if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
|
|
1208
|
-
|
|
1334
|
+
return profileRes.json();
|
|
1335
|
+
};
|
|
1336
|
+
const finish3 = async (ctx, raw, opts) => {
|
|
1209
1337
|
const { onProfile } = ctx.options.auth;
|
|
1210
1338
|
const profile = onProfile ? await onProfile(raw, config2.name) : config2.profile(raw);
|
|
1211
1339
|
assertUser(profile, "onProfile");
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1340
|
+
return finishLogin(
|
|
1341
|
+
ctx,
|
|
1342
|
+
{
|
|
1343
|
+
provider: config2.name,
|
|
1344
|
+
key: profile.id,
|
|
1345
|
+
email: profile.email,
|
|
1346
|
+
user: profile
|
|
1347
|
+
},
|
|
1348
|
+
opts
|
|
1349
|
+
);
|
|
1350
|
+
};
|
|
1351
|
+
const callback3 = async (ctx) => {
|
|
1352
|
+
checkState(ctx, ctx.url.query.state);
|
|
1353
|
+
const raw = await exchange2(ctx, ctx.url.query.code, {
|
|
1354
|
+
redirect_uri: callbackUrl(ctx)
|
|
1217
1355
|
});
|
|
1356
|
+
const res = await finish3(ctx, raw);
|
|
1218
1357
|
res.headers.append("set-cookie", clearState());
|
|
1219
1358
|
return res;
|
|
1220
1359
|
};
|
|
1221
|
-
|
|
1360
|
+
const verify4 = async (ctx) => {
|
|
1361
|
+
const { code, redirect_uri, code_verifier } = ctx.body ?? {};
|
|
1362
|
+
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1363
|
+
const raw = await exchange2(ctx, code, { redirect_uri, code_verifier });
|
|
1364
|
+
return finish3(ctx, raw, { json: true });
|
|
1365
|
+
};
|
|
1366
|
+
return { login: login3, callback: callback3, verify: verify4 };
|
|
1222
1367
|
}
|
|
1223
1368
|
|
|
1224
1369
|
// src/auth/providers/discord.ts
|
|
@@ -1250,10 +1395,10 @@ async function emailLogin(ctx) {
|
|
|
1250
1395
|
if (!/@/.test(email)) throw ServerError_default.LOGIN_INVALID_EMAIL();
|
|
1251
1396
|
if (!password) throw ServerError_default.LOGIN_NO_PASSWORD();
|
|
1252
1397
|
if (password.length < 8) throw ServerError_default.LOGIN_INVALID_PASSWORD();
|
|
1253
|
-
const
|
|
1254
|
-
if (!await
|
|
1255
|
-
const user = await
|
|
1256
|
-
const isValid = await
|
|
1398
|
+
const users = ctx.options.auth.users;
|
|
1399
|
+
if (!await users.has(email)) throw ServerError_default.LOGIN_WRONG_EMAIL();
|
|
1400
|
+
const user = await users.get(email);
|
|
1401
|
+
const isValid = await verify2(password, user.password);
|
|
1257
1402
|
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
1258
1403
|
return finishLogin(ctx, {
|
|
1259
1404
|
provider: "email",
|
|
@@ -1268,8 +1413,8 @@ async function emailRegister(ctx) {
|
|
|
1268
1413
|
if (!/@/.test(email)) throw ServerError_default.REGISTER_INVALID_EMAIL();
|
|
1269
1414
|
if (!password) throw ServerError_default.REGISTER_NO_PASSWORD();
|
|
1270
1415
|
if (password.length < 8) throw ServerError_default.REGISTER_INVALID_PASSWORD();
|
|
1271
|
-
const
|
|
1272
|
-
if (await
|
|
1416
|
+
const users = ctx.options.auth.users;
|
|
1417
|
+
if (await users.has(email)) throw ServerError_default.REGISTER_EMAIL_EXISTS();
|
|
1273
1418
|
const time = (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "");
|
|
1274
1419
|
const user = {
|
|
1275
1420
|
id: createId(email),
|
|
@@ -1291,12 +1436,12 @@ async function emailResetPassword() {
|
|
|
1291
1436
|
}
|
|
1292
1437
|
async function emailUpdatePassword(ctx) {
|
|
1293
1438
|
const passwords = ctx.body;
|
|
1294
|
-
const fullUser = await ctx.options.auth.
|
|
1439
|
+
const fullUser = await ctx.options.auth.users.get(ctx.user.email);
|
|
1295
1440
|
if (!fullUser) throw ServerError_default.AUTH_NO_USER();
|
|
1296
|
-
const isValid = await
|
|
1441
|
+
const isValid = await verify2(passwords.previous, fullUser.password);
|
|
1297
1442
|
if (!isValid) throw ServerError_default.LOGIN_WRONG_PASSWORD();
|
|
1298
1443
|
fullUser.password = await hash2(passwords.updated);
|
|
1299
|
-
await updateUser(fullUser, ctx.user, ctx.options.auth.
|
|
1444
|
+
await updateUser(fullUser, ctx.user, ctx.options.auth.users);
|
|
1300
1445
|
return 200;
|
|
1301
1446
|
}
|
|
1302
1447
|
var email_default = {
|
|
@@ -1322,7 +1467,8 @@ var facebook_default = oauthProvider({
|
|
|
1322
1467
|
});
|
|
1323
1468
|
|
|
1324
1469
|
// src/auth/providers/github.ts
|
|
1325
|
-
var
|
|
1470
|
+
var AUTHORIZE2 = "https://github.com/login/oauth/authorize";
|
|
1471
|
+
var oauth = async (code, extra) => {
|
|
1326
1472
|
const fch = async (url, { body, headers: headers2 = {}, ...rest } = {}) => {
|
|
1327
1473
|
headers2.accept = "application/json";
|
|
1328
1474
|
headers2["content-type"] = "application/json";
|
|
@@ -1330,13 +1476,17 @@ var oauth = async (code) => {
|
|
|
1330
1476
|
if (!res2.ok) throw new Error("Invalid request");
|
|
1331
1477
|
return res2.json();
|
|
1332
1478
|
};
|
|
1479
|
+
const params = {
|
|
1480
|
+
client_id: env.GITHUB_ID,
|
|
1481
|
+
client_secret: env.GITHUB_SECRET,
|
|
1482
|
+
code
|
|
1483
|
+
};
|
|
1484
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
1485
|
+
if (value) params[key] = value;
|
|
1486
|
+
}
|
|
1333
1487
|
const res = await fch("https://github.com/login/oauth/access_token", {
|
|
1334
1488
|
method: "post",
|
|
1335
|
-
body: JSON.stringify(
|
|
1336
|
-
client_id: env.GITHUB_ID,
|
|
1337
|
-
client_secret: env.GITHUB_SECRET,
|
|
1338
|
-
code
|
|
1339
|
-
})
|
|
1489
|
+
body: JSON.stringify(params)
|
|
1340
1490
|
});
|
|
1341
1491
|
return (path) => {
|
|
1342
1492
|
return fch(`https://api.github.com${path}`, {
|
|
@@ -1344,19 +1494,25 @@ var oauth = async (code) => {
|
|
|
1344
1494
|
});
|
|
1345
1495
|
};
|
|
1346
1496
|
};
|
|
1347
|
-
var
|
|
1348
|
-
const
|
|
1349
|
-
const params = new URLSearchParams({
|
|
1497
|
+
var authorizeUrl = (params) => {
|
|
1498
|
+
const search = new URLSearchParams({
|
|
1350
1499
|
client_id: env.GITHUB_ID,
|
|
1351
|
-
scope: "user:email"
|
|
1352
|
-
state
|
|
1500
|
+
scope: "user:email"
|
|
1353
1501
|
});
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1502
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1503
|
+
if (value) search.set(key, value);
|
|
1504
|
+
}
|
|
1505
|
+
return `${AUTHORIZE2}?${search}`;
|
|
1357
1506
|
};
|
|
1358
|
-
var
|
|
1359
|
-
|
|
1507
|
+
var login2 = (ctx) => {
|
|
1508
|
+
if (wantsJson(ctx)) {
|
|
1509
|
+
return json({ url: authorizeUrl(clientParams(ctx.url.query)) });
|
|
1510
|
+
}
|
|
1511
|
+
const { state, cookie } = startState(ctx);
|
|
1512
|
+
return cookies("oauth_state", cookie).redirect(authorizeUrl({ state }));
|
|
1513
|
+
};
|
|
1514
|
+
var getUserProfile = async (code, extra = {}) => {
|
|
1515
|
+
const api = await oauth(code, extra);
|
|
1360
1516
|
const [profile, emails] = await Promise.all([
|
|
1361
1517
|
api("/user"),
|
|
1362
1518
|
api("/user/emails")
|
|
@@ -1372,22 +1528,35 @@ var defaultProfile = (raw) => ({
|
|
|
1372
1528
|
location: raw.location,
|
|
1373
1529
|
created: raw.created_at
|
|
1374
1530
|
});
|
|
1375
|
-
var
|
|
1376
|
-
checkState(ctx, ctx.url.query.state);
|
|
1377
|
-
const raw = await getUserProfile(ctx.url.query.code);
|
|
1531
|
+
var finish2 = async (ctx, raw, opts) => {
|
|
1378
1532
|
const { onProfile } = ctx.options.auth;
|
|
1379
1533
|
const profile = onProfile ? await onProfile(raw, "github") : defaultProfile(raw);
|
|
1380
1534
|
assertUser(profile, "onProfile");
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1535
|
+
return finishLogin(
|
|
1536
|
+
ctx,
|
|
1537
|
+
{
|
|
1538
|
+
provider: "github",
|
|
1539
|
+
key: profile.id,
|
|
1540
|
+
email: profile.email,
|
|
1541
|
+
user: profile
|
|
1542
|
+
},
|
|
1543
|
+
opts
|
|
1544
|
+
);
|
|
1545
|
+
};
|
|
1546
|
+
var callback2 = async (ctx) => {
|
|
1547
|
+
checkState(ctx, ctx.url.query.state);
|
|
1548
|
+
const raw = await getUserProfile(ctx.url.query.code);
|
|
1549
|
+
const res = await finish2(ctx, raw);
|
|
1387
1550
|
res.headers.append("set-cookie", clearState());
|
|
1388
1551
|
return res;
|
|
1389
1552
|
};
|
|
1390
|
-
var
|
|
1553
|
+
var verify3 = async (ctx) => {
|
|
1554
|
+
const { code, redirect_uri, code_verifier } = ctx.body ?? {};
|
|
1555
|
+
if (!code) throw ServerError_default.AUTH_NO_CODE();
|
|
1556
|
+
const raw = await getUserProfile(code, { redirect_uri, code_verifier });
|
|
1557
|
+
return finish2(ctx, raw, { json: true });
|
|
1558
|
+
};
|
|
1559
|
+
var github_default = { login: login2, callback: callback2, verify: verify3 };
|
|
1391
1560
|
|
|
1392
1561
|
// src/auth/providers/google.ts
|
|
1393
1562
|
var google_default = oauthProvider({
|
|
@@ -1437,7 +1606,7 @@ function defaultOnUser(fullUser) {
|
|
|
1437
1606
|
return user;
|
|
1438
1607
|
}
|
|
1439
1608
|
var available = Object.keys(providers_default);
|
|
1440
|
-
function parseAuthOptions(auth2
|
|
1609
|
+
function parseAuthOptions(auth2) {
|
|
1441
1610
|
if (!auth2) return null;
|
|
1442
1611
|
if (typeof auth2 === "string") {
|
|
1443
1612
|
const [strategy2, provider] = auth2.split(":");
|
|
@@ -1460,15 +1629,8 @@ function parseAuthOptions(auth2, all) {
|
|
|
1460
1629
|
const redirect2 = auth2.redirect || defaultRedirect;
|
|
1461
1630
|
const { onProfile, onLogin, onLogout } = auth2;
|
|
1462
1631
|
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:");
|
|
1632
|
+
const onToken = auth2.onToken || defaultOnUser;
|
|
1633
|
+
const users = auth2.users ? toStore(auth2.users) : null;
|
|
1472
1634
|
return {
|
|
1473
1635
|
strategy,
|
|
1474
1636
|
providers: list,
|
|
@@ -1476,23 +1638,12 @@ function parseAuthOptions(auth2, all) {
|
|
|
1476
1638
|
onProfile,
|
|
1477
1639
|
onLogin,
|
|
1478
1640
|
onUser,
|
|
1641
|
+
onToken,
|
|
1479
1642
|
onLogout,
|
|
1480
|
-
|
|
1481
|
-
session: sessionStore
|
|
1643
|
+
users
|
|
1482
1644
|
};
|
|
1483
1645
|
}
|
|
1484
1646
|
|
|
1485
|
-
// src/helpers/bucket.ts
|
|
1486
|
-
import FileSystem from "bucket/fs";
|
|
1487
|
-
function bucket(root) {
|
|
1488
|
-
if (!root) return null;
|
|
1489
|
-
if (typeof root === "string") return FileSystem(root);
|
|
1490
|
-
if (typeof root.file === "function") return root;
|
|
1491
|
-
throw new Error(
|
|
1492
|
-
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
1647
|
// src/helpers/color.ts
|
|
1497
1648
|
var map = {
|
|
1498
1649
|
reset: 0,
|
|
@@ -1687,7 +1838,11 @@ function config(options = {}) {
|
|
|
1687
1838
|
parser: options.parser ?? "parse",
|
|
1688
1839
|
// Secure-by-default response headers + trustProxy for ctx.ip. `false` turns
|
|
1689
1840
|
// the added headers off; see resolveSecurity for the defaults.
|
|
1690
|
-
security: resolveSecurity(options.security)
|
|
1841
|
+
security: resolveSecurity(options.security),
|
|
1842
|
+
// Sessions: one record per device, exposed as ctx.session. Anything
|
|
1843
|
+
// polystore accepts works; raw sources (a Map, a Redis client) get a 1w
|
|
1844
|
+
// expiry, a built store is honored as-is, prefix and expiry included.
|
|
1845
|
+
sessions: toStoreExpiring(options.sessions ?? /* @__PURE__ */ new Map(), "1w")
|
|
1691
1846
|
};
|
|
1692
1847
|
if (options.cache !== void 0) settings.cache = options.cache;
|
|
1693
1848
|
options.cors = options.cors || env2.CORS || null;
|
|
@@ -1728,29 +1883,29 @@ function config(options = {}) {
|
|
|
1728
1883
|
}
|
|
1729
1884
|
const publicDir = options.public || env2.PUBLIC;
|
|
1730
1885
|
settings.public = publicDir ? bucket(publicDir) : null;
|
|
1731
|
-
|
|
1732
|
-
if (!up) {
|
|
1733
|
-
settings.uploads = null;
|
|
1734
|
-
} else if (typeof up === "object" && "bucket" in up) {
|
|
1735
|
-
const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
|
|
1736
|
-
if (maxSize != null) parseBytes(maxSize);
|
|
1737
|
-
if (minSize != null) parseBytes(minSize);
|
|
1738
|
-
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
|
|
1739
|
-
} else {
|
|
1740
|
-
settings.uploads = { bucket: bucket(up) };
|
|
1741
|
-
}
|
|
1886
|
+
settings.uploads = resolveUploads(options.uploads);
|
|
1742
1887
|
const favicon2 = options.favicon || env2.FAVICON;
|
|
1743
1888
|
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
|
-
}
|
|
1889
|
+
const production = env2.NODE_ENV === "production";
|
|
1890
|
+
const defaulted = options.sessions == null;
|
|
1891
|
+
settings.sessionsDefault = defaulted;
|
|
1752
1892
|
if (options.auth || env2.AUTH) {
|
|
1753
|
-
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null
|
|
1893
|
+
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null);
|
|
1894
|
+
}
|
|
1895
|
+
if (settings.auth) {
|
|
1896
|
+
if (!settings.auth.users) {
|
|
1897
|
+
if (production) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
"Auth in production needs a persistent `users` store, like auth: { ..., users: kv(redis).prefix('user:') }."
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
settings.auth.users = toStore(/* @__PURE__ */ new Map());
|
|
1903
|
+
}
|
|
1904
|
+
if (production && defaulted && !settings.auth.strategy.includes("jwt")) {
|
|
1905
|
+
throw new Error(
|
|
1906
|
+
"Auth in production needs a persistent `sessions` store, like sessions: kv(redis).prefix('session:')."
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1754
1909
|
}
|
|
1755
1910
|
if (settings.auth?.strategy.includes("jwt") && settings.secret.startsWith("unsafe-")) {
|
|
1756
1911
|
console.warn(
|
|
@@ -1774,7 +1929,7 @@ function config(options = {}) {
|
|
|
1774
1929
|
}
|
|
1775
1930
|
if (settings.public) log.message("public", loc(options.public));
|
|
1776
1931
|
if (settings.uploads) log.message("uploads", loc(options.uploads));
|
|
1777
|
-
if (
|
|
1932
|
+
if (options.sessions) log.message("sessions", "enabled");
|
|
1778
1933
|
if (settings.cors) {
|
|
1779
1934
|
const origin = settings.cors.origin === true ? "*" : String(settings.cors.origin);
|
|
1780
1935
|
log.message("cors", origin);
|
|
@@ -1885,6 +2040,14 @@ function getMachine() {
|
|
|
1885
2040
|
}
|
|
1886
2041
|
|
|
1887
2042
|
// src/parseResponse.ts
|
|
2043
|
+
var warned = false;
|
|
2044
|
+
var warnDefault = () => {
|
|
2045
|
+
if (warned) return;
|
|
2046
|
+
warned = true;
|
|
2047
|
+
console.warn(
|
|
2048
|
+
"[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:')."
|
|
2049
|
+
);
|
|
2050
|
+
};
|
|
1888
2051
|
async function parseResponse(out, ctx) {
|
|
1889
2052
|
if (!out && typeof out !== "string") return null;
|
|
1890
2053
|
if (typeof out === "function") {
|
|
@@ -1952,11 +2115,12 @@ async function parseResponse(out, ctx) {
|
|
|
1952
2115
|
if (ctx.time?.times?.length > 1) {
|
|
1953
2116
|
out.headers.set("Server-Timing", ctx.time.headers());
|
|
1954
2117
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2118
|
+
const prev = loaded.get(ctx);
|
|
2119
|
+
if (prev && JSON.stringify(ctx.session ?? {}) !== prev.data) {
|
|
2120
|
+
if (ctx.options.sessionsDefault && ctx.platform.production) {
|
|
2121
|
+
warnDefault();
|
|
1958
2122
|
}
|
|
1959
|
-
let id =
|
|
2123
|
+
let id = prev.id;
|
|
1960
2124
|
if (!id) {
|
|
1961
2125
|
id = createId();
|
|
1962
2126
|
out.headers.append(
|
|
@@ -1970,7 +2134,7 @@ async function parseResponse(out, ctx) {
|
|
|
1970
2134
|
})
|
|
1971
2135
|
);
|
|
1972
2136
|
}
|
|
1973
|
-
ctx.options.
|
|
2137
|
+
ctx.options.sessions.set(id, ctx.session);
|
|
1974
2138
|
}
|
|
1975
2139
|
if (ctx?.res?.headers) {
|
|
1976
2140
|
for (const key in ctx.res.headers) {
|
|
@@ -2252,7 +2416,7 @@ function timingSafeEqual(a, b) {
|
|
|
2252
2416
|
}
|
|
2253
2417
|
return mismatch === 0;
|
|
2254
2418
|
}
|
|
2255
|
-
async function
|
|
2419
|
+
async function verify2(password, hash3) {
|
|
2256
2420
|
if ("Bun" in globalThis) {
|
|
2257
2421
|
return Bun.password.verify(password, hash3, "argon2id");
|
|
2258
2422
|
}
|
|
@@ -2286,94 +2450,72 @@ async function verify(password, hash3) {
|
|
|
2286
2450
|
});
|
|
2287
2451
|
}
|
|
2288
2452
|
|
|
2289
|
-
// src/auth/
|
|
2290
|
-
|
|
2291
|
-
const
|
|
2292
|
-
if (
|
|
2453
|
+
// src/auth/getUser.ts
|
|
2454
|
+
async function getJwtUser(ctx) {
|
|
2455
|
+
const header = ctx.headers.authorization;
|
|
2456
|
+
if (!header) return;
|
|
2457
|
+
const [type2, token] = header.trim().split(" ");
|
|
2458
|
+
if (type2?.toLowerCase() !== "bearer" || !token) {
|
|
2293
2459
|
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2294
2460
|
}
|
|
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);
|
|
2461
|
+
const payload = await verifyJwt(token, ctx.options.secret);
|
|
2462
|
+
if (!payload) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2463
|
+
const { iat, exp, ...claims } = payload;
|
|
2464
|
+
if (!claims.id || !claims.email) throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2465
|
+
if (!ctx.options.auth.providers.includes(claims.provider)) {
|
|
2466
|
+
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
2467
|
+
provider: claims.provider,
|
|
2468
|
+
valid: ctx.options.auth.providers
|
|
2469
|
+
});
|
|
2316
2470
|
}
|
|
2317
|
-
|
|
2471
|
+
const exposed = await ctx.options.auth.onUser(claims, ctx);
|
|
2472
|
+
assertUser(exposed, "onUser");
|
|
2473
|
+
return exposed;
|
|
2318
2474
|
}
|
|
2319
|
-
|
|
2320
|
-
// src/auth/getUser.ts
|
|
2321
2475
|
async function getAuthSession(ctx) {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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;
|
|
2476
|
+
if (loaded.has(ctx)) {
|
|
2477
|
+
const session3 = ctx.session;
|
|
2478
|
+
return session3?.user ? session3 : void 0;
|
|
2333
2479
|
}
|
|
2334
2480
|
const id = findSessionId(ctx);
|
|
2335
2481
|
if (!id) return;
|
|
2336
|
-
|
|
2482
|
+
const session2 = await ctx.options.sessions.get(id);
|
|
2483
|
+
return session2?.user ? session2 : void 0;
|
|
2337
2484
|
}
|
|
2338
2485
|
async function getUser(ctx) {
|
|
2339
2486
|
if (!ctx.options.auth) return;
|
|
2340
2487
|
const options = ctx.options.auth;
|
|
2488
|
+
if (options.strategy.includes("jwt")) return getJwtUser(ctx);
|
|
2341
2489
|
const auth2 = await getAuthSession(ctx);
|
|
2342
2490
|
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
2491
|
if (!options.providers.includes(auth2.provider)) {
|
|
2350
2492
|
throw ServerError_default.AUTH_INVALID_PROVIDER({
|
|
2351
2493
|
provider: auth2.provider,
|
|
2352
2494
|
valid: options.providers
|
|
2353
2495
|
});
|
|
2354
2496
|
}
|
|
2355
|
-
const user = await
|
|
2497
|
+
const user = await options.users.get(auth2.user);
|
|
2356
2498
|
if (!user) throw ServerError_default.AUTH_NO_USER();
|
|
2357
|
-
|
|
2358
|
-
user.provider = auth2.provider;
|
|
2359
|
-
const exposed = await ctx.options.auth.onUser(user, ctx);
|
|
2499
|
+
const exposed = await options.onUser(user, ctx);
|
|
2360
2500
|
assertUser(exposed, "onUser");
|
|
2361
2501
|
return exposed;
|
|
2362
2502
|
}
|
|
2363
2503
|
|
|
2364
2504
|
// src/auth/logout.ts
|
|
2365
2505
|
async function logout(ctx) {
|
|
2366
|
-
const { strategy } = ctx.
|
|
2367
|
-
if (!strategy) throw new Error(`Invalid strategy "${strategy}"`);
|
|
2506
|
+
const { strategy } = ctx.options.auth;
|
|
2368
2507
|
if (!strategy.includes("jwt")) {
|
|
2369
|
-
|
|
2508
|
+
const prev = loaded.get(ctx);
|
|
2509
|
+
if (prev?.id) await ctx.options.sessions.del(prev.id);
|
|
2510
|
+
ctx.session = {};
|
|
2511
|
+
loaded.set(ctx, { id: void 0, data: "{}" });
|
|
2370
2512
|
}
|
|
2371
2513
|
if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
|
|
2372
2514
|
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
2373
2515
|
return { token: null };
|
|
2374
2516
|
}
|
|
2375
2517
|
if (strategy.includes("cookie")) {
|
|
2376
|
-
return cookies({
|
|
2518
|
+
return cookies({ session: null }).redirect("/");
|
|
2377
2519
|
}
|
|
2378
2520
|
throw new Error("Unknown auth type");
|
|
2379
2521
|
}
|
|
@@ -2399,6 +2541,7 @@ function auth(app) {
|
|
|
2399
2541
|
if (!env[`${key}_SECRET`]) throw new Error(`${key}_SECRET not defined`);
|
|
2400
2542
|
app.get(`/auth/login/${name}`, providers_default[name].login);
|
|
2401
2543
|
app.get(`/auth/callback/${name}`, providers_default[name].callback);
|
|
2544
|
+
app.post(`/auth/verify/${name}`, providers_default[name].verify);
|
|
2402
2545
|
}
|
|
2403
2546
|
if (enabled.includes("apple")) {
|
|
2404
2547
|
const keys = ["APPLE_ID", "APPLE_TEAM_ID", "APPLE_KEY_ID", "APPLE_PRIVATE_KEY"];
|
|
@@ -2407,6 +2550,7 @@ function auth(app) {
|
|
|
2407
2550
|
}
|
|
2408
2551
|
app.get("/auth/login/apple", providers_default.apple.login);
|
|
2409
2552
|
app.post("/auth/callback/apple", providers_default.apple.callback);
|
|
2553
|
+
app.post("/auth/verify/apple", providers_default.apple.verify);
|
|
2410
2554
|
}
|
|
2411
2555
|
if (enabled.includes("email")) {
|
|
2412
2556
|
app.post("/auth/register/email", providers_default.email.register);
|
|
@@ -2681,39 +2825,6 @@ function preflight(ctx) {
|
|
|
2681
2825
|
return 204;
|
|
2682
2826
|
}
|
|
2683
2827
|
|
|
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
2828
|
// src/middle/timer.ts
|
|
2718
2829
|
var createTime = () => {
|
|
2719
2830
|
const times2 = [["init", performance.now()]];
|
|
@@ -3128,6 +3239,9 @@ var Router = class _Router {
|
|
|
3128
3239
|
options = rest.shift();
|
|
3129
3240
|
}
|
|
3130
3241
|
checkParserConflict(options, this.settings?.parser);
|
|
3242
|
+
if (options.uploads !== void 0) {
|
|
3243
|
+
options.uploads = resolveUploads(options.uploads);
|
|
3244
|
+
}
|
|
3131
3245
|
const base = method === "socket" ? [] : this.middleware;
|
|
3132
3246
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
3133
3247
|
this.handlers[method].push({ path, options, fns });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.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).
|