Skip to content

Commit efc35c9

Browse files
v1rtlv1rtl
authored andcommitted
create cookie and cookie-signature modules, improve readmes
1 parent dcc5c9b commit efc35c9

17 files changed

Lines changed: 507 additions & 157 deletions

File tree

examples/basic/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ app.all('/page/:page/', (req, res) => {
1616
`)
1717
})
1818

19-
2019
app.use(staticFolder())
2120

2221
app.use(logger())

packages/app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"author": "v1rtl",
2828
"license": "MIT",
2929
"dependencies": {
30-
"@tinyhttp/etag": "workspace:*",
30+
"@tinyhttp/cookie": "^0.0.1",
31+
"@tinyhttp/etag": "^0.1.15",
3132
"content-type": "^1.0.4",
3233
"proxy-addr": "^2.0.6",
3334
"range-parser": "^1.2.1",

packages/app/pnpm-lock.yaml

Lines changed: 0 additions & 82 deletions
This file was deleted.

packages/app/src/classes/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const compileTrust = (val: any) => {
4343

4444
if (val === true) {
4545
// Support plain true/false
46-
return function () {
46+
return function() {
4747
return true
4848
}
4949
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# @tinyhttp/cookie-signature
2+
3+
[![npm (scoped)](https://img.shields.io/npm/v/@tinyhttp/cookie-signature?style=flat-square)](npmjs.com/package/@tinyhttp/cookie-signature) [![npm](https://img.shields.io/npm/dt/@tinyhttp/cookie-signature?style=flat-square)](npmjs.com/package/@tinyhttp/cookie-signature)
4+
5+
HTTP cookie signing and unsigning. A rewrite of [cookie-signature](https://github.com/tj/node-cookie-signature) module.
6+
7+
## Installation
8+
9+
```sh
10+
npm install @tinyhttp/cookie-signature
11+
```
12+
13+
## API
14+
15+
```js
16+
import { sign, unsign } from '@tinyhttp/cookie-signature'
17+
```
18+
19+
### `sign(val, secret)`
20+
21+
Signd the given `val` with `secret`.
22+
23+
### `unsign(val, secret)
24+
25+
Unsign and decode the given `val` with `secret`, returning `false` if the signature is invalid.
26+
27+
## License
28+
29+
MIT
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@tinyhttp/cookie-signature",
3+
"version": "0.0.1",
4+
"description": "HTTP cookie signing and unsigning",
5+
"homepage": "https://github.com/talentlessguy/tinyhttp",
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/talentlessguy/tinyhttp.git",
12+
"directory": "packages/cookie-signature"
13+
},
14+
"main": "dist/index.js",
15+
"types": "dist/index.d.ts",
16+
"module": "dist/index.esm.js",
17+
"keywords": [
18+
"tinyhttp",
19+
"node.js",
20+
"web framework",
21+
"web",
22+
"backend",
23+
"static",
24+
"cookie"
25+
],
26+
"author": "v1rtl",
27+
"license": "MIT"
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createHmac, timingSafeEqual } from 'crypto'
2+
3+
/**
4+
* Sign the given `val` with `secret`.
5+
*/
6+
export const sign = (val: string, secret: string) => {
7+
return `${val}.${createHmac('sha256', secret)
8+
.update(val)
9+
.digest('base64')
10+
.replace(/\=+$/, '')}`
11+
}
12+
13+
/**
14+
* Unsign and decode the given `val` with `secret`,
15+
* returning `false` if the signature is invalid.
16+
*/
17+
export const unsign = (val: string, secret: string) => {
18+
const str = val.slice(0, val.lastIndexOf('.')),
19+
mac = sign(str, secret),
20+
macBuffer = Buffer.from(mac),
21+
valBuffer = Buffer.alloc(macBuffer.length)
22+
23+
valBuffer.write(val)
24+
return timingSafeEqual(macBuffer, valBuffer) ? str : false
25+
}

