@server/next 0.20.50 → 0.21.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/package.json +2 -3
- package/src/auth/auth.js +9 -6
- package/src/auth/index.js +7 -0
- package/src/auth/index.test.js +17 -17
- package/src/auth/providers/email.js +13 -12
- package/src/auth/providers/github.js +82 -0
- package/src/auth/providers/index.js +2 -1
- package/src/auth/user.js +1 -2
- package/src/context/parseBody.test.js +3 -3
- package/src/helpers/config.js +4 -1
- package/src/helpers/cookies.test.js +2 -2
- package/src/helpers/createCookies.js +1 -1
- package/src/helpers/createId.js +40 -5
- package/src/parseResponse.js +1 -1
- package/src/reply.js +5 -0
- package/src/auth/findUser.js +0 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@server/next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -34,11 +34,10 @@
|
|
|
34
34
|
"index.d.ts",
|
|
35
35
|
"jsx-dev-runtime.js"
|
|
36
36
|
],
|
|
37
|
-
"dependencies": {},
|
|
38
37
|
"devDependencies": {
|
|
39
38
|
"argon2": "^0.40.3",
|
|
40
39
|
"jest": "^29.7.0",
|
|
41
|
-
"polystore": "^0.
|
|
40
|
+
"polystore": "^0.14.1"
|
|
42
41
|
},
|
|
43
42
|
"engines": {
|
|
44
43
|
"node": ">=20.0.0"
|
package/src/auth/auth.js
CHANGED
|
@@ -5,14 +5,14 @@ const validateToken = (authorization) => {
|
|
|
5
5
|
if (type.toLowerCase() !== "bearer") {
|
|
6
6
|
throw ServerError.AUTH_INVALID_TYPE({ type });
|
|
7
7
|
}
|
|
8
|
-
if (id.length !==
|
|
8
|
+
if (id.length !== 16) {
|
|
9
9
|
throw ServerError.AUTH_INVALID_TOKEN();
|
|
10
10
|
}
|
|
11
11
|
return id;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const validateCookie = (authorization) => {
|
|
15
|
-
if (authorization.length !==
|
|
15
|
+
if (authorization.length !== 16) {
|
|
16
16
|
throw ServerError.AUTH_INVALID_COOKIE();
|
|
17
17
|
}
|
|
18
18
|
return authorization;
|
|
@@ -31,15 +31,15 @@ const findSessionId = (ctx) => {
|
|
|
31
31
|
|
|
32
32
|
if (type === "cookie") {
|
|
33
33
|
// If the user is not authenticated, there's no auth to retrieve
|
|
34
|
-
if (!ctx.cookies.
|
|
34
|
+
if (!ctx.cookies.authentication) return;
|
|
35
35
|
|
|
36
|
-
return validateCookie(ctx.cookies.
|
|
36
|
+
return validateCookie(ctx.cookies.authentication);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
throw new Error(
|
|
39
|
+
throw new Error(`Invalid auth type "${type}"`);
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
export default async function
|
|
42
|
+
export default async function auth(ctx) {
|
|
43
43
|
if (!ctx.options.auth) return; // NO AUTH AT ALL; nothing to do here
|
|
44
44
|
const options = ctx.options.auth;
|
|
45
45
|
|
|
@@ -47,7 +47,10 @@ export default async function findAuth(ctx) {
|
|
|
47
47
|
if (!sessionId) return; // NO SESSION FOUND; no auth
|
|
48
48
|
|
|
49
49
|
const auth = await options.session.get(sessionId);
|
|
50
|
+
// Mmh, which one to do...
|
|
50
51
|
if (!auth) throw ServerError.AUTH_NO_SESSION();
|
|
52
|
+
// if (!auth) return; // SESSION ALREADY INVALID; no auth
|
|
53
|
+
|
|
51
54
|
if (!auth.provider) throw ServerError.AUTH_NO_PROVIDER();
|
|
52
55
|
if (!options.provider.includes(auth.provider)) {
|
|
53
56
|
const valid = JSON.stringify(options.provider);
|
package/src/auth/index.js
CHANGED
|
@@ -14,6 +14,13 @@ const middle = async (ctx) => {
|
|
|
14
14
|
if (ctx.options.auth) {
|
|
15
15
|
ctx.app.post("/auth/logout", logout);
|
|
16
16
|
|
|
17
|
+
if (ctx.options.auth.provider.includes("github")) {
|
|
18
|
+
if (!env.GITHUB_ID) throw new Error("GITHUB_ID not defined");
|
|
19
|
+
if (!env.GITHUB_SECRET) throw new Error("GITHUB_SECRET not defined");
|
|
20
|
+
ctx.app.get("/auth/login/github", providers.github.login);
|
|
21
|
+
ctx.app.get("/auth/callback/github", providers.github.callback);
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
if (ctx.options.auth.provider.includes("email")) {
|
|
18
25
|
ctx.app.post("/auth/register/email", providers.email.register);
|
|
19
26
|
ctx.app.post("/auth/login/email", providers.email.login);
|
package/src/auth/index.test.js
CHANGED
|
@@ -4,7 +4,7 @@ import kv from "polystore";
|
|
|
4
4
|
|
|
5
5
|
import server from "../index.js";
|
|
6
6
|
|
|
7
|
-
const ID = "
|
|
7
|
+
const ID = "REqA2l022l8Q0tuI";
|
|
8
8
|
|
|
9
9
|
describe("auth", () => {
|
|
10
10
|
it("requires a provider", () => {
|
|
@@ -24,14 +24,14 @@ describe("auth", () => {
|
|
|
24
24
|
id: ID,
|
|
25
25
|
type: "token",
|
|
26
26
|
provider: "wrong",
|
|
27
|
-
user: "
|
|
27
|
+
user: "QypOn5SQApyOPdUp",
|
|
28
28
|
email: "abc@test.com",
|
|
29
29
|
time: "2024-07-01T03:21:40Z",
|
|
30
30
|
});
|
|
31
|
-
const authorization = "Bearer
|
|
31
|
+
const authorization = "Bearer REqA2l022l8Q0tuI";
|
|
32
32
|
const req = await api.get("/", { headers: { authorization } });
|
|
33
33
|
expect(req).not.toSucceed(
|
|
34
|
-
'Invalid provider "wrong", valid ones are: "email"'
|
|
34
|
+
'Invalid provider "wrong", valid ones are: "email"',
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
});
|
|
@@ -47,7 +47,7 @@ describe("token", () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("should be Bearer", async () => {
|
|
50
|
-
const authorization = "Basic
|
|
50
|
+
const authorization = "Basic REqA2l022l8Q0tuI";
|
|
51
51
|
const req = await api.get("/", { headers: { authorization } });
|
|
52
52
|
expect(req).not.toSucceed("Invalid Authorization type, 'Basic'");
|
|
53
53
|
});
|
|
@@ -59,7 +59,7 @@ describe("token", () => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it("cannot get the session", async () => {
|
|
62
|
-
const authorization = "Bearer
|
|
62
|
+
const authorization = "Bearer REqA2l022l8Q0tuI";
|
|
63
63
|
const req = await api.get("/", { headers: { authorization } });
|
|
64
64
|
expect(req).not.toSucceed("Invalid session");
|
|
65
65
|
});
|
|
@@ -69,29 +69,29 @@ describe("token", () => {
|
|
|
69
69
|
id: ID,
|
|
70
70
|
type: "token",
|
|
71
71
|
provider: "email",
|
|
72
|
-
user: "
|
|
72
|
+
user: "QypOn5SQApyOPdUp",
|
|
73
73
|
email: "abc@test.com",
|
|
74
74
|
time: "2024-07-01T03:21:40Z",
|
|
75
75
|
});
|
|
76
|
-
const authorization = "Bearer
|
|
76
|
+
const authorization = "Bearer REqA2l022l8Q0tuI";
|
|
77
77
|
const req = await api.get("/", { headers: { authorization } });
|
|
78
78
|
expect(req).not.toSucceed("Credentials do not correspond to a user");
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
it("cannot get the user", async () => {
|
|
82
|
-
store.set("auth:
|
|
83
|
-
id: "
|
|
82
|
+
store.set("auth:REqA2l022l8Q0tuI", {
|
|
83
|
+
id: "REqA2l022l8Q0tuI",
|
|
84
84
|
type: "token",
|
|
85
85
|
provider: "email",
|
|
86
|
-
user: "
|
|
86
|
+
user: "QypOn5SQApyOPdUp",
|
|
87
87
|
email: "abc@test.com",
|
|
88
88
|
time: "2024-07-01T03:21:40Z",
|
|
89
89
|
});
|
|
90
|
-
store.set("user:
|
|
91
|
-
id: "
|
|
90
|
+
store.set("user:QypOn5SQApyOPdUp", {
|
|
91
|
+
id: "QypOn5SQApyOPdUp",
|
|
92
92
|
email: "abc@test.com",
|
|
93
93
|
});
|
|
94
|
-
const authorization = "Bearer
|
|
94
|
+
const authorization = "Bearer REqA2l022l8Q0tuI";
|
|
95
95
|
const req = await api.get("/", { headers: { authorization } });
|
|
96
96
|
expect(req).toSucceed();
|
|
97
97
|
});
|
|
@@ -104,19 +104,19 @@ describe("cookie", () => {
|
|
|
104
104
|
.test();
|
|
105
105
|
|
|
106
106
|
it("should have the proper token in email", async () => {
|
|
107
|
-
const cookie = "
|
|
107
|
+
const cookie = "authentication=hello";
|
|
108
108
|
const req = await api.get("/", { headers: { cookie } });
|
|
109
109
|
expect(req).not.toSucceed("Invalid Authorization cookie");
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
it("can get the proper session", async () => {
|
|
113
|
-
const cookie = "
|
|
113
|
+
const cookie = "authentication=REqA2l022l8Q0tuI";
|
|
114
114
|
const req = await api.get("/", { headers: { cookie } });
|
|
115
115
|
expect(req).not.toSucceed("Invalid session");
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
it("can get the proper session", async () => {
|
|
119
|
-
const cookie = "
|
|
119
|
+
const cookie = "authentication=REqA2l022l8Q0tuI";
|
|
120
120
|
const req = await api.get("/", { headers: { cookie } });
|
|
121
121
|
expect(req).not.toSucceed();
|
|
122
122
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createId } from "../../helpers/index.js";
|
|
2
2
|
import { ServerError, status } from "../../index.js";
|
|
3
|
-
import findUser from "../findUser.js";
|
|
4
3
|
import updateUser from "../updateUser.js";
|
|
5
4
|
|
|
6
5
|
const hash = new Proxy(
|
|
@@ -12,7 +11,7 @@ const hash = new Proxy(
|
|
|
12
11
|
self,
|
|
13
12
|
await import("argon2").catch(() => {
|
|
14
13
|
throw new ServerError.AUTH_ARGON_NEEDED();
|
|
15
|
-
})
|
|
14
|
+
}),
|
|
16
15
|
);
|
|
17
16
|
if (key === "verify" && !self.verify) {
|
|
18
17
|
return async (hash, pass) => {
|
|
@@ -28,29 +27,29 @@ const hash = new Proxy(
|
|
|
28
27
|
}
|
|
29
28
|
return self[key];
|
|
30
29
|
},
|
|
31
|
-
}
|
|
30
|
+
},
|
|
32
31
|
);
|
|
33
32
|
|
|
34
33
|
const createSession = async (user, ctx) => {
|
|
35
|
-
const { type, session, cleanUser } = ctx.options.auth;
|
|
34
|
+
const { type, session, cleanUser, redirect = "/user" } = ctx.options.auth;
|
|
36
35
|
user = cleanUser(user);
|
|
37
|
-
const
|
|
36
|
+
const id = createId();
|
|
38
37
|
const provider = "email";
|
|
39
38
|
const time = new Date().toISOString().replace(/\.[0-9]*/, "");
|
|
40
39
|
ctx.auth = {
|
|
41
|
-
id
|
|
40
|
+
id,
|
|
42
41
|
type,
|
|
43
42
|
provider,
|
|
44
|
-
user: user.
|
|
43
|
+
user: user.email,
|
|
45
44
|
email: user.email,
|
|
46
45
|
time,
|
|
47
46
|
};
|
|
48
|
-
await session.set(
|
|
47
|
+
await session.set(id, ctx.auth, { expires: "1w" });
|
|
49
48
|
|
|
50
49
|
if (type === "token") {
|
|
51
|
-
return status(201).json({ ...user, token });
|
|
50
|
+
return status(201).json({ ...user, token: id });
|
|
52
51
|
} else if (type === "cookie") {
|
|
53
|
-
return status(
|
|
52
|
+
return status(302).cookies({ authentication: id }).redirect(redirect);
|
|
54
53
|
} else if (type === "jwt") {
|
|
55
54
|
throw new Error("JWT auth not supported yet");
|
|
56
55
|
} else if (type === "key") {
|
|
@@ -87,10 +86,12 @@ async function register(ctx) {
|
|
|
87
86
|
const store = ctx.options.auth.store;
|
|
88
87
|
if (await store.has(email)) throw ServerError.REGISTER_EMAIL_EXISTS();
|
|
89
88
|
|
|
89
|
+
const time = new Date().toISOString().replace(/\.[0-9]*/, "");
|
|
90
90
|
const user = {
|
|
91
|
-
id: createId(),
|
|
91
|
+
id: createId(user.email),
|
|
92
92
|
email,
|
|
93
93
|
password: await hash.hash(password),
|
|
94
|
+
time,
|
|
94
95
|
...data,
|
|
95
96
|
};
|
|
96
97
|
await store.set(email, user);
|
|
@@ -123,7 +124,7 @@ async function reset(ctx) {
|
|
|
123
124
|
async function password(ctx) {
|
|
124
125
|
const { previous, updated } = ctx.body;
|
|
125
126
|
|
|
126
|
-
const fullUser = await
|
|
127
|
+
const fullUser = await ctx.options.auth.store.get(ctx.auth.user);
|
|
127
128
|
|
|
128
129
|
const isValid = await hash.verify(fullUser.password, previous);
|
|
129
130
|
if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import createId from "../../helpers/createId.js";
|
|
2
|
+
import { redirect, status } from "../../reply.js";
|
|
3
|
+
|
|
4
|
+
const oauth = async (code) => {
|
|
5
|
+
const fch = async (url, { body, headers = {}, ...rest } = {}) => {
|
|
6
|
+
headers.accept = "application/json";
|
|
7
|
+
headers["content-type"] = "application/json";
|
|
8
|
+
const res = await fetch(url, { ...rest, body, headers });
|
|
9
|
+
if (!res.ok) throw new Error("Invalid request");
|
|
10
|
+
return res.json();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const res = await fch("https://github.com/login/oauth/access_token", {
|
|
14
|
+
method: "post",
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
client_id: env.GITHUB_ID,
|
|
17
|
+
client_secret: env.GITHUB_SECRET,
|
|
18
|
+
code,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
return (path) => {
|
|
22
|
+
return fch("https://api.github.com" + path, {
|
|
23
|
+
headers: { Authorization: "Bearer " + res.access_token },
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const login = (ctx) => {
|
|
29
|
+
return redirect(
|
|
30
|
+
`https://github.com/login/oauth/authorize?client_id=${env.GITHUB_ID}&scope=user:email`,
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getUserProfile = async (code) => {
|
|
35
|
+
const api = await oauth(code);
|
|
36
|
+
const [profile, emails] = await Promise.all([
|
|
37
|
+
api("/user"),
|
|
38
|
+
api("/user/emails"),
|
|
39
|
+
]);
|
|
40
|
+
const email = emails.sort((a) => (a.primary ? -1 : 1))[0]?.email;
|
|
41
|
+
return { ...profile, email };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const callback = async (ctx) => {
|
|
45
|
+
const { type, cleanUser, store, session, redirect } = ctx.options.auth;
|
|
46
|
+
|
|
47
|
+
const profile = await getUserProfile(ctx.url.query.code);
|
|
48
|
+
|
|
49
|
+
const auth = {
|
|
50
|
+
id: createId(),
|
|
51
|
+
type,
|
|
52
|
+
provider: "github",
|
|
53
|
+
user: createId(profile.email),
|
|
54
|
+
email: profile.email,
|
|
55
|
+
time: new Date().toISOString().replace(/\.[0-9]*/, ""),
|
|
56
|
+
};
|
|
57
|
+
const user = cleanUser({
|
|
58
|
+
id: profile.id,
|
|
59
|
+
name: profile.name,
|
|
60
|
+
email: profile.email,
|
|
61
|
+
picture: profile.avatar_url,
|
|
62
|
+
location: profile.location,
|
|
63
|
+
created: profile.created_at,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await store.set(auth.user, user);
|
|
67
|
+
await session.set(auth.id, auth, { expires: "1w" });
|
|
68
|
+
|
|
69
|
+
if (auth.type === "token") {
|
|
70
|
+
return status(201).json({ ...user, token: auth.id });
|
|
71
|
+
} else if (auth.type === "cookie") {
|
|
72
|
+
return status(302).cookies({ authentication: auth.id }).redirect(redirect);
|
|
73
|
+
} else if (auth.type === "jwt") {
|
|
74
|
+
throw new Error("JWT auth not supported yet");
|
|
75
|
+
} else if (auth.type === "key") {
|
|
76
|
+
throw new Error("Key auth not supported yet");
|
|
77
|
+
} else {
|
|
78
|
+
throw new Error("Unknown auth type");
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default { login, callback };
|
package/src/auth/user.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import ServerError from "../ServerError.js";
|
|
2
|
-
import findUser from "./findUser.js";
|
|
3
2
|
|
|
4
3
|
export default async function user(ctx) {
|
|
5
4
|
if (!ctx.auth) return;
|
|
6
5
|
|
|
7
|
-
const user = await
|
|
6
|
+
const user = await ctx.options.auth.store.get(ctx.auth.user);
|
|
8
7
|
if (!user) throw ServerError.AUTH_NO_USER();
|
|
9
8
|
return ctx.options.auth.cleanUser(user);
|
|
10
9
|
}
|
|
@@ -41,15 +41,15 @@ describe("parseBody", () => {
|
|
|
41
41
|
const body = await parseBody(
|
|
42
42
|
getBody(),
|
|
43
43
|
"multipart/form-data; boundary=----WebKitFormBoundaryvef1fLxmoUdYZWXp",
|
|
44
|
-
{ write: (id) => id }
|
|
44
|
+
{ write: (id) => id },
|
|
45
45
|
);
|
|
46
46
|
expect(body).toMatchObject({
|
|
47
47
|
hello: "world",
|
|
48
48
|
test: ["test message 123456", "test message number two"],
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
const matchMd = expect.stringMatching(/^\w{
|
|
52
|
-
const matchTxt = expect.stringMatching(/^\w{
|
|
51
|
+
const matchMd = expect.stringMatching(/^\w{16}.md$/);
|
|
52
|
+
const matchTxt = expect.stringMatching(/^\w{16}.txt$/);
|
|
53
53
|
expect(body).toMatchObject({
|
|
54
54
|
profile: matchMd,
|
|
55
55
|
gallery: [matchTxt, matchTxt],
|
package/src/helpers/config.js
CHANGED
|
@@ -58,10 +58,13 @@ export default function config(options) {
|
|
|
58
58
|
}
|
|
59
59
|
if (!options.auth.cleanUser) {
|
|
60
60
|
options.auth.cleanUser = (fullUser) => {
|
|
61
|
-
const { password, ...user } = fullUser;
|
|
61
|
+
const { password, token, ...user } = fullUser;
|
|
62
62
|
return user;
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
if (!options.auth.redirect) {
|
|
66
|
+
options.auth.redirect = "/user";
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
return options;
|
|
@@ -12,12 +12,12 @@ describe("set-cookie", () => {
|
|
|
12
12
|
it("sets the right cookie", async () => {
|
|
13
13
|
const api = app.test();
|
|
14
14
|
const res = await api.get("/hello");
|
|
15
|
-
expect(res.headers["set-cookie"]).toBe("hello=world");
|
|
15
|
+
expect(res.headers["set-cookie"]).toBe("hello=world;Path=/");
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("can set multiple cookies", async () => {
|
|
19
19
|
const api = app.test();
|
|
20
20
|
const res = await api.get("/multiple");
|
|
21
|
-
expect(res.headers["set-cookie"]).toEqual(["a=b", "c=d"]);
|
|
21
|
+
expect(res.headers["set-cookie"]).toEqual(["a=b;Path=/", "c=d;Path=/"]);
|
|
22
22
|
});
|
|
23
23
|
});
|
|
@@ -9,7 +9,7 @@ export default function createCookies(cookies) {
|
|
|
9
9
|
val = { value: val };
|
|
10
10
|
}
|
|
11
11
|
const { value, path, expires } = val;
|
|
12
|
-
const pathPart =
|
|
12
|
+
const pathPart = ";Path=" + (path || "/");
|
|
13
13
|
const expiresPart = expires ? ";Expires=" + expires : "";
|
|
14
14
|
return `${key}=${value || ""}${pathPart}${expiresPart}`;
|
|
15
15
|
});
|
package/src/helpers/createId.js
CHANGED
|
@@ -1,17 +1,52 @@
|
|
|
1
|
-
const
|
|
1
|
+
const alphabet =
|
|
2
2
|
"useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
|
|
3
3
|
|
|
4
4
|
export let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// Credit: https://stackoverflow.com/a/52171480/938236
|
|
7
|
+
const cyrb53 = (str, seed = 0) => {
|
|
8
|
+
if (typeof str !== "string") str = String(str);
|
|
9
|
+
let h1 = 0xdeadbeef ^ seed;
|
|
10
|
+
let h2 = 0x41c6ce57 ^ seed;
|
|
11
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
12
|
+
ch = str.charCodeAt(i);
|
|
13
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
14
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
15
|
+
}
|
|
16
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
|
17
|
+
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
18
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
|
19
|
+
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
20
|
+
|
|
21
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const hash = (str, size) => {
|
|
25
|
+
let chars = "";
|
|
26
|
+
|
|
27
|
+
let num = cyrb53(str);
|
|
28
|
+
for (let i = 0; i < size; i++) {
|
|
29
|
+
if (num < alphabet.length) num = cyrb53(str, i);
|
|
30
|
+
chars += alphabet[num % alphabet.length];
|
|
31
|
+
num = Math.floor(num / alphabet.length);
|
|
32
|
+
}
|
|
33
|
+
return chars;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const randomId = (size = 16) => {
|
|
8
37
|
let id = "";
|
|
9
|
-
let bytes =
|
|
38
|
+
let bytes = random(size);
|
|
10
39
|
while (size--) {
|
|
11
40
|
// Using the bitwise AND operator to "cap" the value of
|
|
12
41
|
// the random byte from 255 to 63, in that way we can make sure
|
|
13
42
|
// that the value will be a valid index for the "chars" string.
|
|
14
|
-
id +=
|
|
43
|
+
id += alphabet[bytes[size] & 61];
|
|
15
44
|
}
|
|
16
45
|
return id;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default function createId(source) {
|
|
49
|
+
const size = 16;
|
|
50
|
+
if (source) return hash(source, size);
|
|
51
|
+
return randomId(size);
|
|
17
52
|
}
|
package/src/parseResponse.js
CHANGED
|
@@ -19,7 +19,7 @@ export default async function parseResponse(out, ctx) {
|
|
|
19
19
|
|
|
20
20
|
// A plain string will be converted to either html or plain
|
|
21
21
|
if (typeof out === "string") {
|
|
22
|
-
const type =
|
|
22
|
+
const type = /^\s*\</.test(out) ? "text/html" : "text/plain";
|
|
23
23
|
out = new Response(out, { headers: { "content-type": type } });
|
|
24
24
|
}
|
|
25
25
|
|
package/src/reply.js
CHANGED
|
@@ -60,6 +60,10 @@ Reply.prototype.json = function (body) {
|
|
|
60
60
|
}).send(JSON.stringify(body));
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
Reply.prototype.redirect = function (Location) {
|
|
64
|
+
return this.headers({ Location }).status(302).send();
|
|
65
|
+
};
|
|
66
|
+
|
|
63
67
|
Reply.prototype.file = async function (path) {
|
|
64
68
|
try {
|
|
65
69
|
const data = await fs.readFile(path);
|
|
@@ -133,4 +137,5 @@ export const cookies = (...args) => new Reply().cookies(...args);
|
|
|
133
137
|
export const send = (...args) => new Reply().send(...args);
|
|
134
138
|
export const json = (...args) => new Reply().json(...args);
|
|
135
139
|
export const file = (...args) => new Reply().file(...args);
|
|
140
|
+
export const redirect = (...args) => new Reply().redirect(...args);
|
|
136
141
|
export const view = (...args) => new Reply().view(...args);
|