Skip to content

Commit 55f5dc4

Browse files
authored
Merge pull request #1 from shgysk8zer0/setup
setup
2 parents 329b34b + ad467b3 commit 55f5dc4

File tree

19 files changed

+720
-23
lines changed

19 files changed

+720
-23
lines changed

.eslintrc.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,7 @@
3030
"no-async-promise-executor": 0,
3131
"no-prototype-builtins": 0
3232
},
33-
"globals": {}
33+
"globals": {
34+
"globalThis": "readonly"
35+
}
3436
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
*.cjs
22
*.min.*
33
*.map
4+
.env
45
# Logs
56
logs
67
*.log

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ node_modules/
77
.gitattributes
88
.gitignore
99
.nvmrc
10+
.env
1011
importmap.json
1112
importmap.yaml
1213
*.config.js

README.md

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,91 @@
1-
# npm-template
2-
A template repo for npm packages
1+
# `@shgysk8zer0/http`
2+
A JavaScript library that provides various utilities for working with HTTP
3+
4+
[![CodeQL](https://github.com/shgysk8zer0/node-http/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/shgysk8zer0/http/actions/workflows/codeql-analysis.yml)
5+
![Node CI](https://github.com/shgysk8zer0/node-http/workflows/Node%20CI/badge.svg)
6+
![Lint Code Base](https://github.com/shgysk8zer0/node-http/workflows/Lint%20Code%20Base/badge.svg)
7+
8+
[![GitHub license](https://img.shields.io/github/license/shgysk8zer0/node-http.svg)](https://github.com/shgysk8zer0/node-http/blob/master/LICENSE)
9+
[![GitHub last commit](https://img.shields.io/github/last-commit/shgysk8zer0/node-http.svg)](https://github.com/shgysk8zer0/node-http/commits/master)
10+
[![GitHub release](https://img.shields.io/github/release/shgysk8zer0/node-http?logo=github)](https://github.com/shgysk8zer0/node-http/releases)
11+
[![GitHub Sponsors](https://img.shields.io/github/sponsors/shgysk8zer0?logo=github)](https://github.com/sponsors/shgysk8zer0)
12+
13+
[![npm](https://img.shields.io/npm/v/@shgysk8zer0/http)](https://www.npmjs.com/package/@shgysk8zer0/http)
14+
![node-current](https://img.shields.io/node/v/@shgysk8zer0/http)
15+
![npm bundle size gzipped](https://img.shields.io/bundlephobia/minzip/@shgysk8zer0/http)
16+
[![npm](https://img.shields.io/npm/dw/@shgysk8zer0/http?logo=npm)](https://www.npmjs.com/package/@shgysk8zer0/http)
17+
18+
[![GitHub followers](https://img.shields.io/github/followers/shgysk8zer0.svg?style=social)](https://github.com/shgysk8zer0)
19+
![GitHub forks](https://img.shields.io/github/forks/shgysk8zer0/node-http.svg?style=social)
20+
![GitHub stars](https://img.shields.io/github/stars/shgysk8zer0/node-http.svg?style=social)
21+
[![Twitter Follow](https://img.shields.io/twitter/follow/shgysk8zer0.svg?style=social)](https://twitter.com/shgysk8zer0)
22+
23+
[![Donate using Liberapay](https://img.shields.io/liberapay/receives/shgysk8zer0.svg?logo=liberapay)](https://liberapay.com/shgysk8zer0/donate "Donate using Liberapay")
24+
- - -
25+
26+
- [Code of Conduct](./.github/CODE_OF_CONDUCT.md)
27+
- [Contributing](./.github/CONTRIBUTING.md)
28+
<!-- - [Security Policy](./.github/SECURITY.md) -->
29+
30+
# Key Features
31+
32+
- Exported constants for common HTTP status codes, such as `ok` for 200.
33+
- An extended `HTTPError` class that inherits from Error.
34+
- Form data parsing that mirrors browser behavior, allowing `formData.get('file')` to return a File object.
35+
- Useful polyfills, including an extended `File` object (derived from `Blob`) and `URL.canParse()` for URL validation.
36+
- A set of constants for commonly used Content-Types.
37+
- A versatile `openLink()` function compatible with various JavaScript environments.
38+
- A `Cookie` class for working with HTTP cookies, enabling easy cookie creation and management.
39+
40+
## Installation
41+
42+
### NPM Installation
43+
44+
```bash
45+
npm i @shgysk8zer0/http
46+
```
47+
48+
### NPM Imports
49+
```js
50+
import { HTTPError } from 'shgysk8zer0/http@shgysk8zer0/http/error.js';
51+
import { NOT_IMPLEMENTED } from 'shgysk8zer0/http@shgysk8zer0/http/status.js';
52+
import { JSON } from 'shgysk8zer0/http@shgysk8zer0/http/types.js';
53+
import { Cookie } from 'shgysk8zer0/http@shgysk8zer0/http/cookie.js';
54+
```
55+
56+
### Alternative `import`s
57+
58+
This package is available on [unpkg.com](https://unpkg.com/browse/@shgys8zer0/http/) as a collection of modules, making it easily accessible for browser-based projects.
59+
It is designed to be versatile and is not limited to a specific Node.js environment, ensuring compatibility across various platforms.
60+
61+
```js
62+
import { HTTPError } from 'https://unpkg.com/@shgysk8zer0/http/error.js';
63+
import { NOT_IMPLEMENTED } from 'https://unpkg.com/@shgysk8zer0/http/status.js';
64+
import { JSON } from 'https://unpkg.com/@shgysk8zer0/http/types.js';
65+
import { Cookie } from 'https://unpkg.com/@shgysk8zer0/http/cookie.js';
66+
```
67+
68+
### Example Code
69+
70+
export async function handler() {
71+
const error = new HTTPError('Not implemented.', {
72+
status: NOT_IMPLEMENTED,
73+
cause: new Error('I have not done this yet...'),
74+
});
75+
76+
return new Response([error], {
77+
status: error.status,
78+
headers: new Headers({
79+
'Content-Type': JSON,
80+
'Set-Cookie': new Cookie('uid', crypto.randomUUID(), {
81+
domain: 'example.com',
82+
path: '/foo',
83+
maxAge: 86_400_000,
84+
sameSite: 'Strict',
85+
httpOnly: true,
86+
partitioned: true,
87+
}
88+
}),
89+
});
90+
}
91+
```

cookie.js

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/**
2+
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
3+
*/
4+
export class Cookie {
5+
#name;
6+
#value;
7+
#domain;
8+
#path;
9+
#expires;
10+
#maxAge;
11+
#sameSite;
12+
#httpOnly;
13+
#secure;
14+
#partitioned;
15+
16+
constructor(name, value, {
17+
domain,
18+
path = '/',
19+
expires,
20+
maxAge,
21+
sameSite = 'Lax',
22+
httpOnly = false,
23+
secure = false,
24+
partitioned = false,
25+
} = {}) {
26+
this.name = name;
27+
this.value = value;
28+
this.path = path;
29+
this.sameSite = sameSite;
30+
this.httpOnly = httpOnly;
31+
this.secure = secure;
32+
this.partitioned = partitioned;
33+
34+
if (typeof domain !== 'undefined') {
35+
this.domain = domain;
36+
}
37+
38+
if (typeof expires !== 'undefined') {
39+
this.expires = expires;
40+
}
41+
42+
if (typeof maxAge !== 'undefined') {
43+
this.maxAge = maxAge;
44+
}
45+
}
46+
47+
get name() {
48+
return this.#name;
49+
}
50+
51+
set name(val) {
52+
if (typeof val !== 'string' || val.legnth === 0) {
53+
throw new TypeError('Cookie name must be a non-empty string.');
54+
} else {
55+
this.#name = val;
56+
}
57+
}
58+
59+
set value(val) {
60+
if (typeof val === 'undefined' || val === null) {
61+
this.#value = '';
62+
} else {
63+
this.#value = val.toString();
64+
}
65+
}
66+
67+
get maxAge() {
68+
return this.#maxAge;
69+
}
70+
71+
set maxAge(val) {
72+
if (typeof val !== 'number' || ! Number.isSafeInteger(val)) {
73+
throw new TypeError('Max-Age must be an integer.');
74+
} else {
75+
this.#maxAge = val;
76+
}
77+
}
78+
79+
get path() {
80+
return this.#path;
81+
}
82+
83+
set path(val) {
84+
if (val instanceof URL) {
85+
this.#path = val.pathname;
86+
} else if (typeof val === 'string') {
87+
if (new URL(val, 'https://example.com').pathname !== val) {
88+
throw new Error(`Invalid path: ${val}`);
89+
} else {
90+
console.log(`Set #path to ${val}`);
91+
this.#path = val;
92+
}
93+
} else {
94+
throw new TypeError('Path must be a string or URL.');
95+
}
96+
}
97+
98+
get domain() {
99+
return this.#domain;
100+
}
101+
102+
set domain(val) {
103+
if (val instanceof URL) {
104+
this.#domain = val.hostname;
105+
} else if (typeof val === 'string') {
106+
if (new URL(`https://${val}`).hostname === val) {
107+
this.#domain = val;
108+
} else {
109+
throw new Error(`Invalid domain: ${val}`);
110+
}
111+
} else {
112+
throw new TypeError('Domain must be a URL or string.');
113+
}
114+
}
115+
116+
get expires() {
117+
return this.#expires;
118+
}
119+
120+
set expires(val) {
121+
if (val instanceof Date) {
122+
this.#expires = val;
123+
} else if (typeof val === 'string' || typeof val === 'number') {
124+
this.expires = new Date(val);
125+
}
126+
}
127+
128+
get httpOnly() {
129+
return this.#httpOnly;
130+
}
131+
132+
set httpOnly(val) {
133+
if (typeof val !== 'boolean') {
134+
throw new TypeError('HttpOnly must be a boolean.');
135+
} else {
136+
this.#httpOnly = val;
137+
}
138+
}
139+
140+
get secure() {
141+
return this.#secure;
142+
}
143+
144+
set secure(val) {
145+
if (typeof val !== 'boolean') {
146+
throw new TypeError('Secure must be a boolean.');
147+
} else {
148+
this.#secure = val;
149+
}
150+
}
151+
152+
get partitioned() {
153+
return this.#partitioned;
154+
}
155+
156+
set partitioned(val) {
157+
if (typeof val !== 'boolean') {
158+
throw new TypeError('Partitioned must be a boolean.');
159+
} else {
160+
this.#partitioned = val;
161+
}
162+
}
163+
164+
get sameSite() {
165+
return this.#sameSite;
166+
}
167+
168+
set sameSite(val) {
169+
if (! ['Strict', 'Lax', 'None'].includes(val)) {
170+
throw new TypeError('SameSite must be a "Strict", "Lax", or "None".');
171+
} else {
172+
this.#sameSite = val;
173+
}
174+
}
175+
176+
toString() {
177+
let cookie = `${encodeURIComponent(this.#name)}=${encodeURIComponent(this.#value)}`;
178+
179+
const flags = {
180+
Domain: this.#domain,
181+
Path: this.#path,
182+
Expires: this.#expires,
183+
'Max-Age': this.#maxAge,
184+
SameSite: this.#sameSite,
185+
HttpOnly: this.#httpOnly,
186+
Secure: this.#secure,
187+
Partitioned: this.#partitioned,
188+
};
189+
190+
return (cookie + '; ' + Object.entries(flags).reduce((flags, [key, val]) => {
191+
if (typeof val === 'string') {
192+
flags.push(key === 'Path' ? `${key}=${val}` : `${key}=${encodeURIComponent(val)}`);
193+
} else if (typeof val === 'number' && ! Number.isNaN(val)) {
194+
flags.push(`${key}=${val}`);
195+
} else if (typeof val === 'boolean') {
196+
if (val) {
197+
flags.push(key);
198+
}
199+
} else if (val instanceof Date) {
200+
flags.push(`${key}=${val.toGMTString()}`);
201+
}
202+
203+
return flags;
204+
}, []).join('; ')).trim();
205+
}
206+
}

0 commit comments

Comments
 (0)