@server/next 0.38.0 → 0.40.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 +23 -14
- package/index.js +160 -245
- package/package.json +6 -11
- package/readme.md +28 -0
package/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
|
+
export { default as kv } from 'polystore';
|
|
3
|
+
export { default as bucket } from 'bucket';
|
|
2
4
|
|
|
3
5
|
type LimitOptions = {
|
|
4
6
|
maxSize?: number | string;
|
|
@@ -128,6 +130,7 @@ type BasicValue = string | number | boolean | null;
|
|
|
128
130
|
type SerializableValue = BasicValue | {
|
|
129
131
|
[key: string]: SerializableValue;
|
|
130
132
|
} | Array<SerializableValue>;
|
|
133
|
+
type StoreSource = KVStore | Map<string, any> | string | Record<string, any>;
|
|
131
134
|
type KVStore = {
|
|
132
135
|
name?: string;
|
|
133
136
|
prefix: (prefix?: string) => KVStore;
|
|
@@ -140,7 +143,7 @@ type KVStore = {
|
|
|
140
143
|
keys: () => Promise<string[]>;
|
|
141
144
|
};
|
|
142
145
|
type Provider = "email" | "github" | "google" | "microsoft" | "discord" | "facebook" | "apple";
|
|
143
|
-
type Strategy = "cookie" | "jwt" | "token"
|
|
146
|
+
type Strategy = "cookie" | "jwt" | "token";
|
|
144
147
|
type AuthSession = {
|
|
145
148
|
id: string;
|
|
146
149
|
provider: Provider;
|
|
@@ -153,22 +156,30 @@ type AuthUser<T = Record<string, any>> = T & {
|
|
|
153
156
|
strategy: Strategy;
|
|
154
157
|
email: string;
|
|
155
158
|
};
|
|
156
|
-
type
|
|
159
|
+
type ProfileUser = {
|
|
160
|
+
id: string | number;
|
|
161
|
+
email: string;
|
|
162
|
+
} & Record<string, any>;
|
|
163
|
+
type AuthOption = `${Strategy}:${Provider}` | {
|
|
157
164
|
strategy: Strategy;
|
|
158
165
|
providers?: Provider | Provider[];
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
store?: KVStore;
|
|
166
|
+
session?: StoreSource;
|
|
167
|
+
store?: StoreSource;
|
|
162
168
|
redirect?: string;
|
|
163
|
-
|
|
169
|
+
onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
|
|
170
|
+
onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
171
|
+
onUser?: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
|
|
172
|
+
onLogout?: (ctx: Context) => unknown;
|
|
164
173
|
};
|
|
165
174
|
type AuthSettings = {
|
|
166
175
|
providers: Provider[];
|
|
167
176
|
strategy: Strategy;
|
|
168
177
|
store: KVStore;
|
|
169
178
|
session: KVStore;
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
onProfile?: (raw: any, provider: Provider) => ProfileUser | Promise<ProfileUser>;
|
|
180
|
+
onLogin?: (loginUser: AuthUser, existingUser: AuthUser | null, ctx: Context) => ProfileUser | Promise<ProfileUser>;
|
|
181
|
+
onUser: <T = AuthUser>(user: T, ctx: Context) => T | Promise<T>;
|
|
182
|
+
onLogout?: (ctx: Context) => unknown;
|
|
172
183
|
redirect: string;
|
|
173
184
|
};
|
|
174
185
|
type LogLevel = "info";
|
|
@@ -204,10 +215,9 @@ type Options = {
|
|
|
204
215
|
secret?: string;
|
|
205
216
|
public?: string | Bucket;
|
|
206
217
|
uploads?: string | Bucket | UploadOptions;
|
|
207
|
-
store?:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
store: KVStore;
|
|
218
|
+
store?: StoreSource;
|
|
219
|
+
session?: StoreSource | {
|
|
220
|
+
store: StoreSource;
|
|
211
221
|
};
|
|
212
222
|
cors?: CorsOptions;
|
|
213
223
|
auth?: AuthOption;
|
|
@@ -228,7 +238,6 @@ type Settings = {
|
|
|
228
238
|
bucket: Bucket;
|
|
229
239
|
} & LimitOptions) | null;
|
|
230
240
|
store?: KVStore;
|
|
231
|
-
cookies?: KVStore;
|
|
232
241
|
session?: {
|
|
233
242
|
store: KVStore;
|
|
234
243
|
};
|
|
@@ -500,4 +509,4 @@ declare class Server<O extends ServerConfig = {}> extends Router<O> {
|
|
|
500
509
|
}
|
|
501
510
|
declare function server<Session extends Record<string, any> = {}, User extends Record<string, any> = {}>(options?: Options): Server<ServerConfig<Session, User>>;
|
|
502
511
|
|
|
503
|
-
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type Strategy, type Time, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
|
|
512
|
+
export { type AuthOption, type AuthSession, type AuthSettings, type AuthUser, type BasicValue, type Body, type BodyMode, type BodyOption, type Bucket, type BucketFile, type BunEnv, type CacheOption, type Context, type Cookie, type CorsSettings, type ExtractPathParams, type FileInfo, type InferParamType, type InlineReply, type KVStore, type LogLevel, type Logger, type Method, type Middleware, type Options, type ParamTypeMap, type ParamsToObject, type PathToParams, type Platform, type ProfileUser, type Provider, type Route, type RouteOptions, type RouterMethod, type SecurityOptions, type SecuritySettings, type SerializableValue, Server, type ServerConfig, TypedServerError as ServerError, type Settings, type StoreSource, type Strategy, type Time, type UploadOptions, type UploadedFile, cache, cookies, server as default, download, file, headers, json, redirect, router, send, status, type };
|
package/index.js
CHANGED
|
@@ -68,6 +68,10 @@ ServerError_default.extend({
|
|
|
68
68
|
status: 401,
|
|
69
69
|
message: "Credentials do not correspond to a user"
|
|
70
70
|
},
|
|
71
|
+
AUTH_INVALID_USER: {
|
|
72
|
+
status: 500,
|
|
73
|
+
message: "{callback} must return a user with an 'id' and an 'email'"
|
|
74
|
+
},
|
|
71
75
|
LOGIN_NO_EMAIL: "The email is required to log in",
|
|
72
76
|
LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
|
|
73
77
|
LOGIN_NO_PASSWORD: "The email is required to log in",
|
|
@@ -175,7 +179,7 @@ async function saveFileToBucket(originalName, data, bucket2, contentType) {
|
|
|
175
179
|
};
|
|
176
180
|
}
|
|
177
181
|
function validateFile(originalName, data, contentType, limits) {
|
|
178
|
-
const { maxSize, minSize, fileType } = limits;
|
|
182
|
+
const { maxSize, minSize, fileType: fileType2 } = limits;
|
|
179
183
|
if (maxSize !== void 0 && data.length > parseBytes(maxSize)) {
|
|
180
184
|
throw new Error(
|
|
181
185
|
`File "${originalName}" is too large (${data.length} bytes, limit is ${maxSize})`
|
|
@@ -186,15 +190,15 @@ function validateFile(originalName, data, contentType, limits) {
|
|
|
186
190
|
`File "${originalName}" is too small (${data.length} bytes, minimum is ${minSize})`
|
|
187
191
|
);
|
|
188
192
|
}
|
|
189
|
-
if (
|
|
193
|
+
if (fileType2 && fileType2.length > 0) {
|
|
190
194
|
const ext2 = getExt(originalName);
|
|
191
195
|
const mime = contentType.toLowerCase();
|
|
192
|
-
const allowed =
|
|
196
|
+
const allowed = fileType2.some(
|
|
193
197
|
(t) => t.toLowerCase() === mime || t.toLowerCase() === ext2
|
|
194
198
|
);
|
|
195
199
|
if (!allowed) {
|
|
196
200
|
throw new Error(
|
|
197
|
-
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${
|
|
201
|
+
`File type not allowed for "${originalName}" (got "${contentType}", allowed: ${fileType2.join(", ")})`
|
|
198
202
|
);
|
|
199
203
|
}
|
|
200
204
|
}
|
|
@@ -531,9 +535,9 @@ async function parseBody(input, contentType, dest, max = INF) {
|
|
|
531
535
|
let limits;
|
|
532
536
|
if (dest && "bucket" in dest) {
|
|
533
537
|
bucket2 = dest.bucket;
|
|
534
|
-
const { maxSize, minSize, fileType } = dest;
|
|
535
|
-
if (maxSize != null || minSize != null ||
|
|
536
|
-
limits = { maxSize, minSize, fileType };
|
|
538
|
+
const { maxSize, minSize, fileType: fileType2 } = dest;
|
|
539
|
+
if (maxSize != null || minSize != null || fileType2 != null) {
|
|
540
|
+
limits = { maxSize, minSize, fileType: fileType2 };
|
|
537
541
|
}
|
|
538
542
|
} else {
|
|
539
543
|
bucket2 = dest;
|
|
@@ -661,8 +665,8 @@ function normalizeExpires(expires) {
|
|
|
661
665
|
}
|
|
662
666
|
function createCookies(key, val) {
|
|
663
667
|
if (val.value === null) val.expires = EXPIRED;
|
|
664
|
-
const { value, path
|
|
665
|
-
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${
|
|
668
|
+
const { value, path, expires, maxAge, httpOnly, secure, sameSite } = val;
|
|
669
|
+
let str = `${key}=${encodeURIComponent(value ?? "")};Path=${path || "/"}`;
|
|
666
670
|
if (typeof expires !== "undefined") str += `;Expires=${normalizeExpires(expires)}`;
|
|
667
671
|
if (typeof maxAge === "number") str += `;Max-Age=${maxAge}`;
|
|
668
672
|
if (httpOnly) str += ";HttpOnly";
|
|
@@ -725,6 +729,16 @@ function clientIp(headers2, opts = {}) {
|
|
|
725
729
|
return normalize(remoteAddress);
|
|
726
730
|
}
|
|
727
731
|
|
|
732
|
+
// src/helpers/store.ts
|
|
733
|
+
import kv from "polystore";
|
|
734
|
+
function toStore(source) {
|
|
735
|
+
const store = source;
|
|
736
|
+
if (store && typeof store.prefix === "function" && typeof store.get === "function" && typeof store.set === "function") {
|
|
737
|
+
return store;
|
|
738
|
+
}
|
|
739
|
+
return kv(source);
|
|
740
|
+
}
|
|
741
|
+
|
|
728
742
|
// src/helpers/disposition.ts
|
|
729
743
|
var encodeExt = (name) => encodeURIComponent(name).replace(
|
|
730
744
|
/['()*]/g,
|
|
@@ -740,6 +754,14 @@ function disposition(name) {
|
|
|
740
754
|
return `${value}; filename*=UTF-8''${encodeExt(clean)}`;
|
|
741
755
|
}
|
|
742
756
|
|
|
757
|
+
// src/helpers/fileType.ts
|
|
758
|
+
function fileType(file2) {
|
|
759
|
+
if (file2.type) return file2.type;
|
|
760
|
+
const name = file2.path || file2.name || "";
|
|
761
|
+
const ext2 = name.split(".").pop()?.toLowerCase();
|
|
762
|
+
return ext2 ? mimes_default[ext2] : void 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
743
765
|
// src/helpers/isHtml.ts
|
|
744
766
|
var TAG = /^\s*<[a-zA-Z!/]/;
|
|
745
767
|
function isHtml(body) {
|
|
@@ -817,22 +839,22 @@ var Reply = class {
|
|
|
817
839
|
}
|
|
818
840
|
return this.send(JSON.stringify(body));
|
|
819
841
|
}
|
|
820
|
-
redirect(
|
|
821
|
-
this.headers("location",
|
|
842
|
+
redirect(path) {
|
|
843
|
+
this.headers("location", path);
|
|
822
844
|
if (this.res.status == null) this.res.status = 302;
|
|
823
845
|
return this.send();
|
|
824
846
|
}
|
|
825
|
-
async file(
|
|
826
|
-
if (typeof
|
|
827
|
-
if (!await
|
|
828
|
-
return this.type(
|
|
847
|
+
async file(path) {
|
|
848
|
+
if (typeof path !== "string") {
|
|
849
|
+
if (!await path.exists()) return this.status(404).send();
|
|
850
|
+
return this.type(fileType(path)).send(path.stream());
|
|
829
851
|
}
|
|
830
|
-
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(
|
|
852
|
+
if (/(?:^|[\\/])\.\.(?:[\\/]|$)/.test(path)) return this.status(404).send();
|
|
831
853
|
try {
|
|
832
|
-
const
|
|
833
|
-
const ext2 =
|
|
834
|
-
await
|
|
835
|
-
const stream =
|
|
854
|
+
const fs = await import("fs");
|
|
855
|
+
const ext2 = path.split(".").pop();
|
|
856
|
+
await fs.promises.access(path);
|
|
857
|
+
const stream = fs.createReadStream(path);
|
|
836
858
|
return this.type(ext2).send(stream);
|
|
837
859
|
} catch (error) {
|
|
838
860
|
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
|
@@ -894,6 +916,13 @@ var json = (...args) => r().json(...args);
|
|
|
894
916
|
var file = (...args) => r().file(...args);
|
|
895
917
|
var redirect = (...args) => r().redirect(...args);
|
|
896
918
|
|
|
919
|
+
// src/auth/assertUser.ts
|
|
920
|
+
function assertUser(user, callback3) {
|
|
921
|
+
if (!user || typeof user !== "object" || user.id == null || !user.email) {
|
|
922
|
+
throw ServerError_default.AUTH_INVALID_USER({ callback: callback3 });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
897
926
|
// src/helpers/jwt.ts
|
|
898
927
|
var enc = new TextEncoder();
|
|
899
928
|
var dec = new TextDecoder();
|
|
@@ -964,7 +993,7 @@ async function verifyJwt(token, secret) {
|
|
|
964
993
|
// src/auth/finishLogin.ts
|
|
965
994
|
async function finishLogin(ctx, input) {
|
|
966
995
|
const settings = ctx.options.auth;
|
|
967
|
-
const { strategy,
|
|
996
|
+
const { strategy, onLogin, onUser } = settings;
|
|
968
997
|
const key = String(input.key);
|
|
969
998
|
const auth2 = {
|
|
970
999
|
id: createId(),
|
|
@@ -974,22 +1003,28 @@ async function finishLogin(ctx, input) {
|
|
|
974
1003
|
email: input.email,
|
|
975
1004
|
time: (/* @__PURE__ */ new Date()).toISOString().replace(/\.[0-9]*/, "")
|
|
976
1005
|
};
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
|
|
1006
|
+
const loginUser = {
|
|
1007
|
+
...input.user,
|
|
1008
|
+
provider: input.provider,
|
|
1009
|
+
strategy
|
|
1010
|
+
};
|
|
1011
|
+
const existingUser = await settings.store.get(key) ?? null;
|
|
1012
|
+
const user = onLogin ? await onLogin(loginUser, existingUser, ctx) : { ...existingUser ?? {}, ...loginUser };
|
|
1013
|
+
assertUser(user, "onLogin");
|
|
1014
|
+
await settings.store.set(key, user);
|
|
984
1015
|
if (!strategy.includes("jwt")) {
|
|
985
1016
|
await settings.session.set(auth2.id, auth2, { expires: "1w" });
|
|
986
1017
|
}
|
|
987
1018
|
if (strategy.includes("jwt")) {
|
|
988
1019
|
const token = await signJwt(auth2, ctx.options.secret, 7 * 24 * 60 * 60);
|
|
989
|
-
|
|
1020
|
+
const exposed = await onUser(user, ctx);
|
|
1021
|
+
assertUser(exposed, "onUser");
|
|
1022
|
+
return status(201).json({ ...exposed, token });
|
|
990
1023
|
}
|
|
991
1024
|
if (strategy.includes("token")) {
|
|
992
|
-
|
|
1025
|
+
const exposed = await onUser(user, ctx);
|
|
1026
|
+
assertUser(exposed, "onUser");
|
|
1027
|
+
return status(201).json({ ...exposed, token: auth2.id });
|
|
993
1028
|
}
|
|
994
1029
|
if (strategy.includes("cookie")) {
|
|
995
1030
|
return cookies("authentication", {
|
|
@@ -1000,7 +1035,6 @@ async function finishLogin(ctx, input) {
|
|
|
1000
1035
|
sameSite: "Lax"
|
|
1001
1036
|
}).redirect(settings.redirect);
|
|
1002
1037
|
}
|
|
1003
|
-
if (strategy.includes("key")) throw new Error("Key auth not supported yet");
|
|
1004
1038
|
throw new Error("Unknown auth type");
|
|
1005
1039
|
}
|
|
1006
1040
|
|
|
@@ -1110,11 +1144,15 @@ var callback = async (ctx) => {
|
|
|
1110
1144
|
const parsed = JSON.parse(body.user).name;
|
|
1111
1145
|
if (parsed) name = `${parsed.firstName} ${parsed.lastName}`.trim();
|
|
1112
1146
|
}
|
|
1147
|
+
const raw = { ...claims, name };
|
|
1148
|
+
const { onProfile } = ctx.options.auth;
|
|
1149
|
+
const profile = onProfile ? await onProfile(raw, "apple") : { id: raw.sub, name: raw.name, email: raw.email };
|
|
1150
|
+
assertUser(profile, "onProfile");
|
|
1113
1151
|
const res = await finishLogin(ctx, {
|
|
1114
1152
|
provider: "apple",
|
|
1115
|
-
key:
|
|
1116
|
-
email:
|
|
1117
|
-
user:
|
|
1153
|
+
key: profile.id,
|
|
1154
|
+
email: profile.email,
|
|
1155
|
+
user: profile
|
|
1118
1156
|
});
|
|
1119
1157
|
res.headers.append("set-cookie", clearState());
|
|
1120
1158
|
return res;
|
|
@@ -1163,17 +1201,15 @@ function oauthProvider(config2) {
|
|
|
1163
1201
|
}
|
|
1164
1202
|
});
|
|
1165
1203
|
if (!profileRes.ok) throw new Error(`${config2.name}: profile fetch failed`);
|
|
1166
|
-
const
|
|
1204
|
+
const raw = await profileRes.json();
|
|
1205
|
+
const { onProfile } = ctx.options.auth;
|
|
1206
|
+
const profile = onProfile ? await onProfile(raw, config2.name) : config2.profile(raw);
|
|
1207
|
+
assertUser(profile, "onProfile");
|
|
1167
1208
|
const res = await finishLogin(ctx, {
|
|
1168
1209
|
provider: config2.name,
|
|
1169
1210
|
key: profile.id,
|
|
1170
1211
|
email: profile.email,
|
|
1171
|
-
user:
|
|
1172
|
-
id: profile.id,
|
|
1173
|
-
name: profile.name,
|
|
1174
|
-
email: profile.email,
|
|
1175
|
-
picture: profile.picture
|
|
1176
|
-
}
|
|
1212
|
+
user: profile
|
|
1177
1213
|
});
|
|
1178
1214
|
res.headers.append("set-cookie", clearState());
|
|
1179
1215
|
return res;
|
|
@@ -1219,8 +1255,7 @@ async function emailLogin(ctx) {
|
|
|
1219
1255
|
provider: "email",
|
|
1220
1256
|
key: user.email,
|
|
1221
1257
|
email: user.email,
|
|
1222
|
-
user
|
|
1223
|
-
store: false
|
|
1258
|
+
user
|
|
1224
1259
|
});
|
|
1225
1260
|
}
|
|
1226
1261
|
async function emailRegister(ctx) {
|
|
@@ -1241,13 +1276,11 @@ async function emailRegister(ctx) {
|
|
|
1241
1276
|
time,
|
|
1242
1277
|
...data
|
|
1243
1278
|
};
|
|
1244
|
-
await store.set(email, user);
|
|
1245
1279
|
return finishLogin(ctx, {
|
|
1246
1280
|
provider: "email",
|
|
1247
1281
|
key: email,
|
|
1248
1282
|
email,
|
|
1249
|
-
user
|
|
1250
|
-
store: false
|
|
1283
|
+
user
|
|
1251
1284
|
});
|
|
1252
1285
|
}
|
|
1253
1286
|
async function emailResetPassword() {
|
|
@@ -1301,8 +1334,8 @@ var oauth = async (code) => {
|
|
|
1301
1334
|
code
|
|
1302
1335
|
})
|
|
1303
1336
|
});
|
|
1304
|
-
return (
|
|
1305
|
-
return fch(`https://api.github.com${
|
|
1337
|
+
return (path) => {
|
|
1338
|
+
return fch(`https://api.github.com${path}`, {
|
|
1306
1339
|
headers: { Authorization: `Bearer ${res.access_token}` }
|
|
1307
1340
|
});
|
|
1308
1341
|
};
|
|
@@ -1327,21 +1360,25 @@ var getUserProfile = async (code) => {
|
|
|
1327
1360
|
const email = emails.sort((a) => a.primary ? -1 : 1)[0]?.email;
|
|
1328
1361
|
return { ...profile, email };
|
|
1329
1362
|
};
|
|
1363
|
+
var defaultProfile = (raw) => ({
|
|
1364
|
+
id: raw.id,
|
|
1365
|
+
name: raw.name,
|
|
1366
|
+
email: raw.email,
|
|
1367
|
+
picture: raw.avatar_url,
|
|
1368
|
+
location: raw.location,
|
|
1369
|
+
created: raw.created_at
|
|
1370
|
+
});
|
|
1330
1371
|
var callback2 = async (ctx) => {
|
|
1331
1372
|
checkState(ctx, ctx.url.query.state);
|
|
1332
|
-
const
|
|
1373
|
+
const raw = await getUserProfile(ctx.url.query.code);
|
|
1374
|
+
const { onProfile } = ctx.options.auth;
|
|
1375
|
+
const profile = onProfile ? await onProfile(raw, "github") : defaultProfile(raw);
|
|
1376
|
+
assertUser(profile, "onProfile");
|
|
1333
1377
|
const res = await finishLogin(ctx, {
|
|
1334
1378
|
provider: "github",
|
|
1335
1379
|
key: profile.id,
|
|
1336
1380
|
email: profile.email,
|
|
1337
|
-
user:
|
|
1338
|
-
id: profile.id,
|
|
1339
|
-
name: profile.name,
|
|
1340
|
-
email: profile.email,
|
|
1341
|
-
picture: profile.avatar_url,
|
|
1342
|
-
location: profile.location,
|
|
1343
|
-
created: profile.created_at
|
|
1344
|
-
}
|
|
1381
|
+
user: profile
|
|
1345
1382
|
});
|
|
1346
1383
|
res.headers.append("set-cookie", clearState());
|
|
1347
1384
|
return res;
|
|
@@ -1391,7 +1428,7 @@ var providers_default = {
|
|
|
1391
1428
|
|
|
1392
1429
|
// src/auth/parseAuthOptions.ts
|
|
1393
1430
|
var defaultRedirect = "/user";
|
|
1394
|
-
function
|
|
1431
|
+
function defaultOnUser(fullUser) {
|
|
1395
1432
|
const { password: _password, ...user } = fullUser;
|
|
1396
1433
|
return user;
|
|
1397
1434
|
}
|
|
@@ -1406,19 +1443,6 @@ function parseAuthOptions(auth2, all) {
|
|
|
1406
1443
|
throw new Error("Auth options needs a strategy");
|
|
1407
1444
|
}
|
|
1408
1445
|
const strategy = auth2.strategy;
|
|
1409
|
-
if (strategy === "key") {
|
|
1410
|
-
const key = auth2.key || env.AUTH_KEY;
|
|
1411
|
-
if (!key) {
|
|
1412
|
-
throw new Error("`key` auth needs the AUTH_KEY env var (or auth.key)");
|
|
1413
|
-
}
|
|
1414
|
-
return {
|
|
1415
|
-
strategy,
|
|
1416
|
-
providers: [],
|
|
1417
|
-
key,
|
|
1418
|
-
redirect: auth2.redirect || defaultRedirect,
|
|
1419
|
-
cleanUser: auth2.cleanUser || defaultCleanUser
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
1446
|
const list = Array.isArray(auth2.providers) ? auth2.providers : auth2.providers ? [auth2.providers] : [];
|
|
1423
1447
|
if (!list.length) {
|
|
1424
1448
|
throw new Error("Auth options needs a provider");
|
|
@@ -1430,119 +1454,35 @@ function parseAuthOptions(auth2, all) {
|
|
|
1430
1454
|
);
|
|
1431
1455
|
}
|
|
1432
1456
|
const redirect2 = auth2.redirect || defaultRedirect;
|
|
1433
|
-
const
|
|
1457
|
+
const { onProfile, onLogin, onLogout } = auth2;
|
|
1458
|
+
const onUser = auth2.onUser || defaultOnUser;
|
|
1434
1459
|
if (!auth2.store && !all.store) {
|
|
1435
1460
|
throw new Error("Need a userStore store for Auth");
|
|
1436
1461
|
}
|
|
1437
1462
|
if (!auth2.session && !all.store) {
|
|
1438
1463
|
throw new Error("Need a sessionStore store for Auth");
|
|
1439
1464
|
}
|
|
1440
|
-
const store =
|
|
1441
|
-
const
|
|
1465
|
+
const store = all.store ? toStore(all.store) : null;
|
|
1466
|
+
const authStore = auth2.store ? toStore(auth2.store) : store.prefix("user:");
|
|
1467
|
+
const sessionStore = auth2.session ? toStore(auth2.session) : store.prefix("auth:");
|
|
1442
1468
|
return {
|
|
1443
1469
|
strategy,
|
|
1444
1470
|
providers: list,
|
|
1445
1471
|
redirect: redirect2,
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1472
|
+
onProfile,
|
|
1473
|
+
onLogin,
|
|
1474
|
+
onUser,
|
|
1475
|
+
onLogout,
|
|
1476
|
+
store: authStore,
|
|
1477
|
+
session: sessionStore
|
|
1449
1478
|
};
|
|
1450
1479
|
}
|
|
1451
1480
|
|
|
1452
1481
|
// src/helpers/bucket.ts
|
|
1453
|
-
import
|
|
1454
|
-
import * as fsp from "fs/promises";
|
|
1455
|
-
import * as path from "path";
|
|
1456
|
-
function localBucket(root, prefix = "") {
|
|
1457
|
-
const base = path.resolve(root);
|
|
1458
|
-
const resolveKey = (name) => {
|
|
1459
|
-
if (!name) throw new Error("File name is required");
|
|
1460
|
-
const full = path.resolve(base, name.replace(/^\/+/, ""));
|
|
1461
|
-
if (full !== base && !full.startsWith(base + path.sep)) {
|
|
1462
|
-
throw new Error(`Path "${name}" escapes the bucket root`);
|
|
1463
|
-
}
|
|
1464
|
-
return full;
|
|
1465
|
-
};
|
|
1466
|
-
const file2 = (name, win) => {
|
|
1467
|
-
const full = resolveKey(name);
|
|
1468
|
-
const key = prefix + name.replace(/^\/+/, "");
|
|
1469
|
-
const type2 = mimes_default[path.extname(name).slice(1).toLowerCase()];
|
|
1470
|
-
const read = () => {
|
|
1471
|
-
let opts;
|
|
1472
|
-
if (win) {
|
|
1473
|
-
opts = { start: win.start };
|
|
1474
|
-
if (Number.isFinite(win.end)) opts.end = Math.max(win.start, win.end - 1);
|
|
1475
|
-
}
|
|
1476
|
-
const nodeStream = fs.createReadStream(full, opts);
|
|
1477
|
-
return new ReadableStream({
|
|
1478
|
-
start(controller) {
|
|
1479
|
-
nodeStream.on("data", (chunk) => controller.enqueue(chunk));
|
|
1480
|
-
nodeStream.on("end", () => controller.close());
|
|
1481
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
1482
|
-
},
|
|
1483
|
-
cancel() {
|
|
1484
|
-
nodeStream.destroy();
|
|
1485
|
-
}
|
|
1486
|
-
});
|
|
1487
|
-
};
|
|
1488
|
-
return {
|
|
1489
|
-
path: key,
|
|
1490
|
-
name: path.basename(name),
|
|
1491
|
-
type: type2,
|
|
1492
|
-
async exists() {
|
|
1493
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
1494
|
-
return !!stats?.isFile();
|
|
1495
|
-
},
|
|
1496
|
-
async info() {
|
|
1497
|
-
const stats = await fsp.stat(full).catch(() => null);
|
|
1498
|
-
if (!stats?.isFile()) return null;
|
|
1499
|
-
const size = win ? Math.max(0, Math.min(win.end, stats.size) - win.start) : stats.size;
|
|
1500
|
-
return { size, type: type2 ?? null, modified: stats.mtime };
|
|
1501
|
-
},
|
|
1502
|
-
// Read-only view of [start, end), composed relative to the current window.
|
|
1503
|
-
slice(start, end) {
|
|
1504
|
-
const base2 = win?.start ?? 0;
|
|
1505
|
-
const cap = win?.end ?? Number.POSITIVE_INFINITY;
|
|
1506
|
-
const s = Math.min(cap, base2 + Math.max(0, start));
|
|
1507
|
-
const e = end === void 0 ? cap : Math.min(cap, base2 + end);
|
|
1508
|
-
return file2(name, { start: s, end: e });
|
|
1509
|
-
},
|
|
1510
|
-
async write(content) {
|
|
1511
|
-
await fsp.mkdir(path.dirname(full), { recursive: true });
|
|
1512
|
-
if (content instanceof ReadableStream) {
|
|
1513
|
-
const writable = fs.createWriteStream(full);
|
|
1514
|
-
for await (const chunk of content) {
|
|
1515
|
-
writable.write(chunk);
|
|
1516
|
-
}
|
|
1517
|
-
await new Promise((resolve2, reject) => {
|
|
1518
|
-
writable.on("error", reject);
|
|
1519
|
-
writable.end(() => resolve2());
|
|
1520
|
-
});
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1523
|
-
await fsp.writeFile(full, content);
|
|
1524
|
-
},
|
|
1525
|
-
stream() {
|
|
1526
|
-
return read();
|
|
1527
|
-
},
|
|
1528
|
-
async bytes() {
|
|
1529
|
-
if (win) return new Uint8Array(await new Response(read()).arrayBuffer());
|
|
1530
|
-
return new Uint8Array(await fsp.readFile(full));
|
|
1531
|
-
},
|
|
1532
|
-
async remove() {
|
|
1533
|
-
await fsp.unlink(full).catch(() => {
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
};
|
|
1537
|
-
};
|
|
1538
|
-
return {
|
|
1539
|
-
file: file2,
|
|
1540
|
-
folder: (sub) => localBucket(path.join(base, sub), `${prefix}${sub.replace(/^\/+|\/+$/g, "")}/`)
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1482
|
+
import FileSystem from "bucket/fs";
|
|
1543
1483
|
function bucket(root) {
|
|
1544
1484
|
if (!root) return null;
|
|
1545
|
-
if (typeof root === "string") return
|
|
1485
|
+
if (typeof root === "string") return FileSystem(root);
|
|
1546
1486
|
if (typeof root.file === "function") return root;
|
|
1547
1487
|
throw new Error(
|
|
1548
1488
|
"Invalid bucket: pass a directory path or a `bucket` instance (with .file())"
|
|
@@ -1638,14 +1578,14 @@ function createLogger(level) {
|
|
|
1638
1578
|
const request = (ctx, res) => {
|
|
1639
1579
|
if (!enabled) return;
|
|
1640
1580
|
const method = ctx.method.toUpperCase();
|
|
1641
|
-
const
|
|
1581
|
+
const path = ctx.url.pathname;
|
|
1642
1582
|
const reqLen = Number(ctx.headers["content-length"]) || 0;
|
|
1643
1583
|
const resLen = Number(res.headers.get("content-length")) || 0;
|
|
1644
1584
|
const status2 = res.status;
|
|
1645
1585
|
const text = STATUS_TEXT[status2] || "";
|
|
1646
1586
|
const reqSize = reqLen ? ` ${formatBytes(reqLen)}` : "";
|
|
1647
1587
|
const resSize = resLen ? ` ${formatBytes(resLen)}` : "";
|
|
1648
|
-
let line = `${method} ${
|
|
1588
|
+
let line = `${method} ${path}${reqSize} \u2192 ${status2}${text ? ` ${text}` : ""}${resSize}`;
|
|
1649
1589
|
const location = res.headers.get("location");
|
|
1650
1590
|
if (location) line += ` \u2192 ${location}`;
|
|
1651
1591
|
message("api", line);
|
|
@@ -1772,22 +1712,22 @@ function config(options = {}) {
|
|
|
1772
1712
|
if (!up) {
|
|
1773
1713
|
settings.uploads = null;
|
|
1774
1714
|
} else if (typeof up === "object" && "bucket" in up) {
|
|
1775
|
-
const { bucket: bucket2, maxSize, minSize, fileType } = up;
|
|
1715
|
+
const { bucket: bucket2, maxSize, minSize, fileType: fileType2 } = up;
|
|
1776
1716
|
if (maxSize != null) parseBytes(maxSize);
|
|
1777
1717
|
if (minSize != null) parseBytes(minSize);
|
|
1778
|
-
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType };
|
|
1718
|
+
settings.uploads = { bucket: bucket(bucket2), maxSize, minSize, fileType: fileType2 };
|
|
1779
1719
|
} else {
|
|
1780
1720
|
settings.uploads = { bucket: bucket(up) };
|
|
1781
1721
|
}
|
|
1782
1722
|
const favicon2 = options.favicon || env2.FAVICON;
|
|
1783
1723
|
if (favicon2) settings.favicon = favicon2;
|
|
1784
|
-
settings.store = options.store
|
|
1785
|
-
settings.cookies = options.cookies ?? null;
|
|
1724
|
+
settings.store = options.store ? toStore(options.store) : null;
|
|
1786
1725
|
if (options.session) {
|
|
1787
|
-
|
|
1726
|
+
const store = typeof options.session === "object" && "store" in options.session ? options.session.store : options.session;
|
|
1727
|
+
settings.session = { store: toStore(store) };
|
|
1788
1728
|
}
|
|
1789
|
-
if (
|
|
1790
|
-
settings.session = { store:
|
|
1729
|
+
if (settings.store && !options.session) {
|
|
1730
|
+
settings.session = { store: settings.store.prefix("session:") };
|
|
1791
1731
|
}
|
|
1792
1732
|
if (options.auth || env2.AUTH) {
|
|
1793
1733
|
settings.auth = parseAuthOptions(options.auth || env2.AUTH || null, options);
|
|
@@ -1940,9 +1880,10 @@ async function parseResponse(out, ctx) {
|
|
|
1940
1880
|
if (!await out.exists()) {
|
|
1941
1881
|
out = new Response(null, { status: 404 });
|
|
1942
1882
|
} else {
|
|
1883
|
+
const type2 = fileType(out);
|
|
1943
1884
|
out = new Response(
|
|
1944
1885
|
out.stream(),
|
|
1945
|
-
|
|
1886
|
+
type2 ? { headers: { "content-type": type2 } } : void 0
|
|
1946
1887
|
);
|
|
1947
1888
|
}
|
|
1948
1889
|
}
|
|
@@ -2011,13 +1952,6 @@ async function parseResponse(out, ctx) {
|
|
|
2011
1952
|
}
|
|
2012
1953
|
ctx.options.session.store.set(id, ctx.session);
|
|
2013
1954
|
}
|
|
2014
|
-
if (ctx.options.cookies) {
|
|
2015
|
-
if (Object.keys(ctx.res?.cookies || {}).length) {
|
|
2016
|
-
for (const cookie of Object.values(ctx.res.cookies)) {
|
|
2017
|
-
ctx.res.headers.append("set-cookie", cookie);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
1955
|
if (ctx?.res?.headers) {
|
|
2022
1956
|
for (const key in ctx.res.headers) {
|
|
2023
1957
|
out.headers[key] = ctx.res.headers[key];
|
|
@@ -2027,14 +1961,14 @@ async function parseResponse(out, ctx) {
|
|
|
2027
1961
|
}
|
|
2028
1962
|
|
|
2029
1963
|
// src/pathPattern.ts
|
|
2030
|
-
function pathPattern(pattern,
|
|
2031
|
-
if (pattern === "*" &&
|
|
1964
|
+
function pathPattern(pattern, path) {
|
|
1965
|
+
if (pattern === "*" && path === "/") return {};
|
|
2032
1966
|
pattern = `/${pattern.replace(/^\//, "")}`;
|
|
2033
1967
|
pattern = pattern.replace(/\/$/, "") || "/";
|
|
2034
|
-
|
|
2035
|
-
if (pattern ===
|
|
1968
|
+
path = path.replace(/\/$/, "") || "/";
|
|
1969
|
+
if (pattern === path) return {};
|
|
2036
1970
|
const params = {};
|
|
2037
|
-
const pathParts =
|
|
1971
|
+
const pathParts = path.split("/").slice(1).map((u) => decodeURIComponent(u));
|
|
2038
1972
|
const pattParts = pattern.split("/").slice(1);
|
|
2039
1973
|
let allSame = true;
|
|
2040
1974
|
for (let i = 0; i < Math.max(pathParts.length, pattParts.length); i++) {
|
|
@@ -2093,7 +2027,7 @@ function validate(ctx, schema) {
|
|
|
2093
2027
|
} catch (error) {
|
|
2094
2028
|
if (error.name === "ZodError" || error.constructor.name === "ZodError") {
|
|
2095
2029
|
const message = error.issues.map(
|
|
2096
|
-
({ path
|
|
2030
|
+
({ path, message: message2 }) => `[${base}.${path.join(".")}]: ${message2}`
|
|
2097
2031
|
).sort().join("\n");
|
|
2098
2032
|
throw new StatusError(message, 422);
|
|
2099
2033
|
}
|
|
@@ -2289,7 +2223,7 @@ async function verify(password, hash3) {
|
|
|
2289
2223
|
const [, variant, , memory, passes, parallelism, saltB64, hashB64] = match;
|
|
2290
2224
|
const nonce = Buffer.from(saltB64, "base64");
|
|
2291
2225
|
const expected = Buffer.from(hashB64, "base64");
|
|
2292
|
-
return new Promise((
|
|
2226
|
+
return new Promise((resolve, reject) => {
|
|
2293
2227
|
crypto3.argon2(
|
|
2294
2228
|
`argon2${variant}`,
|
|
2295
2229
|
{
|
|
@@ -2303,23 +2237,15 @@ async function verify(password, hash3) {
|
|
|
2303
2237
|
(err, derivedKey) => {
|
|
2304
2238
|
if (err) return reject(err);
|
|
2305
2239
|
if (derivedKey.length === expected.length && timingSafeEqual(derivedKey, expected)) {
|
|
2306
|
-
|
|
2240
|
+
resolve(true);
|
|
2307
2241
|
} else {
|
|
2308
|
-
|
|
2242
|
+
resolve(false);
|
|
2309
2243
|
}
|
|
2310
2244
|
}
|
|
2311
2245
|
);
|
|
2312
2246
|
});
|
|
2313
2247
|
}
|
|
2314
2248
|
|
|
2315
|
-
// src/helpers/safeEqual.ts
|
|
2316
|
-
function safeEqual(a, b) {
|
|
2317
|
-
if (a.length !== b.length) return false;
|
|
2318
|
-
let diff = 0;
|
|
2319
|
-
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
2320
|
-
return diff === 0;
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
2249
|
// src/auth/findSessionId.ts
|
|
2324
2250
|
var validateToken = (authorization) => {
|
|
2325
2251
|
const [type2, id] = authorization.trim().split(" ");
|
|
@@ -2352,19 +2278,6 @@ function findSessionId(ctx) {
|
|
|
2352
2278
|
}
|
|
2353
2279
|
|
|
2354
2280
|
// src/auth/getUser.ts
|
|
2355
|
-
function getKeyUser(ctx) {
|
|
2356
|
-
const expected = ctx.options.auth.key;
|
|
2357
|
-
const header = ctx.headers.authorization;
|
|
2358
|
-
if (!header) return;
|
|
2359
|
-
const [type2, provided] = header.trim().split(" ");
|
|
2360
|
-
if (type2?.toLowerCase() !== "bearer" || !provided) {
|
|
2361
|
-
throw ServerError_default.AUTH_INVALID_HEADER({ type: type2 });
|
|
2362
|
-
}
|
|
2363
|
-
if (!expected || !safeEqual(provided, expected)) {
|
|
2364
|
-
throw ServerError_default.AUTH_INVALID_TOKEN();
|
|
2365
|
-
}
|
|
2366
|
-
return { id: "key", strategy: "key", provider: "key" };
|
|
2367
|
-
}
|
|
2368
2281
|
async function getAuthSession(ctx) {
|
|
2369
2282
|
const strategy = ctx.options.auth.strategy;
|
|
2370
2283
|
if (strategy.includes("jwt")) {
|
|
@@ -2385,7 +2298,6 @@ async function getAuthSession(ctx) {
|
|
|
2385
2298
|
async function getUser(ctx) {
|
|
2386
2299
|
if (!ctx.options.auth) return;
|
|
2387
2300
|
const options = ctx.options.auth;
|
|
2388
|
-
if (options.strategy === "key") return getKeyUser(ctx);
|
|
2389
2301
|
const auth2 = await getAuthSession(ctx);
|
|
2390
2302
|
if (!auth2) return;
|
|
2391
2303
|
if (options.strategy !== auth2.strategy) {
|
|
@@ -2404,7 +2316,9 @@ async function getUser(ctx) {
|
|
|
2404
2316
|
if (!user) throw ServerError_default.AUTH_NO_USER();
|
|
2405
2317
|
user.strategy = auth2.strategy;
|
|
2406
2318
|
user.provider = auth2.provider;
|
|
2407
|
-
|
|
2319
|
+
const exposed = await ctx.options.auth.onUser(user, ctx);
|
|
2320
|
+
assertUser(exposed, "onUser");
|
|
2321
|
+
return exposed;
|
|
2408
2322
|
}
|
|
2409
2323
|
|
|
2410
2324
|
// src/auth/logout.ts
|
|
@@ -2414,15 +2328,13 @@ async function logout(ctx) {
|
|
|
2414
2328
|
if (!strategy.includes("jwt")) {
|
|
2415
2329
|
await ctx.options.auth.session.del(findSessionId(ctx));
|
|
2416
2330
|
}
|
|
2331
|
+
if (ctx.options.auth.onLogout) await ctx.options.auth.onLogout(ctx);
|
|
2417
2332
|
if (strategy.includes("token") || strategy.includes("jwt")) {
|
|
2418
2333
|
return { token: null };
|
|
2419
2334
|
}
|
|
2420
2335
|
if (strategy.includes("cookie")) {
|
|
2421
2336
|
return cookies({ authentication: null }).redirect("/");
|
|
2422
2337
|
}
|
|
2423
|
-
if (strategy.includes("key")) {
|
|
2424
|
-
throw new Error("Key auth not supported yet");
|
|
2425
|
-
}
|
|
2426
2338
|
throw new Error("Unknown auth type");
|
|
2427
2339
|
}
|
|
2428
2340
|
|
|
@@ -2438,7 +2350,6 @@ function auth(app) {
|
|
|
2438
2350
|
app.use(async function middle(ctx) {
|
|
2439
2351
|
ctx.user = await getUser(ctx);
|
|
2440
2352
|
});
|
|
2441
|
-
if (app.settings.auth.strategy === "key") return;
|
|
2442
2353
|
app.post("/auth/logout", logout);
|
|
2443
2354
|
const enabled = app.settings.auth.providers;
|
|
2444
2355
|
for (const name of oauth2) {
|
|
@@ -2500,8 +2411,8 @@ async function assets(ctx) {
|
|
|
2500
2411
|
const info = file2.info?.bind(file2);
|
|
2501
2412
|
const meta = info ? await info() : null;
|
|
2502
2413
|
if (info ? !meta : !await file2.exists()) return;
|
|
2503
|
-
const ext2 = ctx.url.pathname.split(".").pop();
|
|
2504
|
-
const ctype = meta?.type || ext2;
|
|
2414
|
+
const ext2 = ctx.url.pathname.split(".").pop()?.toLowerCase();
|
|
2415
|
+
const ctype = ext2 && mimes_default[ext2] || meta?.type || ext2;
|
|
2505
2416
|
const headers2 = { "cache-control": CACHE_CONTROL };
|
|
2506
2417
|
let tag;
|
|
2507
2418
|
if (meta) {
|
|
@@ -2564,7 +2475,7 @@ async function favicon(ctx) {
|
|
|
2564
2475
|
}
|
|
2565
2476
|
|
|
2566
2477
|
// src/middle/openapi.ts
|
|
2567
|
-
import * as
|
|
2478
|
+
import * as fsp from "fs/promises";
|
|
2568
2479
|
var entities = {
|
|
2569
2480
|
"&": "&",
|
|
2570
2481
|
"<": "<",
|
|
@@ -2610,7 +2521,7 @@ function zodToSchema(schema) {
|
|
|
2610
2521
|
}
|
|
2611
2522
|
return { type: type2 };
|
|
2612
2523
|
}
|
|
2613
|
-
var pkgProm =
|
|
2524
|
+
var pkgProm = fsp.readFile("package.json", "utf-8").then((data) => JSON.parse(data)).catch(() => ({}));
|
|
2614
2525
|
var getTag = (name, fn) => {
|
|
2615
2526
|
const found = fn.toString().split("\n").filter((l) => /\s+\/\/\s/.test(l)).map((l) => l.trim().replace("// ", "")).find((l) => l.startsWith(name));
|
|
2616
2527
|
if (!found) return "";
|
|
@@ -2622,14 +2533,14 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2622
2533
|
const paths = {};
|
|
2623
2534
|
for (const [method, routes] of Object.entries(handlers)) {
|
|
2624
2535
|
for (const route of routes) {
|
|
2625
|
-
const
|
|
2536
|
+
const path = route.path;
|
|
2626
2537
|
const fn = route.fns.find((p) => typeof p === "function");
|
|
2627
2538
|
const meta = route.fns.find((p) => typeof p === "object");
|
|
2628
2539
|
const config2 = getConfig(route.options);
|
|
2629
|
-
if (typeof
|
|
2540
|
+
if (typeof path !== "string" || path === "*" || path === "/docs" || !fn) {
|
|
2630
2541
|
continue;
|
|
2631
2542
|
}
|
|
2632
|
-
const normalizedPath =
|
|
2543
|
+
const normalizedPath = path.replace(/\(\w+\)/gi, "").replace(/:([a-zA-Z0-9_]+)/g, "{$1}");
|
|
2633
2544
|
if (!paths[normalizedPath]) {
|
|
2634
2545
|
paths[normalizedPath] = {};
|
|
2635
2546
|
}
|
|
@@ -2656,7 +2567,7 @@ var generateOpenApiPaths = (handlers) => {
|
|
|
2656
2567
|
};
|
|
2657
2568
|
}
|
|
2658
2569
|
const parameters = [];
|
|
2659
|
-
const matched = Array.from(
|
|
2570
|
+
const matched = Array.from(path.matchAll(/:[\w()]+/gi));
|
|
2660
2571
|
matched.forEach((match) => {
|
|
2661
2572
|
const [name, type2 = "string"] = match[0].slice(1).replace(/\)/, "").split("(");
|
|
2662
2573
|
parameters.push({
|
|
@@ -2995,18 +2906,18 @@ async function createNode(req, app) {
|
|
|
2995
2906
|
const cookies2 = parseCookies(headers2.cookie);
|
|
2996
2907
|
const scheme = req.socket instanceof TLSSocket ? "https" : "http";
|
|
2997
2908
|
const host = headers2.host || `localhost:${app.settings.port}`;
|
|
2998
|
-
const
|
|
2909
|
+
const path = (req.url || "/").replace(/\/$/, "") || "/";
|
|
2999
2910
|
const baseUrl = `${scheme}://${host}`;
|
|
3000
|
-
const url = new URL(
|
|
2911
|
+
const url = new URL(path, baseUrl);
|
|
3001
2912
|
define(
|
|
3002
2913
|
url,
|
|
3003
2914
|
"query",
|
|
3004
2915
|
(url2) => Object.fromEntries(url2.searchParams.entries())
|
|
3005
2916
|
);
|
|
3006
2917
|
const source = {
|
|
3007
|
-
getBuffer: () => new Promise((
|
|
2918
|
+
getBuffer: () => new Promise((resolve, reject) => {
|
|
3008
2919
|
const chunks2 = [];
|
|
3009
|
-
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () =>
|
|
2920
|
+
req.on("data", (chunk) => chunks2.push(chunk)).on("end", () => resolve(Buffer.concat(chunks2))).on("error", reject);
|
|
3010
2921
|
}),
|
|
3011
2922
|
getStream: () => toWeb(req)
|
|
3012
2923
|
};
|
|
@@ -3158,9 +3069,9 @@ var Router = class _Router {
|
|
|
3158
3069
|
// functions into a single flat `fns` list. A plain options object may sit
|
|
3159
3070
|
// between the path and the handlers, and it's pulled out here.
|
|
3160
3071
|
handle(method, pathOrFn, ...rest) {
|
|
3161
|
-
let
|
|
3072
|
+
let path = "*";
|
|
3162
3073
|
if (typeof pathOrFn === "string") {
|
|
3163
|
-
|
|
3074
|
+
path = pathOrFn;
|
|
3164
3075
|
} else if (pathOrFn != null) {
|
|
3165
3076
|
rest.unshift(pathOrFn);
|
|
3166
3077
|
}
|
|
@@ -3170,7 +3081,7 @@ var Router = class _Router {
|
|
|
3170
3081
|
}
|
|
3171
3082
|
const base = method === "socket" ? [] : this.middleware;
|
|
3172
3083
|
const fns = [...base, ...rest].filter((fn) => fn != null);
|
|
3173
|
-
this.handlers[method].push({ path
|
|
3084
|
+
this.handlers[method].push({ path, options, fns });
|
|
3174
3085
|
return this.self();
|
|
3175
3086
|
}
|
|
3176
3087
|
socket(pathOrMid, optionsOrMid, ...middleware) {
|
|
@@ -3235,31 +3146,33 @@ function isSerializable(body) {
|
|
|
3235
3146
|
}
|
|
3236
3147
|
function ServerTest(app) {
|
|
3237
3148
|
const port = app.settings.port;
|
|
3238
|
-
const fetch2 = async (method,
|
|
3149
|
+
const fetch2 = async (method, path, options = {}) => {
|
|
3239
3150
|
if (!options.headers) options.headers = {};
|
|
3240
3151
|
if (isSerializable(options.body)) {
|
|
3241
3152
|
options.headers["content-type"] = "application/json";
|
|
3242
3153
|
options.body = JSON.stringify(options.body);
|
|
3243
3154
|
}
|
|
3244
3155
|
return await app.fetch(
|
|
3245
|
-
new Request(`http://localhost:${port}${
|
|
3156
|
+
new Request(`http://localhost:${port}${path}`, {
|
|
3246
3157
|
method,
|
|
3247
3158
|
...options
|
|
3248
3159
|
})
|
|
3249
3160
|
);
|
|
3250
3161
|
};
|
|
3251
3162
|
return {
|
|
3252
|
-
get: (
|
|
3253
|
-
head: (
|
|
3254
|
-
post: (
|
|
3255
|
-
put: (
|
|
3256
|
-
patch: (
|
|
3257
|
-
delete: (
|
|
3258
|
-
options: (
|
|
3163
|
+
get: (path, options) => fetch2("get", path, options),
|
|
3164
|
+
head: (path, options) => fetch2("head", path, options),
|
|
3165
|
+
post: (path, body, options) => fetch2("post", path, { body, ...options }),
|
|
3166
|
+
put: (path, body, options) => fetch2("put", path, { body, ...options }),
|
|
3167
|
+
patch: (path, body, options) => fetch2("patch", path, { body, ...options }),
|
|
3168
|
+
delete: (path, options) => fetch2("delete", path, options),
|
|
3169
|
+
options: (path, options) => fetch2("options", path, options)
|
|
3259
3170
|
};
|
|
3260
3171
|
}
|
|
3261
3172
|
|
|
3262
3173
|
// src/index.ts
|
|
3174
|
+
import { default as default2 } from "polystore";
|
|
3175
|
+
import { default as default3 } from "bucket";
|
|
3263
3176
|
var Server = class extends Router {
|
|
3264
3177
|
settings;
|
|
3265
3178
|
platform;
|
|
@@ -3327,6 +3240,7 @@ function server(options) {
|
|
|
3327
3240
|
export {
|
|
3328
3241
|
Server,
|
|
3329
3242
|
ServerError_default as ServerError,
|
|
3243
|
+
default3 as bucket,
|
|
3330
3244
|
cache,
|
|
3331
3245
|
cookies,
|
|
3332
3246
|
server as default,
|
|
@@ -3334,6 +3248,7 @@ export {
|
|
|
3334
3248
|
file,
|
|
3335
3249
|
headers,
|
|
3336
3250
|
json,
|
|
3251
|
+
default2 as kv,
|
|
3337
3252
|
redirect,
|
|
3338
3253
|
router,
|
|
3339
3254
|
send,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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",
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"start": "bun test --watch",
|
|
18
18
|
"lint": "npx tsc --noEmit && npx @biomejs/biome lint ./src --skip=lint/suspicious/noExplicitAny --skip=lint/style/noParameterAssign --skip=lint/suspicious/noConfusingVoidType --skip=lint/complexity/noBannedTypes",
|
|
19
19
|
"test": "npm run test:bun && tsc --noEmit",
|
|
20
|
-
"test:bun": "bun test"
|
|
21
|
-
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
20
|
+
"test:bun": "bun test"
|
|
22
21
|
},
|
|
23
22
|
"main": "index.js",
|
|
24
23
|
"type": "module",
|
|
@@ -64,20 +63,16 @@
|
|
|
64
63
|
},
|
|
65
64
|
"tutorials": "docs/tutorials"
|
|
66
65
|
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"bucket": "^0.7.1",
|
|
68
|
+
"polystore": "^0.23.3"
|
|
69
|
+
},
|
|
67
70
|
"devDependencies": {
|
|
68
71
|
"@types/bun": "^1.3.0",
|
|
69
|
-
"@types/jest": "^30.0.0",
|
|
70
72
|
"@types/node": "^24.10.0",
|
|
71
|
-
"bucket": "^0.5.0",
|
|
72
73
|
"bun": "^1.3.13",
|
|
73
74
|
"check-dts": "^0.8.2",
|
|
74
|
-
"jest": "^29.7.0",
|
|
75
|
-
"polystore": "^0.21.1",
|
|
76
75
|
"tsup": "^8.5.1",
|
|
77
76
|
"typescript": "^6.0.2"
|
|
78
|
-
},
|
|
79
|
-
"jest": {
|
|
80
|
-
"testEnvironment": "jest-environment-node",
|
|
81
|
-
"transform": {}
|
|
82
77
|
}
|
|
83
78
|
}
|
package/readme.md
CHANGED
|
@@ -1 +1,29 @@
|
|
|
1
1
|
# Server JS [](https://www.npmjs.com/package/@server/next) [](https://github.com/franciscop/server-next/actions)
|
|
2
|
+
|
|
3
|
+
A modern web server for Bun and Node, with routing, authentication, uploads, WebSockets and testing built in.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @server/next
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import server from '@server/next';
|
|
11
|
+
|
|
12
|
+
export default server({ store: new Map(), uploads: './uploads' })
|
|
13
|
+
.get('/', () => 'Hello world')
|
|
14
|
+
.get('/users/:id', (ctx) => db.users.find(ctx.url.params.id))
|
|
15
|
+
.post('/avatar', (ctx) => ctx.body.avatar.path);
|
|
16
|
+
```
|
|
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:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import server, { kv, bucket } from '@server/next';
|
|
22
|
+
|
|
23
|
+
const store = kv(createClient({ url }).connect());
|
|
24
|
+
const uploads = bucket.S3('my-bucket', { id, key });
|
|
25
|
+
|
|
26
|
+
export default server({ store, uploads });
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
See the [full documentation](https://serverjs.io/documentation).
|