@server/next 0.22.0 → 0.23.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 +48 -14
- package/package.json +2 -1
- package/src/ServerError.js +6 -5
- package/src/auth/auth-cookie.test.js +2 -2
- package/src/auth/auth-token.test.js +5 -5
- package/src/auth/auth.js +4 -2
- package/src/auth/index.test.js +1 -1
- package/src/auth/logout.js +7 -5
- package/src/auth/providers/email.js +7 -5
- package/src/auth/providers/github.js +9 -7
- package/src/context/node.js +7 -5
- package/src/context/parseBody.js +5 -5
- package/src/context/winter.js +6 -4
- package/src/errors/index.js +14 -13
- package/src/helpers/bucket.js +5 -7
- package/src/helpers/config.js +1 -1
- package/src/helpers/createCookies.js +2 -2
- package/src/helpers/createId.js +2 -2
- package/src/helpers/handleRequest.js +2 -2
- package/src/helpers/jsx.js +1 -1
- package/src/helpers/jsx.test.jsx +2 -1
- package/src/index.js +14 -12
- package/src/parseResponse.js +3 -3
- package/src/pathPattern.js +1 -1
- package/src/reply.js +7 -7
- package/src/router.test.js +3 -3
- package/src/session.test.js +3 -3
package/index.d.ts
CHANGED
|
@@ -18,12 +18,34 @@ type ServerOptions = {
|
|
|
18
18
|
auth?: string | { type: string; provider: string };
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
type
|
|
21
|
+
type ExtractPathParams<Path extends string> =
|
|
22
|
+
Path extends `${string}:${infer Param}/${infer Rest}`
|
|
23
|
+
? Param | ExtractPathParams<`/${Rest}`>
|
|
24
|
+
: Path extends `${string}:${infer Param}`
|
|
25
|
+
? Param
|
|
26
|
+
: never;
|
|
27
|
+
|
|
28
|
+
type ParamsToObject<Params extends string> = {
|
|
29
|
+
[K in Params as K extends `${infer Key}?`
|
|
30
|
+
? Key
|
|
31
|
+
: K]: K extends `${infer Key}?` ? string | undefined : string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type PathToParams<Path extends string> = ParamsToObject<
|
|
35
|
+
ExtractPathParams<Path>
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
type Simplify<T> = T extends object ? { [K in keyof T]: T[K] } : T;
|
|
39
|
+
|
|
40
|
+
type Context<Path extends string = string> = {
|
|
22
41
|
method: Method;
|
|
23
42
|
headers: { [key: string]: string | string[] };
|
|
24
43
|
cookies: { [key: string]: any };
|
|
25
44
|
body?: any;
|
|
26
|
-
url: URL & {
|
|
45
|
+
url: URL & {
|
|
46
|
+
params: Simplify<PathToParams<Path>>; // Simplify here
|
|
47
|
+
query: {};
|
|
48
|
+
};
|
|
27
49
|
options: ServerOptions;
|
|
28
50
|
};
|
|
29
51
|
|
|
@@ -57,28 +79,40 @@ type InlineReply =
|
|
|
57
79
|
| number
|
|
58
80
|
| void;
|
|
59
81
|
|
|
60
|
-
type Middleware =
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
type Middleware<Path extends string = string> = (
|
|
83
|
+
ctx: Context<Path>,
|
|
84
|
+
) => InlineReply;
|
|
85
|
+
|
|
86
|
+
declare interface Router {
|
|
87
|
+
get<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
88
|
+
head<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
89
|
+
post<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
90
|
+
put<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
91
|
+
patch<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
92
|
+
del<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
93
|
+
options<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
94
|
+
}
|
|
63
95
|
|
|
64
96
|
declare interface Server {
|
|
65
97
|
(options?: ServerOptions): this;
|
|
66
98
|
|
|
67
|
-
socket(path: string, ...
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
socket(path: string, ...middle: Middleware[]): this;
|
|
100
|
+
|
|
101
|
+
get<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
102
|
+
head<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
103
|
+
post<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
104
|
+
put<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
105
|
+
patch<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
106
|
+
del<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
107
|
+
options<Path extends string>(path: Path, ...middle: Middleware<Path>[]): this;
|
|
75
108
|
|
|
76
|
-
use(...
|
|
109
|
+
use(...middle: Middleware[]): this;
|
|
77
110
|
router(router: Router): this;
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
type headers = (obj?: Headers) => any;
|
|
81
114
|
|
|
82
115
|
declare const server: Server;
|
|
116
|
+
export const router: Router;
|
|
83
117
|
export const headers: headers;
|
|
84
118
|
export default server;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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": "https://github.com/franciscop/server-next.git",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun test --watch",
|
|
22
|
+
"lint": "npx @biomejs/biome lint ./src --skip=lint/style/noParameterAssign",
|
|
22
23
|
"test": "bun test",
|
|
23
24
|
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
24
25
|
},
|
package/src/ServerError.js
CHANGED
|
@@ -3,10 +3,11 @@ export default class ServerError extends Error {
|
|
|
3
3
|
if (typeof message === "function") {
|
|
4
4
|
message = message(vars);
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
if (typeof message !== "string") throw Error(`Invalid error ${message}`);
|
|
7
|
+
for (const key in vars) {
|
|
8
|
+
let val = vars[key];
|
|
9
|
+
if (Array.isArray(val)) val = vars[key].join(",");
|
|
10
|
+
message = message.replaceAll(`{${key}}`, val);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
super(message);
|
|
@@ -15,7 +16,7 @@ export default class ServerError extends Error {
|
|
|
15
16
|
this.status = status;
|
|
16
17
|
}
|
|
17
18
|
static extend(errors) {
|
|
18
|
-
for (
|
|
19
|
+
for (const code in errors) {
|
|
19
20
|
const message = errors[code]?.message || errors[code];
|
|
20
21
|
const status = errors[code]?.status;
|
|
21
22
|
ServerError[code] = (vars) =>
|
|
@@ -21,7 +21,7 @@ describe("user creation flow", () => {
|
|
|
21
21
|
expect(register).toSucceed();
|
|
22
22
|
expect(await store.keys()).toEqual([
|
|
23
23
|
"user:abc@test.com",
|
|
24
|
-
|
|
24
|
+
`auth:${register.headers["set-cookie"].split(";")[0].split("=")[1]}`,
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
27
|
const me = await api.get("/me");
|
|
@@ -36,7 +36,7 @@ describe("user creation flow", () => {
|
|
|
36
36
|
expect(login).toSucceed();
|
|
37
37
|
expect(await store.keys()).toEqual([
|
|
38
38
|
"user:abc@test.com",
|
|
39
|
-
|
|
39
|
+
`auth:${login.headers["set-cookie"].split(";")[0].split("=")[1]}`,
|
|
40
40
|
]);
|
|
41
41
|
});
|
|
42
42
|
});
|
|
@@ -34,7 +34,7 @@ describe("user creation flow", () => {
|
|
|
34
34
|
|
|
35
35
|
// CAN GET MY OWN INFO
|
|
36
36
|
await (async () => {
|
|
37
|
-
const headers = { authorization:
|
|
37
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
38
38
|
const me = await api.get("/me", { headers });
|
|
39
39
|
expect(me).toSucceed();
|
|
40
40
|
expect(me.body.email).toEqual(EMAIL);
|
|
@@ -45,7 +45,7 @@ describe("user creation flow", () => {
|
|
|
45
45
|
|
|
46
46
|
// LOGOUT TEST
|
|
47
47
|
await (async () => {
|
|
48
|
-
const headers = { authorization:
|
|
48
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
49
49
|
const logout = await api.post("/auth/logout", {}, { headers });
|
|
50
50
|
expect(logout).toSucceed();
|
|
51
51
|
expect(await users()).toEqual(["abc@test.com"]);
|
|
@@ -63,7 +63,7 @@ describe("user creation flow", () => {
|
|
|
63
63
|
|
|
64
64
|
// CAN GET MY OWN INFO
|
|
65
65
|
await (async () => {
|
|
66
|
-
const headers = { authorization:
|
|
66
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
67
67
|
const me = await api.get("/me", { headers });
|
|
68
68
|
expect(me).toSucceed();
|
|
69
69
|
expect(me.body.email).toEqual(EMAIL);
|
|
@@ -71,7 +71,7 @@ describe("user creation flow", () => {
|
|
|
71
71
|
|
|
72
72
|
// UPDATE PASSWORD
|
|
73
73
|
await (async () => {
|
|
74
|
-
const headers = { authorization:
|
|
74
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
75
75
|
const body = { previous: PASS, updated: "22222222" };
|
|
76
76
|
const update = await api.put("/auth/password/email", body, { headers });
|
|
77
77
|
expect(update).toSucceed();
|
|
@@ -81,7 +81,7 @@ describe("user creation flow", () => {
|
|
|
81
81
|
|
|
82
82
|
// LOGOUT AGAIN
|
|
83
83
|
await (async () => {
|
|
84
|
-
const headers = { authorization:
|
|
84
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
85
85
|
const logout = await api.post("/auth/logout", {}, { headers });
|
|
86
86
|
expect(logout).toSucceed();
|
|
87
87
|
expect(await users()).toEqual(["abc@test.com"]);
|
package/src/auth/auth.js
CHANGED
|
@@ -53,8 +53,10 @@ export default async function auth(ctx) {
|
|
|
53
53
|
|
|
54
54
|
if (!auth.provider) throw ServerError.AUTH_NO_PROVIDER();
|
|
55
55
|
if (!options.provider.includes(auth.provider)) {
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
throw ServerError.AUTH_INVALID_PROVIDER({
|
|
57
|
+
provider: auth.provider,
|
|
58
|
+
valid: options.provider,
|
|
59
|
+
});
|
|
58
60
|
}
|
|
59
61
|
return auth;
|
|
60
62
|
}
|
package/src/auth/index.test.js
CHANGED
|
@@ -31,7 +31,7 @@ describe("auth", () => {
|
|
|
31
31
|
const authorization = "Bearer REqA2l022l8Q0tuI";
|
|
32
32
|
const req = await api.get("/", { headers: { authorization } });
|
|
33
33
|
expect(req).not.toSucceed(
|
|
34
|
-
|
|
34
|
+
"Invalid provider 'wrong', valid ones are: 'email'",
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
});
|
package/src/auth/logout.js
CHANGED
|
@@ -6,13 +6,15 @@ export default async function logout(ctx) {
|
|
|
6
6
|
|
|
7
7
|
if (type === "token") {
|
|
8
8
|
return { token: null };
|
|
9
|
-
}
|
|
9
|
+
}
|
|
10
|
+
if (type === "cookie") {
|
|
10
11
|
return cookies({ authorization: null }).redirect("/");
|
|
11
|
-
}
|
|
12
|
+
}
|
|
13
|
+
if (type === "jwt") {
|
|
12
14
|
throw new Error("JWT auth not supported yet");
|
|
13
|
-
}
|
|
15
|
+
}
|
|
16
|
+
if (type === "key") {
|
|
14
17
|
throw new Error("Key auth not supported yet");
|
|
15
|
-
} else {
|
|
16
|
-
throw new Error("Unknown auth type");
|
|
17
18
|
}
|
|
19
|
+
throw new Error("Unknown auth type");
|
|
18
20
|
}
|
|
@@ -48,15 +48,17 @@ const createSession = async (user, ctx) => {
|
|
|
48
48
|
|
|
49
49
|
if (type === "token") {
|
|
50
50
|
return status(201).json({ ...user, token: id });
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
|
+
if (type === "cookie") {
|
|
52
53
|
return status(302).cookies({ authentication: id }).redirect(redirect);
|
|
53
|
-
}
|
|
54
|
+
}
|
|
55
|
+
if (type === "jwt") {
|
|
54
56
|
throw new Error("JWT auth not supported yet");
|
|
55
|
-
}
|
|
57
|
+
}
|
|
58
|
+
if (type === "key") {
|
|
56
59
|
throw new Error("Key auth not supported yet");
|
|
57
|
-
} else {
|
|
58
|
-
throw new Error("Unknown auth type");
|
|
59
60
|
}
|
|
61
|
+
throw new Error("Unknown auth type");
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
async function login(ctx) {
|
|
@@ -19,8 +19,8 @@ const oauth = async (code) => {
|
|
|
19
19
|
}),
|
|
20
20
|
});
|
|
21
21
|
return (path) => {
|
|
22
|
-
return fch(
|
|
23
|
-
headers: { Authorization:
|
|
22
|
+
return fch(`https://api.github.com${path}`, {
|
|
23
|
+
headers: { Authorization: `Bearer ${res.access_token}` },
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
26
|
};
|
|
@@ -68,15 +68,17 @@ const callback = async (ctx) => {
|
|
|
68
68
|
|
|
69
69
|
if (auth.type === "token") {
|
|
70
70
|
return status(201).json({ ...user, token: auth.id });
|
|
71
|
-
}
|
|
71
|
+
}
|
|
72
|
+
if (auth.type === "cookie") {
|
|
72
73
|
return status(302).cookies({ authentication: auth.id }).redirect(redirect);
|
|
73
|
-
}
|
|
74
|
+
}
|
|
75
|
+
if (auth.type === "jwt") {
|
|
74
76
|
throw new Error("JWT auth not supported yet");
|
|
75
|
-
}
|
|
77
|
+
}
|
|
78
|
+
if (auth.type === "key") {
|
|
76
79
|
throw new Error("Key auth not supported yet");
|
|
77
|
-
} else {
|
|
78
|
-
throw new Error("Unknown auth type");
|
|
79
80
|
}
|
|
81
|
+
throw new Error("Unknown auth type");
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
export default { login, callback };
|
package/src/context/node.js
CHANGED
|
@@ -10,9 +10,9 @@ const chunkArray = (arr, size) =>
|
|
|
10
10
|
? [arr.slice(0, size), ...chunkArray(arr.slice(size), size)]
|
|
11
11
|
: [arr];
|
|
12
12
|
|
|
13
|
-
export default async (request,
|
|
13
|
+
export default async (request, app) => {
|
|
14
14
|
const ctx = {};
|
|
15
|
-
ctx.options =
|
|
15
|
+
ctx.options = app.opts || {};
|
|
16
16
|
ctx.req = request;
|
|
17
17
|
ctx.res = { status: null, headers: {}, cookies: {} };
|
|
18
18
|
ctx.method = request.method.toLowerCase();
|
|
@@ -25,7 +25,9 @@ export default async (request, options = {}, app) => {
|
|
|
25
25
|
};
|
|
26
26
|
ctx.unstableFire = (name, data) => {
|
|
27
27
|
if (!events[name]) return;
|
|
28
|
-
events[name]
|
|
28
|
+
for (const cb of events[name]) {
|
|
29
|
+
cb(data);
|
|
30
|
+
}
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
ctx.headers = parseHeaders(new Headers(chunkArray(request.rawHeaders, 2)));
|
|
@@ -33,7 +35,7 @@ export default async (request, options = {}, app) => {
|
|
|
33
35
|
await auth.load(ctx);
|
|
34
36
|
|
|
35
37
|
const https = request.connection.encrypted ? "https" : "http";
|
|
36
|
-
const host = ctx.headers.host ||
|
|
38
|
+
const host = ctx.headers.host || `localhost:${ctx.options.port}`;
|
|
37
39
|
const path = request.url.replace(/\/$/, "") || "/";
|
|
38
40
|
ctx.url = new URL(path, `${https}://${host}`);
|
|
39
41
|
define(ctx.url, "query", (url) =>
|
|
@@ -51,7 +53,7 @@ export default async (request, options = {}, app) => {
|
|
|
51
53
|
ctx.body = await parseBody(
|
|
52
54
|
Buffer.concat(body).toString(),
|
|
53
55
|
type,
|
|
54
|
-
options.uploads,
|
|
56
|
+
ctx.options.uploads,
|
|
55
57
|
);
|
|
56
58
|
resolve();
|
|
57
59
|
})
|
package/src/context/parseBody.js
CHANGED
|
@@ -2,12 +2,12 @@ import { createId } from "../helpers/index.js";
|
|
|
2
2
|
|
|
3
3
|
function getBoundary(header) {
|
|
4
4
|
if (!header) return null;
|
|
5
|
-
|
|
5
|
+
const items = header.split(";");
|
|
6
6
|
if (items)
|
|
7
|
-
for (
|
|
8
|
-
|
|
7
|
+
for (let j = 0; j < items.length; j++) {
|
|
8
|
+
const item = new String(items[j]).trim();
|
|
9
9
|
if (item.indexOf("boundary") >= 0) {
|
|
10
|
-
|
|
10
|
+
const k = item.split("=");
|
|
11
11
|
return new String(k[1]).trim();
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -48,7 +48,7 @@ export default async function parseBody(raw, contentType, bucket) {
|
|
|
48
48
|
const body = {};
|
|
49
49
|
|
|
50
50
|
const rawDataArray = rawData.split(boundary);
|
|
51
|
-
for (
|
|
51
|
+
for (const item of rawDataArray) {
|
|
52
52
|
// Use non-matching groups to exclude part of the result
|
|
53
53
|
const name = getMatching(item, /(?:name=")(.+?)(?:")/)
|
|
54
54
|
.trim()
|
package/src/context/winter.js
CHANGED
|
@@ -3,10 +3,10 @@ import { define, parseHeaders } from "../helpers/index.js";
|
|
|
3
3
|
import parseBody from "./parseBody.js";
|
|
4
4
|
import parseCookies from "./parseCookies.js";
|
|
5
5
|
|
|
6
|
-
export default async (request,
|
|
6
|
+
export default async (request, app) => {
|
|
7
7
|
const ctx = {};
|
|
8
8
|
ctx.init = performance.now();
|
|
9
|
-
ctx.options =
|
|
9
|
+
ctx.options = app.opts || {};
|
|
10
10
|
ctx.req = request;
|
|
11
11
|
ctx.res = { status: null, headers: {}, cookies: {} };
|
|
12
12
|
ctx.method = request.method.toLowerCase();
|
|
@@ -19,7 +19,9 @@ export default async (request, options = {}, app) => {
|
|
|
19
19
|
};
|
|
20
20
|
ctx.unstableFire = (name, data) => {
|
|
21
21
|
if (!events[name]) return;
|
|
22
|
-
events[name]
|
|
22
|
+
for (const cb of events[name]) {
|
|
23
|
+
cb(data);
|
|
24
|
+
}
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
ctx.headers = parseHeaders(request.headers);
|
|
@@ -33,7 +35,7 @@ export default async (request, options = {}, app) => {
|
|
|
33
35
|
|
|
34
36
|
if (request.body) {
|
|
35
37
|
const type = ctx.headers["content-type"];
|
|
36
|
-
ctx.body = await parseBody(request, type, options.uploads);
|
|
38
|
+
ctx.body = await parseBody(request, type, ctx.options.uploads);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
ctx.app = app;
|
package/src/errors/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import ServerError from "../ServerError.js";
|
|
2
2
|
|
|
3
3
|
ServerError.extend({
|
|
4
|
-
NO_STORE:
|
|
5
|
-
NO_STORE_WRITE:
|
|
6
|
-
NO_STORE_READ:
|
|
4
|
+
NO_STORE: "You need a 'store' to write 'ctx.session'",
|
|
5
|
+
NO_STORE_WRITE: "You need a 'store' to write 'ctx.session.{key}'",
|
|
6
|
+
NO_STORE_READ: "You need a 'store' to read 'ctx.session.{key}'",
|
|
7
7
|
|
|
8
8
|
AUTH_ARGON_NEEDED:
|
|
9
9
|
"Argon2 is needed for the auth module, please install it with 'npm i argon2'",
|
|
10
|
-
AUTH_INVALID_TYPE:
|
|
11
|
-
AUTH_INVALID_TOKEN:
|
|
12
|
-
AUTH_INVALID_COOKIE:
|
|
13
|
-
AUTH_NO_PROVIDER:
|
|
14
|
-
AUTH_INVALID_PROVIDER:
|
|
10
|
+
AUTH_INVALID_TYPE: "Invalid Authorization type, '{type}'",
|
|
11
|
+
AUTH_INVALID_TOKEN: "Invalid Authorization token",
|
|
12
|
+
AUTH_INVALID_COOKIE: "Invalid Authorization cookie",
|
|
13
|
+
AUTH_NO_PROVIDER: "No provider passed to the option 'auth.provider'",
|
|
14
|
+
AUTH_INVALID_PROVIDER:
|
|
15
|
+
"Invalid provider '{provider}', valid ones are: '{valid}'",
|
|
15
16
|
AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
|
|
16
17
|
AUTH_NO_USER: {
|
|
17
18
|
status: 401,
|
|
@@ -22,14 +23,14 @@ ServerError.extend({
|
|
|
22
23
|
LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
|
|
23
24
|
LOGIN_NO_PASSWORD: "The email is required to log in",
|
|
24
25
|
LOGIN_INVALID_PASSWORD: "The password you wrote is not correct",
|
|
25
|
-
LOGIN_WRONG_ACCOUNT:
|
|
26
|
-
LOGIN_WRONG_PASSWORD:
|
|
26
|
+
LOGIN_WRONG_ACCOUNT: "That email does not correspond to any account",
|
|
27
|
+
LOGIN_WRONG_PASSWORD: "That is not the valid password",
|
|
27
28
|
|
|
28
|
-
REGISTER_NO_EMAIL:
|
|
29
|
+
REGISTER_NO_EMAIL: "Email needed",
|
|
29
30
|
REGISTER_INVALID_EMAIL: "The email you wrote is not correct",
|
|
30
|
-
REGISTER_NO_PASSWORD:
|
|
31
|
+
REGISTER_NO_PASSWORD: "Password needed",
|
|
31
32
|
REGISTER_INVALID_PASSWORD: "The password you wrote is not correct",
|
|
32
|
-
REGISTER_EMAIL_EXISTS:
|
|
33
|
+
REGISTER_EMAIL_EXISTS: "Email is already registered",
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
export default ServerError;
|
package/src/helpers/bucket.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
|
-
import fsp from "fs/promises";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
5
4
|
|
|
6
5
|
// A fake tiny implementation of a generic bucket, it needs
|
|
7
6
|
// at the very least a read(id) and write(id, value), both returning
|
|
@@ -13,7 +12,7 @@ export default function (root) {
|
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
const absolute = (name) => {
|
|
16
|
-
if (!name) throw new Error(
|
|
15
|
+
if (!name) throw new Error("File name is required");
|
|
17
16
|
return path.resolve(path.join(root, name));
|
|
18
17
|
};
|
|
19
18
|
|
|
@@ -27,9 +26,8 @@ export default function (root) {
|
|
|
27
26
|
const fullPath = absolute(name);
|
|
28
27
|
if (value) {
|
|
29
28
|
return fsp.writeFile(fullPath, value, type).then(() => fullPath);
|
|
30
|
-
} else {
|
|
31
|
-
return fs.createWriteStream(fullPath);
|
|
32
29
|
}
|
|
30
|
+
return fs.createWriteStream(fullPath);
|
|
33
31
|
},
|
|
34
32
|
};
|
|
35
33
|
}
|
package/src/helpers/config.js
CHANGED
|
@@ -8,7 +8,7 @@ export default function config(options = {}) {
|
|
|
8
8
|
|
|
9
9
|
// Basic options
|
|
10
10
|
options.port = options.port || env.PORT || 3000;
|
|
11
|
-
options.secret = options.secret || env.SECRET ||
|
|
11
|
+
options.secret = options.secret || env.SECRET || `unsafe-${createId()}`;
|
|
12
12
|
|
|
13
13
|
// CORS
|
|
14
14
|
options.cors = options.cors || env.CORS || null;
|
|
@@ -9,8 +9,8 @@ export default function createCookies(cookies) {
|
|
|
9
9
|
val = { value: val };
|
|
10
10
|
}
|
|
11
11
|
const { value, path, expires } = val;
|
|
12
|
-
const pathPart =
|
|
13
|
-
const expiresPart = expires ?
|
|
12
|
+
const pathPart = `;Path=${path || "/"}`;
|
|
13
|
+
const expiresPart = expires ? `;Expires=${expires}` : "";
|
|
14
14
|
return `${key}=${value || ""}${pathPart}${expiresPart}`;
|
|
15
15
|
});
|
|
16
16
|
}
|
package/src/helpers/createId.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const alphabet =
|
|
2
2
|
"useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export const random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
5
5
|
|
|
6
6
|
// Credit: https://stackoverflow.com/a/52171480/938236
|
|
7
7
|
const cyrb53 = (str, seed = 0) => {
|
|
@@ -35,7 +35,7 @@ const hash = (str, size) => {
|
|
|
35
35
|
|
|
36
36
|
const randomId = (size = 16) => {
|
|
37
37
|
let id = "";
|
|
38
|
-
|
|
38
|
+
const bytes = random(size);
|
|
39
39
|
while (size--) {
|
|
40
40
|
// Using the bitwise AND operator to "cap" the value of
|
|
41
41
|
// the random byte from 255 to 63, in that way we can make sure
|
|
@@ -4,14 +4,14 @@ import define from "./define.js";
|
|
|
4
4
|
import validate from "./validate.js";
|
|
5
5
|
|
|
6
6
|
export default async function handleRequest(handlers, ctx) {
|
|
7
|
-
for (
|
|
7
|
+
for (const [method, matcher, ...cbs] of handlers[ctx.method]) {
|
|
8
8
|
const match = pathPattern(matcher, ctx.url.pathname || "/");
|
|
9
9
|
// Skip this whole middleware if there was no match
|
|
10
10
|
if (!match) continue;
|
|
11
11
|
|
|
12
12
|
define(ctx.url, "params", () => match);
|
|
13
13
|
|
|
14
|
-
for (
|
|
14
|
+
for (const cb of cbs) {
|
|
15
15
|
if (typeof cb === "function") {
|
|
16
16
|
const res = await cb(ctx);
|
|
17
17
|
const out = await parseResponse(res, ctx);
|
package/src/helpers/jsx.js
CHANGED
|
@@ -65,7 +65,7 @@ export const jsx = (tag, { children, ...props }) => {
|
|
|
65
65
|
: `${altAttrs[k.toLowerCase()] || encode(k)}="${encode(v)}"`,
|
|
66
66
|
)
|
|
67
67
|
.join(" ");
|
|
68
|
-
if (attrStr) attrStr =
|
|
68
|
+
if (attrStr) attrStr = ` ${attrStr}`;
|
|
69
69
|
if (SELFCLOSE.has(tag)) return () => `<${tag}${attrStr} />`;
|
|
70
70
|
const doctype = tag === "html" ? "<!DOCTYPE html>" : "";
|
|
71
71
|
return () => `${doctype}<${tag}${attrStr}>${children}</${tag}>`;
|
package/src/helpers/jsx.test.jsx
CHANGED
|
@@ -16,7 +16,7 @@ expect.extend({
|
|
|
16
16
|
|
|
17
17
|
describe("jsx", () => {
|
|
18
18
|
it("can render a div", () => {
|
|
19
|
-
expect(<div>Hello</div>).toRender(
|
|
19
|
+
expect(<div>Hello</div>).toRender("<div>Hello</div>");
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("can render an input with attributes", () => {
|
|
@@ -40,6 +40,7 @@ describe("jsx", () => {
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it("will inject the doctype for html", () => {
|
|
43
|
+
// biome-ignore lint/a11y/useHtmlLang: This is an example, not user code
|
|
43
44
|
expect(<html>Hello</html>).toRender("<!DOCTYPE html><html>Hello</html>");
|
|
44
45
|
expect(<html lang="en">Hello</html>).toRender(
|
|
45
46
|
`<!DOCTYPE html><html lang="en">Hello</html>`,
|
package/src/index.js
CHANGED
|
@@ -84,7 +84,7 @@ export default function server(options = {}) {
|
|
|
84
84
|
server.prototype.self = function () {
|
|
85
85
|
const cb = this.callback.bind(this);
|
|
86
86
|
const proto = Object.getPrototypeOf(this);
|
|
87
|
-
for (
|
|
87
|
+
for (const key in { ...proto, ...this }) {
|
|
88
88
|
if (typeof this[key] === "function") {
|
|
89
89
|
cb[key] = this[key].bind(this);
|
|
90
90
|
} else {
|
|
@@ -97,11 +97,11 @@ server.prototype.self = function () {
|
|
|
97
97
|
// #region Runtimes
|
|
98
98
|
// Node.js
|
|
99
99
|
server.prototype.node = async function () {
|
|
100
|
-
const http = await import("http");
|
|
100
|
+
const http = await import("node:http");
|
|
101
101
|
http
|
|
102
102
|
.createServer(async (request, response) => {
|
|
103
103
|
try {
|
|
104
|
-
const ctx = await createNodeContext(request, this
|
|
104
|
+
const ctx = await createNodeContext(request, this);
|
|
105
105
|
const out = await handleRequest(this.handlers, ctx);
|
|
106
106
|
|
|
107
107
|
response.writeHead(out.status || 200, parseHeaders(out.headers));
|
|
@@ -128,7 +128,7 @@ server.prototype.callback = async function (request, context) {
|
|
|
128
128
|
if (typeof Netlify === "undefined") {
|
|
129
129
|
throw new Error("Netlify doesn't exist");
|
|
130
130
|
}
|
|
131
|
-
const ctx = await createWinterContext(request, this
|
|
131
|
+
const ctx = await createWinterContext(request, this);
|
|
132
132
|
return await handleRequest(this.handlers, ctx);
|
|
133
133
|
} catch (error) {
|
|
134
134
|
return new Response(error.message, { status: error.status || 500 });
|
|
@@ -140,9 +140,11 @@ server.prototype.fetch = async function (request, env) {
|
|
|
140
140
|
if (env?.upgrade(request)) return;
|
|
141
141
|
Object.assign(globalThis.env, env); // Extend env with the passed vars
|
|
142
142
|
|
|
143
|
-
let ctx
|
|
143
|
+
let ctx;
|
|
144
|
+
let res;
|
|
145
|
+
let error;
|
|
144
146
|
try {
|
|
145
|
-
ctx = await createWinterContext(request, this
|
|
147
|
+
ctx = await createWinterContext(request, this);
|
|
146
148
|
res = await handleRequest(this.handlers, ctx);
|
|
147
149
|
} catch (err) {
|
|
148
150
|
error = err;
|
|
@@ -158,7 +160,7 @@ server.prototype.handle = function (method, path, ...middleware) {
|
|
|
158
160
|
// Do not try to optimize, we NEED the method to remain '*' here so that
|
|
159
161
|
// it doesn't auto-finish
|
|
160
162
|
if (method === "*") {
|
|
161
|
-
for (
|
|
163
|
+
for (const m in this.handlers) {
|
|
162
164
|
this.handlers[m].push([method, path, ...middleware]);
|
|
163
165
|
}
|
|
164
166
|
} else {
|
|
@@ -209,11 +211,11 @@ server.prototype.use = function (...middleware) {
|
|
|
209
211
|
|
|
210
212
|
// Unwind the children routers into the main router
|
|
211
213
|
server.prototype.router = function (basePath, router) {
|
|
212
|
-
basePath =
|
|
213
|
-
for (const
|
|
214
|
-
|
|
214
|
+
basePath = `/${basePath}/`.replace(/^\/+/, "/").replace(/\/+$/, "/");
|
|
215
|
+
for (const m in router.handlers) {
|
|
216
|
+
for (const [method, path, ...callbacks] of router.handlers[m]) {
|
|
215
217
|
this.handle(method, basePath + path.replace(/^\//, ""), ...callbacks);
|
|
216
|
-
}
|
|
218
|
+
}
|
|
217
219
|
}
|
|
218
220
|
return this.self();
|
|
219
221
|
};
|
|
@@ -231,7 +233,7 @@ server.prototype.test = function () {
|
|
|
231
233
|
options.headers.cookie = cookie;
|
|
232
234
|
}
|
|
233
235
|
const res = await this.fetch(
|
|
234
|
-
new Request(
|
|
236
|
+
new Request(`http://localhost:3000${path}`, options),
|
|
235
237
|
);
|
|
236
238
|
|
|
237
239
|
const headers = parseHeaders(res.headers);
|
package/src/parseResponse.js
CHANGED
|
@@ -81,15 +81,15 @@ export default async function parseResponse(out, ctx) {
|
|
|
81
81
|
// Cookies to headers
|
|
82
82
|
if (ctx.options.cookies) {
|
|
83
83
|
if (Object.keys(ctx.res.cookies).length) {
|
|
84
|
-
|
|
84
|
+
for (const cookie of ctx.res.cookies) {
|
|
85
85
|
ctx.res.headers.append("set-cookie", cookie);
|
|
86
|
-
}
|
|
86
|
+
}
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// Add the headers that are neeeded
|
|
91
91
|
if (ctx?.res?.headers) {
|
|
92
|
-
for (
|
|
92
|
+
for (const key in ctx.res.headers) {
|
|
93
93
|
out.headers[key] = ctx.res.headers[key];
|
|
94
94
|
}
|
|
95
95
|
}
|
package/src/pathPattern.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default function pathPattern(pattern, path) {
|
|
2
2
|
if (pattern === "*") return {};
|
|
3
3
|
|
|
4
|
-
pattern =
|
|
4
|
+
pattern = `/${pattern.replace(/^\//, "")}`;
|
|
5
5
|
pattern = pattern.replace(/\/$/, "") || "/";
|
|
6
6
|
path = path.replace(/\/$/, "") || "/";
|
|
7
7
|
|
package/src/reply.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from "fs/promises";
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
2
|
|
|
3
3
|
import { createCookies, toWeb, types } from "./helpers/index.js";
|
|
4
4
|
|
|
@@ -12,9 +12,9 @@ function Reply() {
|
|
|
12
12
|
// INTERNAL
|
|
13
13
|
Reply.prototype.generateHeaders = function () {
|
|
14
14
|
const headers = new Headers(this.res.headers);
|
|
15
|
-
createCookies(this.res.cookies)
|
|
15
|
+
for (const cookie of createCookies(this.res.cookies)) {
|
|
16
16
|
headers.append("set-cookie", cookie);
|
|
17
|
-
}
|
|
17
|
+
}
|
|
18
18
|
return headers;
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -32,9 +32,9 @@ Reply.prototype.type = function (type) {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
// Prompt for download from the user side
|
|
35
|
-
Reply.prototype.download = function (name
|
|
35
|
+
Reply.prototype.download = function (name, type) {
|
|
36
36
|
// filename.txt and no explicit type => add headers "text/plain"
|
|
37
|
-
if (name && !
|
|
37
|
+
if (name && !type) type = name.split(".")[1];
|
|
38
38
|
|
|
39
39
|
// Add the Content-Type if there's a type
|
|
40
40
|
if (type) this.type(type);
|
|
@@ -46,7 +46,7 @@ Reply.prototype.download = function (name = "", type) {
|
|
|
46
46
|
// Set extra headers
|
|
47
47
|
Reply.prototype.headers = function (headers) {
|
|
48
48
|
if (!headers || typeof headers !== "object") return this;
|
|
49
|
-
for (
|
|
49
|
+
for (const key in headers) {
|
|
50
50
|
this.res.headers[key] = headers[key];
|
|
51
51
|
}
|
|
52
52
|
return this;
|
|
@@ -55,7 +55,7 @@ Reply.prototype.headers = function (headers) {
|
|
|
55
55
|
// Set extra cookies
|
|
56
56
|
Reply.prototype.cookies = function (cookies) {
|
|
57
57
|
if (!cookies || typeof cookies !== "object") return this;
|
|
58
|
-
for (
|
|
58
|
+
for (const key in cookies) {
|
|
59
59
|
if (typeof cookies[key] === "string") {
|
|
60
60
|
this.res.cookies[key] = { value: cookies[key] };
|
|
61
61
|
} else {
|
package/src/router.test.js
CHANGED
|
@@ -2,9 +2,9 @@ import server, { router, status } from "./index.js";
|
|
|
2
2
|
|
|
3
3
|
describe("can route properly", () => {
|
|
4
4
|
const apiRouter = router()
|
|
5
|
-
.get("/hello", (ctx) =>
|
|
6
|
-
.put("/hello", (ctx) =>
|
|
7
|
-
.post("/hello", (ctx) =>
|
|
5
|
+
.get("/hello", (ctx) => `Hello ${ctx.url.pathname}`)
|
|
6
|
+
.put("/hello", (ctx) => `Hello ${ctx.url.pathname}`)
|
|
7
|
+
.post("/hello", (ctx) => `Hello ${ctx.url.pathname}`);
|
|
8
8
|
|
|
9
9
|
const app = server()
|
|
10
10
|
.router("/", apiRouter)
|
package/src/session.test.js
CHANGED
|
@@ -5,11 +5,11 @@ import server from "./index.js";
|
|
|
5
5
|
describe("session", () => {
|
|
6
6
|
const store = kv(new Map());
|
|
7
7
|
const api = server({ store })
|
|
8
|
-
.get("/hello", (ctx) =>
|
|
8
|
+
.get("/hello", (ctx) => `Hello ${ctx.session.a}`)
|
|
9
9
|
.post("/hello", (ctx) => {
|
|
10
10
|
if (!ctx.session.a) ctx.session.a = 0;
|
|
11
11
|
ctx.session.a += 1;
|
|
12
|
-
return
|
|
12
|
+
return `Bye ${ctx.session.a}`;
|
|
13
13
|
})
|
|
14
14
|
.get("/", () => "Fallback")
|
|
15
15
|
.test();
|
|
@@ -44,7 +44,7 @@ describe("session", () => {
|
|
|
44
44
|
|
|
45
45
|
describe("missing store", () => {
|
|
46
46
|
const api = server({ store: null })
|
|
47
|
-
.get("/read", (ctx) =>
|
|
47
|
+
.get("/read", (ctx) => `Bye ${ctx.session.a}`)
|
|
48
48
|
.get("/write", (ctx) => {
|
|
49
49
|
ctx.session.a = "hello";
|
|
50
50
|
return "All good";
|