packages/cookie/README.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# @tinyhttp/cookie
2+
3+
[![npm (scoped)](https://img.shields.io/npm/v/@tinyhttp/cookie?style=flat-square)](npmjs.com/package/@tinyhttp/cookie) [![npm](https://img.shields.io/npm/dt/@tinyhttp/cookie?style=flat-square)](npmjs.com/package/@tinyhttp/cookie)
4+
5+
HTTP cookie parser and serializer for Node.js. A rewrite of [cookie](https://github.com/jshttp/cookie) module.
6+
7+
## Installation
8+
9+
```sh
10+
npm install @tinyhttp/cookie
11+
```
12+
13+
## API
14+
15+
```js
16+
import { parse, serialize } from '@tinyhttp/cookie'
17+
```
18+
19+
### `parse(str, options)`
20+
21+
Parse an HTTP `Cookie` header string and returning an object of all cookie name-value pairs.
22+
The `str` argument is the string representing a `Cookie` header value and `options` is an
23+
optional object containing additional parsing options.
24+
25+
```js
26+
import { parse } from '@tinyhttp/cookie'
27+
28+
const cookies = parse('foo=bar; equation=E%3Dmc%5E2')
29+
// { foo: 'bar', equation: 'E=mc^2' }
30+
```
31+
32+
#### Options
33+
34+
`parse` accepts these properties in the options object.
35+
36+
##### `decode`
37+
38+
Specifies a function that will be used to decode a cookie's value. Since the value of a cookie
39+
has a limited character set (and must be a simple string), this function can be used to decode
40+
a previously-encoded cookie value into a JavaScript string or other object.
41+
42+
The default function is the global `decodeURIComponent`, which will decode any URL-encoded
43+
sequences into their byte representations.
44+
45+
**note** if an error is thrown from this function, the original, non-decoded cookie value will
46+
be returned as the cookie's value.
47+
48+
### `serialize(name, value, options)`
49+
50+
Serialize a cookie name-value pair into a `Set-Cookie` header string. The `name` argument is the
51+
name for the cookie, the `value` argument is the value to set the cookie to, and the `options`
52+
argument is an optional object containing additional serialization options.
53+
54+
```js
55+
import { serialize } from '@tinyhttp/cookie'
56+
57+
const setCookie = serialize('foo', 'bar')
58+
// foo=bar
59+
```
60+
61+
#### Options
62+
63+
`serialize` accepts these properties in the options object.
64+
65+
##### `domain`
66+
67+
Specifies the value for the [`Domain` `Set-Cookie` attribute][rfc-6265-5.2.3]. By default, no
68+
domain is set, and most clients will consider the cookie to apply to only the current domain.
69+
70+
##### `encode`
71+
72+
Specifies a function that will be used to encode a cookie's value. Since value of a cookie
73+
has a limited character set (and must be a simple string), this function can be used to encode
74+
a value into a string suited for a cookie's value.
75+
76+
The default function is the global `encodeURIComponent`, which will encode a JavaScript string
77+
into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range.
78+
79+
##### `expires`
80+
81+
Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute][rfc-6265-5.2.1].
82+
By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and
83+
will delete it on a condition like exiting a web browser application.
84+
85+
**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and
86+
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
87+
so if both are set, they should point to the same date and time.
88+
89+
##### `httpOnly`
90+
91+
Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute][rfc-6265-5.2.6]. When truthy,
92+
the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set.
93+
94+
**note** be careful when setting this to `true`, as compliant clients will not allow client-side
95+
JavaScript to see the cookie in `document.cookie`.
96+
97+
##### `maxAge`
98+
99+
Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute][rfc-6265-5.2.2].
100+
The given number will be converted to an integer by rounding down. By default, no maximum age is set.
101+
102+
**note** the [cookie storage model specification][rfc-6265-5.3] states that if both `expires` and
103+
`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
104+
so if both are set, they should point to the same date and time.
105+
106+
##### `path`
107+
108+
Specifies the value for the [`Path` `Set-Cookie` attribute][rfc-6265-5.2.4]. By default, the path
109+
is considered the ["default path"][rfc-6265-5.1.4].
110+
111+
##### `sameSite`
112+
113+
Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute][rfc-6265bis-03-4.1.2.7].
114+
115+
- `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
116+
- `false` will not set the `SameSite` attribute.
117+
- `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
118+
- `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
119+
- `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
120+
121+
More information about the different enforcement levels can be found in
122+
[the specification][rfc-6265bis-03-4.1.2.7].
123+
124+
**note** This is an attribute that has not yet been fully standardized, and may change in the future.
125+
This also means many clients may ignore this attribute until they understand it.
126+
127+
##### `secure`
128+
129+
Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute][rfc-6265-5.2.5]. When truthy,
130+
the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
131+
132+
**note** be careful when setting this to `true`, as compliant clients will not send the cookie back to
133+
the server in the future if the browser does not have an HTTPS connection.
134+
135+
## Example
136+
137+
```ts
138+
import { App } from '@tinyhttp/app'
139+
import cookie from '@tinyhttp/cookie'
140+
import escapeHtml from 'escape-html'
141+
142+
const app = new App()
143+
144+
app.use((req, res) => {
145+
if (req.query?.name) {
146+
// Set a new cookie with the name
147+
res.setHeader(
148+
'Set-Cookie',
149+
cookie.serialize('name', String(query.name), {
150+
httpOnly: true,
151+
maxAge: 60 * 60 * 24 * 7 // 1 week
152+
})
153+
)
154+
155+
// Redirect back after setting cookie
156+
res
157+
.status(302)
158+
.setHeader('Location', req.headers.referer || '/')
159+
.end()
160+
}
161+
162+
const cookie = cookie.parse(req.headers.cookie || '')
163+
164+
const { name } = cookie
165+
166+
res.setHeader('Content-Type', 'text/html; charset=UTF-8')
167+
168+
if (name) {
169+
res.write(`<p>Welcome back, <strong>${escapeHtml(name)}</strong>!</p>`)
170+
} else {
171+
res.write('<p>Hello, new visitor!</p>')
172+
}
173+
res.write('<form method="GET">')
174+
res.write('<input placeholder="enter your name" name="name"><input type="submit" value="Set Name">')
175+
res.end('</form>')
176+
})
177+
178+
app.listen(3000)
179+
```
180+
181+
## License
182+
183+
MIT

0 commit comments

Comments
 (0)