@server/next 0.20.8 → 0.20.10

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 CHANGED
@@ -15,6 +15,7 @@ type ServerOptions = {
15
15
  views?: string | Bucket;
16
16
  public?: string | Bucket;
17
17
  uploads?: string | Bucket;
18
+ auth?: string | { type: string; provider: string };
18
19
  };
19
20
 
20
21
  type Context = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@server/next",
3
- "version": "0.20.8",
3
+ "version": "0.20.10",
4
4
  "description": "An experimental reimplementation of server.js focused on the DX",
5
5
  "homepage": "https://node-server.com/",
6
6
  "repository": "https://github.com/franciscop/server-next.git",
@@ -9,7 +9,6 @@
9
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
10
10
  "license": "UNLICENSED",
11
11
  "scripts": {
12
- "demo": "nodemon ./demo/src/index.js",
13
12
  "start": "bun test --watch",
14
13
  "test": "bun test",
15
14
  "test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
package/readme.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  > **⚠️ WIP** This is an **experimental library** right now!
4
4
 
5
- [**Documentation Here**](https://node-server.com/documentation/)
6
-
7
5
  A fully-fledged web server for Bun and Node.js, with all the basics covered for you:
8
6
 
9
7
  ```js
@@ -26,11 +24,11 @@ It includes all the things you would expect from a modern Server framework, like
26
24
  ```js
27
25
  // Easy testing as well - index.test.js
28
26
  import app from "./";
27
+ const api = app.test(); // Very convenient helper, AXIOS-like interface
29
28
 
30
29
  it("can retrieve the homepage", async () => {
31
- const res = await app.fetch(new Request("http://localhost:3000/books/"));
32
- const books = await res.json();
33
- expect(books[0]).toEqual({ id: 0, name: 'The Catcher In The Rye', ... });
30
+ const { data: books } = await api.get("/books/");
31
+ expect(books[0]).toEqual({ id: 0, name: ... });
34
32
  });
35
33
  ```
36
34
 
@@ -56,6 +54,8 @@ Why? We live in the era of multi-cloud (Heroku, Workers, Lambda, etc) and multi-
56
54
  - A readStream and it'll be piped to the response
57
55
  - An object with `status`, `body` and `headers` and it'll be set raw.
58
56
  - Response compression works
57
+ - Zod light integration
58
+ - Auth work
59
59
 
60
60
  ## Examples
61
61
 
@@ -0,0 +1,18 @@
1
+ import { ServerError } from "../index.js";
2
+
3
+ class NoSession {}
4
+
5
+ export default new Proxy(NoSession, {
6
+ get(target, key) {
7
+ if (target[key]) return target[key];
8
+ if (key === "then") return target[key];
9
+ throw ServerError.NO_STORE_READ({ key });
10
+ },
11
+ set(target, key, value) {
12
+ if (target[key] || key === "then") {
13
+ target[key] = value;
14
+ } else {
15
+ throw ServerError.NO_STORE_WRITE({ key });
16
+ }
17
+ },
18
+ });
@@ -0,0 +1,41 @@
1
+ import "../test/toSucceed.js";
2
+
3
+ import kv from "polystore";
4
+
5
+ import server from "../index.js";
6
+
7
+ describe("user creation flow", () => {
8
+ // These are obviously mock data
9
+ const EMAIL = "abc@test.com";
10
+ const PASS = "11111111";
11
+ const CREDENTIALS = { email: EMAIL, password: PASS };
12
+
13
+ const store = kv(new Map());
14
+ const api = server({ store, auth: "cookie:email" })
15
+ .get("/me", (ctx) => ctx.user || "No data")
16
+ .test();
17
+
18
+ it("can create a new user", async () => {
19
+ const register = await api.post("/auth/register/email", CREDENTIALS);
20
+ expect(register).toSucceed();
21
+ expect(await store.keys()).toEqual([
22
+ "user:abc@test.com",
23
+ "auth:" + register.headers["set-cookie"].split("=")[1],
24
+ ]);
25
+
26
+ const me = await api.get("/me");
27
+ expect(me).toSucceed();
28
+ expect(me.data.email).toEqual(EMAIL);
29
+
30
+ const logout = await api.post("/auth/logout");
31
+ expect(logout).toSucceed();
32
+ expect(await store.keys()).toEqual(["user:abc@test.com"]);
33
+
34
+ const login = await api.post("/auth/login/email", CREDENTIALS);
35
+ expect(login).toSucceed();
36
+ expect(await store.keys()).toEqual([
37
+ "user:abc@test.com",
38
+ "auth:" + login.headers["set-cookie"].split("=")[1],
39
+ ]);
40
+ });
41
+ });
@@ -0,0 +1,42 @@
1
+ import "../test/toSucceed.js";
2
+
3
+ import kv from "polystore";
4
+
5
+ import server from "../index.js";
6
+
7
+ describe("user creation flow", () => {
8
+ // These are obviously mock data
9
+ const EMAIL = "abc@test.com";
10
+ const PASS = "11111111";
11
+ const CREDENTIALS = { email: EMAIL, password: PASS };
12
+
13
+ const store = kv(new Map());
14
+ const api = server({ store, auth: "token:email" })
15
+ .get("/me", (ctx) => ctx.user || "No data")
16
+ .test();
17
+
18
+ it("can create a new user", async () => {
19
+ const register = await api.post("/auth/register/email", CREDENTIALS);
20
+ expect(register).toSucceed();
21
+ expect(await store.keys()).toEqual([
22
+ "user:abc@test.com",
23
+ "auth:" + register.data.token,
24
+ ]);
25
+
26
+ const headers = { authorization: "Bearer " + register.data.token };
27
+ const me = await api.get("/me", { headers });
28
+ expect(me).toSucceed();
29
+ expect(me.data.email).toEqual(EMAIL);
30
+
31
+ const logout = await api.post("/auth/logout", {}, { headers });
32
+ expect(logout).toSucceed();
33
+ expect(await store.keys()).toEqual(["user:abc@test.com"]);
34
+
35
+ const login = await api.post("/auth/login/email", CREDENTIALS);
36
+ expect(login).toSucceed();
37
+ expect(await store.keys()).toEqual([
38
+ "user:abc@test.com",
39
+ "auth:" + login.data.token,
40
+ ]);
41
+ });
42
+ });
@@ -0,0 +1,52 @@
1
+ import { ServerError } from "../index.js";
2
+
3
+ const validateToken = (authorization) => {
4
+ const [type, id] = authorization.trim().split(" ");
5
+ if (type.toLowerCase() !== "bearer") {
6
+ throw ServerError.AUTH_INVALID_TYPE({ type });
7
+ }
8
+ if (id.length !== 24) {
9
+ throw ServerError.AUTH_INVALID_TOKEN();
10
+ }
11
+ return id;
12
+ };
13
+
14
+ const validateCookie = (authorization) => {
15
+ if (authorization.length !== 24) {
16
+ throw ServerError.AUTH_INVALID_COOKIE();
17
+ }
18
+ return authorization;
19
+ };
20
+
21
+ const findSessionId = (ctx) => {
22
+ const type = ctx.options.auth.type;
23
+
24
+ if (type === "token") {
25
+ // If the user is not authenticated, there's no auth to retrieve
26
+ if (!ctx.headers.authorization) return;
27
+
28
+ // Check the authentication header
29
+ return validateToken(ctx.headers.authorization);
30
+ }
31
+
32
+ if (type === "cookie") {
33
+ // If the user is not authenticated, there's no auth to retrieve
34
+ if (!ctx.cookies.authorization) return;
35
+
36
+ return validateCookie(ctx.cookies.authorization);
37
+ }
38
+
39
+ throw new Error("Invalid auth type " + type);
40
+ };
41
+
42
+ export default async function findAuth(ctx) {
43
+ if (!ctx.options.auth) return; // NO AUTH AT ALL; nothing to do here
44
+ const options = ctx.options.auth;
45
+
46
+ const sessionId = findSessionId(ctx);
47
+ if (!sessionId) return; // NO SESSION FOUND; no auth
48
+
49
+ const auth = await options.session.get(sessionId);
50
+ if (!auth) throw ServerError.AUTH_NO_SESSION();
51
+ return auth;
52
+ }
@@ -0,0 +1,24 @@
1
+ import auth from "./auth.js";
2
+ import logout from "./logout.js";
3
+ import providers from "./providers/index.js";
4
+ import session from "./session.js";
5
+ import user from "./user.js";
6
+
7
+ const load = async (ctx) => {
8
+ ctx.session = await session(ctx);
9
+ ctx.auth = await auth(ctx);
10
+ ctx.user = await user(ctx);
11
+ };
12
+
13
+ const middle = async (ctx) => {
14
+ if (ctx.options.auth) {
15
+ ctx.app.post("/auth/logout", logout);
16
+
17
+ if (ctx.options.auth.provider.includes("email")) {
18
+ ctx.app.post("/auth/register/email", providers.email.register);
19
+ ctx.app.post("/auth/login/email", providers.email.login);
20
+ }
21
+ }
22
+ };
23
+
24
+ export default { load, middle };
@@ -0,0 +1,123 @@
1
+ import "../test/toSucceed.js";
2
+
3
+ import kv from "polystore";
4
+
5
+ import server from "../index.js";
6
+
7
+ const ID = "REqA2l022l8Q0tuIRtqLOPUy";
8
+
9
+ describe("auth", () => {
10
+ it("requires a provider", () => {
11
+ const store = kv(new Map());
12
+ const api = server({ store, auth: "token:email" })
13
+ .get("/", (ctx) => ctx.auth)
14
+ .test();
15
+ });
16
+
17
+ it("provider must belong", async () => {
18
+ const store = kv(new Map());
19
+ const api = server({ store, auth: "token:email" })
20
+ .get("/", (ctx) => ctx.auth)
21
+ .test();
22
+
23
+ store.set(`auth:${ID}`, {
24
+ id: ID,
25
+ type: "token",
26
+ provider: "wrong",
27
+ user: "QypOn5SQApyOPdUpIZsO9u2O",
28
+ email: "abc@test.com",
29
+ time: "2024-07-01T03:21:40Z",
30
+ });
31
+ const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
32
+ const req = await api.get("/", { headers: { authorization } });
33
+ expect(req).not.toSucceed(
34
+ 'Invalid provider "wrong", valid ones are: "email"'
35
+ );
36
+ });
37
+ });
38
+
39
+ describe("token", () => {
40
+ const store = kv(new Map());
41
+ const api = server({ store, auth: "token:email" })
42
+ .get("/", (ctx) => ctx.auth)
43
+ .test();
44
+
45
+ afterEach(async () => {
46
+ await store.del(`auth:${ID}`);
47
+ });
48
+
49
+ it("should be Bearer", async () => {
50
+ const authorization = "Basic REqA2l022l8Q0tuIRtqLOPUy";
51
+ const req = await api.get("/", { headers: { authorization } });
52
+ expect(req).not.toSucceed("Invalid Authorization type, 'Basic'");
53
+ });
54
+
55
+ it("should have the proper token", async () => {
56
+ const authorization = "Bearer hola";
57
+ const req = await api.get("/", { headers: { authorization } });
58
+ expect(req).not.toSucceed("Invalid Authorization token");
59
+ });
60
+
61
+ it("cannot get the session", async () => {
62
+ const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
63
+ const req = await api.get("/", { headers: { authorization } });
64
+ expect(req).not.toSucceed("Invalid session");
65
+ });
66
+
67
+ it("cannot get the user", async () => {
68
+ store.set(`auth:${ID}`, {
69
+ id: ID,
70
+ type: "token",
71
+ provider: "email",
72
+ user: "QypOn5SQApyOPdUpIZsO9u2O",
73
+ email: "abc@test.com",
74
+ time: "2024-07-01T03:21:40Z",
75
+ });
76
+ const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
77
+ const req = await api.get("/", { headers: { authorization } });
78
+ expect(req).not.toSucceed("Credentials do not correspond to a user");
79
+ });
80
+
81
+ it("cannot get the user", async () => {
82
+ store.set("auth:REqA2l022l8Q0tuIRtqLOPUy", {
83
+ id: "REqA2l022l8Q0tuIRtqLOPUy",
84
+ type: "token",
85
+ provider: "email",
86
+ user: "QypOn5SQApyOPdUpIZsO9u2O",
87
+ email: "abc@test.com",
88
+ time: "2024-07-01T03:21:40Z",
89
+ });
90
+ store.set("user:abc@test.com", {
91
+ id: "QypOn5SQApyOPdUpIZsO9u2O",
92
+ email: "abc@test.com",
93
+ });
94
+ const authorization = "Bearer REqA2l022l8Q0tuIRtqLOPUy";
95
+ const req = await api.get("/", { headers: { authorization } });
96
+ expect(req).toSucceed();
97
+ });
98
+ });
99
+
100
+ describe("cookie", () => {
101
+ const store = kv(new Map());
102
+ const api = server({ store, auth: "cookie:email" })
103
+ .get("/", (ctx) => ctx.auth)
104
+ .test();
105
+
106
+ it("should have the proper token in email", async () => {
107
+ const cookie = "authorization=hello";
108
+ const req = await api.get("/", { headers: { cookie } });
109
+ expect(req).not.toSucceed("Invalid Authorization cookie");
110
+ });
111
+
112
+ it("can get the proper session", async () => {
113
+ const cookie = "authorization=REqA2l022l8Q0tuIRtqLOPUy";
114
+ const req = await api.get("/", { headers: { cookie } });
115
+ expect(req).not.toSucceed("Invalid session");
116
+ });
117
+
118
+ it("can get the proper session", async () => {
119
+ const cookie = "authorization=REqA2l022l8Q0tuIRtqLOPUy";
120
+ const req = await api.get("/", { headers: { cookie } });
121
+ expect(req).not.toSucceed();
122
+ });
123
+ });
@@ -0,0 +1,18 @@
1
+ import { cookies } from "../reply.js";
2
+
3
+ export default async function logout(ctx) {
4
+ const { id, type } = ctx.auth;
5
+ await ctx.options.auth.session.del(id);
6
+
7
+ if (type === "token") {
8
+ return { token: null };
9
+ } else if (type === "cookie") {
10
+ return cookies({ authorization: null }).send({});
11
+ } else if (type === "jwt") {
12
+ throw new Error("JWT auth not supported yet");
13
+ } else if (type === "key") {
14
+ throw new Error("Key auth not supported yet");
15
+ } else {
16
+ throw new Error("Unknown auth type");
17
+ }
18
+ }
@@ -0,0 +1,73 @@
1
+ import argon2 from "argon2";
2
+
3
+ import { createId } from "../../helpers/index.js";
4
+ import { ServerError, status } from "../../index.js";
5
+
6
+ const createSession = async (user, ctx) => {
7
+ const { type, session, cleanUser } = ctx.options.auth;
8
+ user = cleanUser(user);
9
+ const token = createId();
10
+ const provider = "email";
11
+ const time = new Date().toISOString().replace(/\.[0-9]*/, "");
12
+ ctx.auth = {
13
+ id: token,
14
+ type,
15
+ provider,
16
+ user: user.id,
17
+ email: user.email,
18
+ time,
19
+ };
20
+ await session.set(token, ctx.auth, { expires: "1w" });
21
+
22
+ if (type === "token") {
23
+ return status(201).json({ ...user, token });
24
+ } else if (type === "cookie") {
25
+ return status(201).cookies({ authorization: token }).send(user);
26
+ } else if (type === "jwt") {
27
+ throw new Error("JWT auth not supported yet");
28
+ } else if (type === "key") {
29
+ throw new Error("Key auth not supported yet");
30
+ } else {
31
+ throw new Error("Unknown auth type");
32
+ }
33
+ };
34
+
35
+ async function login(ctx) {
36
+ const { email, password } = ctx.body;
37
+ if (!email) throw ServerError.LOGIN_NO_EMAIL();
38
+ if (!/@/.test(email)) throw ServerError.LOGIN_INVALID_EMAIL();
39
+ if (!password) throw ServerError.LOGIN_NO_PASSWORD();
40
+ if (password.length < 8) throw ServerError.LOGIN_INVALID_PASSWORD();
41
+
42
+ const store = ctx.options.auth.store;
43
+ if (!(await store.has(email))) throw ServerError.LOGIN_WRONG_EMAIL();
44
+
45
+ const user = await store.get(email);
46
+ const isValid = await argon2.verify(user.password, password);
47
+ if (!isValid) throw ServerError.LOGIN_WRONG_PASSWORD();
48
+
49
+ return createSession(user, ctx);
50
+ }
51
+
52
+ async function register(ctx) {
53
+ const { email, password, ...data } = ctx.body;
54
+ if (!email) throw ServerError.REGISTER_NO_EMAIL();
55
+ if (!/@/.test(email)) throw ServerError.REGISTER_INVALID_EMAIL();
56
+ if (!password) throw ServerError.REGISTER_NO_PASSWORD();
57
+ if (password.length < 8) throw ServerError.REGISTER_INVALID_PASSWORD();
58
+
59
+ const store = ctx.options.auth.store;
60
+ if (await store.has(email)) throw ServerError.REGISTER_EMAIL_EXISTS();
61
+
62
+ const user = {
63
+ id: createId(),
64
+ email,
65
+ password: await argon2.hash(password),
66
+ ...data,
67
+ };
68
+ await store.set(email, user);
69
+
70
+ return createSession(user, ctx);
71
+ }
72
+
73
+ export default { login, register };
@@ -0,0 +1,3 @@
1
+ import { default as email } from "./email.js";
2
+
3
+ export default { email };
@@ -0,0 +1,18 @@
1
+ import NoSession from "./NoSession.js";
2
+
3
+ export default async function session(ctx) {
4
+ const store = ctx.options.session?.store;
5
+
6
+ // If there's no store at all, we don't have session available;
7
+ // but that's okay, since it's only a problem if you try to use it
8
+ if (!store) return NoSession;
9
+
10
+ // There's a session cookie; use it as the key to get the data
11
+ // from the store
12
+ if (ctx.cookies.session) {
13
+ const session = await store.get(ctx.cookies.session);
14
+ if (session) return session;
15
+ }
16
+
17
+ return {};
18
+ }
@@ -0,0 +1,21 @@
1
+ import ServerError from "../ServerError";
2
+
3
+ const findUser = async (auth, options) => {
4
+ if (!auth.provider) throw ServerError.AUTH_NO_PROVIDER();
5
+ if (!options.provider.includes(auth.provider)) {
6
+ const valid = JSON.stringify(options.provider);
7
+ throw ServerError.AUTH_INVALID_PROVIDER({ provider: auth.provider, valid });
8
+ }
9
+
10
+ if (auth.provider === "email") {
11
+ return await options.store.get(auth.email);
12
+ }
13
+ };
14
+
15
+ export default async function user(ctx) {
16
+ if (!ctx.auth) return;
17
+
18
+ const user = await findUser(ctx.auth, ctx.options.auth);
19
+ if (!user) throw ServerError.AUTH_NO_USER();
20
+ return ctx.options.auth.cleanUser(user);
21
+ }
@@ -1,9 +1,14 @@
1
+ import auth from "../auth/index.js";
1
2
  import { define } from "../helpers/index.js";
2
- import findAuth from "./findAuth.js";
3
- import findSession from "./findSession.js";
4
3
  import parseBody from "./parseBody.js";
5
4
  import parseCookies from "./parseCookies.js";
6
5
 
6
+ // https://stackoverflow.com/a/54029307/938236
7
+ const chunkArray = (arr, size) =>
8
+ arr.length > size
9
+ ? [arr.slice(0, size), ...chunkArray(arr.slice(size), size)]
10
+ : [arr];
11
+
7
12
  export default async (request, options = {}, app) => {
8
13
  const ctx = {};
9
14
  ctx.options = options;
@@ -11,10 +16,9 @@ export default async (request, options = {}, app) => {
11
16
  ctx.res = { status: null, headers: {}, cookies: {} };
12
17
  ctx.method = request.method.toLowerCase();
13
18
 
14
- ctx.headers = request.headers;
19
+ ctx.headers = parseHeaders(new Headers(chunkArray(request.rawHeaders, 2)));
15
20
  ctx.cookies = parseCookies(ctx.headers.cookie);
16
- ctx.session = await findSession(ctx);
17
- await findAuth(ctx);
21
+ await auth.load(ctx);
18
22
 
19
23
  const https = request.connection.encrypted ? "https" : "http";
20
24
  const host = ctx.headers.host || "localhost" + options.port;
@@ -1,6 +1,5 @@
1
- import { define } from "../helpers/index.js";
2
- import findAuth from "./findAuth.js";
3
- import findSession from "./findSession.js";
1
+ import auth from "../auth/index.js";
2
+ import { define, parseHeaders } from "../helpers/index.js";
4
3
  import parseBody from "./parseBody.js";
5
4
  import parseCookies from "./parseCookies.js";
6
5
 
@@ -11,10 +10,9 @@ export default async (request, options = {}, app) => {
11
10
  ctx.res = { status: null, headers: {}, cookies: {} };
12
11
  ctx.method = request.method.toLowerCase();
13
12
 
14
- ctx.headers = Object.fromEntries(request.headers.entries());
13
+ ctx.headers = parseHeaders(request.headers);
15
14
  ctx.cookies = parseCookies(ctx.headers.cookie);
16
- ctx.session = await findSession(ctx);
17
- await findAuth(ctx);
15
+ await auth.load(ctx);
18
16
 
19
17
  ctx.url = new URL(request.url.replace(/\/$/, ""));
20
18
  define(ctx.url, "query", (url) =>
@@ -6,6 +6,14 @@ ServerError.extend({
6
6
  NO_STORE_READ: `You need a 'store' to read 'ctx.session.{key}'`,
7
7
  AUTH_INVALID_TYPE: `Invalid Authorization type, '{type}'`,
8
8
  AUTH_INVALID_TOKEN: `Invalid Authorization token`,
9
+ AUTH_INVALID_COOKIE: `Invalid Authorization cookie`,
10
+ AUTH_NO_PROVIDER: `No provider passed to the option "auth.provider"`,
11
+ AUTH_INVALID_PROVIDER: `Invalid provider "{provider}", valid ones are: {valid}`,
12
+ AUTH_NO_SESSION: { status: 401, message: "Invalid session" },
13
+ AUTH_NO_USER: {
14
+ status: 401,
15
+ message: "Credentials do not correspond to a user",
16
+ },
9
17
 
10
18
  LOGIN_NO_EMAIL: "The email is required to log in",
11
19
  LOGIN_INVALID_EMAIL: "The email you wrote is not correct",
@@ -13,6 +21,12 @@ ServerError.extend({
13
21
  LOGIN_INVALID_PASSWORD: "The email you wrote is not correct",
14
22
  LOGIN_WRONG_ACCOUNT: `That email does not correspond to any account`,
15
23
  LOGIN_WRONG_PASSWORD: `That is not the valid password`,
24
+
25
+ REGISTER_NO_EMAIL: `Email needed`,
26
+ REGISTER_INVALID_EMAIL: "The email you wrote is not correct",
27
+ REGISTER_NO_PASSWORD: `Password needed`,
28
+ REGISTER_INVALID_PASSWORD: "The email you wrote is not correct",
29
+ REGISTER_EMAIL_EXISTS: `Email is already registered`,
16
30
  });
17
31
 
18
32
  export default ServerError;
@@ -0,0 +1,23 @@
1
+ import server, { cookies } from "../index.js";
2
+
3
+ describe("set-cookie", () => {
4
+ const app = server()
5
+ .get("/hello", () => {
6
+ return cookies({ hello: "world" }).send();
7
+ })
8
+ .get("/multiple", () => {
9
+ return cookies({ a: "b", c: "d" }).send();
10
+ });
11
+
12
+ it("sets the right cookie", async () => {
13
+ const api = app.test();
14
+ const res = await api.get("/hello");
15
+ expect(res.headers["set-cookie"]).toBe("hello=world");
16
+ });
17
+
18
+ it("can set multiple cookies", async () => {
19
+ const api = app.test();
20
+ const res = await api.get("/multiple");
21
+ expect(res.headers["set-cookie"]).toEqual(["a=b", "c=d"]);
22
+ });
23
+ });
@@ -1,13 +1,16 @@
1
1
  // Takes an object and returns a string with the proper cookie values
2
2
  export default function createCookies(cookies) {
3
- if (!cookies || !Object.keys(cookies).length) return "";
4
- return Object.entries(cookies)
5
- .map(([key, val]) => {
6
- if (typeof val === "string") {
7
- val = { value: val, path: "/" };
8
- }
9
- const { value, path } = val;
10
- return `${key}=${value};Path=${path}`;
11
- })
12
- .join(";");
3
+ if (!cookies || !Object.keys(cookies).length) return [];
4
+ return Object.entries(cookies).map(([key, val]) => {
5
+ if (!val) {
6
+ val = { value: "", expires: new Date(0).toUTCString() };
7
+ }
8
+ if (typeof val === "string") {
9
+ val = { value: val };
10
+ }
11
+ const { value, path, expires } = val;
12
+ const pathPart = path ? ";Path=" + path : "";
13
+ const expiresPart = expires ? ";Expires=" + expires : "";
14
+ return `${key}=${value || ""}${pathPart}${expiresPart}`;
15
+ });
13
16
  }
@@ -4,4 +4,5 @@ export { default as define } from "./define.js";
4
4
  export { default as getMachine } from "./getMachine.js";
5
5
  export { default as handleRequest } from "./handleRequest.js";
6
6
  export { default as iterate } from "./iterate.js";
7
+ export { default as parseHeaders } from "./parseHeaders.js";
7
8
  export { default as types } from "./types.js";
@@ -0,0 +1,15 @@
1
+ export default (raw) => {
2
+ const headers = {};
3
+ for (let [key, value] of raw.entries()) {
4
+ key = key.toLowerCase();
5
+ if (headers[key]) {
6
+ if (!Array.isArray(headers[key])) {
7
+ headers[key] = [headers[key]];
8
+ }
9
+ headers[key].push(value);
10
+ } else {
11
+ headers[key] = value;
12
+ }
13
+ }
14
+ return headers;
15
+ };