@atproto/oauth-provider 0.19.6 → 0.19.8
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/CHANGELOG.md +33 -0
- package/dist/errors/error-parser.js +5 -5
- package/dist/errors/error-parser.js.map +1 -1
- package/package.json +22 -18
- package/src/access-token/access-token-mode.ts +0 -4
- package/src/account/account-manager.ts +0 -591
- package/src/account/account-store.ts +0 -305
- package/src/account/sign-in-data.ts +0 -15
- package/src/account/sign-up-input.ts +0 -20
- package/src/client/client-auth.ts +0 -64
- package/src/client/client-data.ts +0 -9
- package/src/client/client-id.ts +0 -4
- package/src/client/client-info.ts +0 -13
- package/src/client/client-manager.ts +0 -772
- package/src/client/client-store.ts +0 -35
- package/src/client/client-utils.ts +0 -37
- package/src/client/client.ts +0 -394
- package/src/constants.ts +0 -80
- package/src/customization/branding.ts +0 -12
- package/src/customization/build-customization-css.ts +0 -55
- package/src/customization/build-customization-data.ts +0 -24
- package/src/customization/colors.ts +0 -48
- package/src/customization/customization.ts +0 -29
- package/src/customization/links.ts +0 -10
- package/src/device/device-data.ts +0 -11
- package/src/device/device-id.ts +0 -27
- package/src/device/device-manager.ts +0 -292
- package/src/device/device-store.ts +0 -31
- package/src/device/session-id.ts +0 -21
- package/src/dpop/dpop-manager.ts +0 -214
- package/src/dpop/dpop-nonce.ts +0 -121
- package/src/dpop/dpop-proof.ts +0 -6
- package/src/errors/access-denied-error.ts +0 -12
- package/src/errors/account-selection-required-error.ts +0 -12
- package/src/errors/authorization-error.ts +0 -45
- package/src/errors/consent-required-error.ts +0 -12
- package/src/errors/error-parser.ts +0 -147
- package/src/errors/handle-unavailable-error.ts +0 -23
- package/src/errors/invalid-authorization-details-error.ts +0 -27
- package/src/errors/invalid-client-error.ts +0 -20
- package/src/errors/invalid-client-id-error.ts +0 -31
- package/src/errors/invalid-client-metadata-error.ts +0 -68
- package/src/errors/invalid-credentials-error.ts +0 -29
- package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
- package/src/errors/invalid-dpop-proof-error.ts +0 -13
- package/src/errors/invalid-grant-error.ts +0 -21
- package/src/errors/invalid-invite-code-error.ts +0 -10
- package/src/errors/invalid-redirect-uri-error.ts +0 -17
- package/src/errors/invalid-request-error.ts +0 -32
- package/src/errors/invalid-scope-error.ts +0 -15
- package/src/errors/invalid-token-error.ts +0 -60
- package/src/errors/login-required-error.ts +0 -12
- package/src/errors/oauth-error.ts +0 -28
- package/src/errors/second-authentication-factor-required-error.ts +0 -25
- package/src/errors/unauthorized-client-error.ts +0 -20
- package/src/errors/use-dpop-nonce-error.ts +0 -32
- package/src/errors/www-authenticate-error.ts +0 -64
- package/src/index.ts +0 -16
- package/src/lexicon/lexicon-data.ts +0 -11
- package/src/lexicon/lexicon-getter.ts +0 -55
- package/src/lexicon/lexicon-manager.ts +0 -116
- package/src/lexicon/lexicon-store.ts +0 -35
- package/src/lib/csp/index.ts +0 -101
- package/src/lib/hcaptcha.ts +0 -228
- package/src/lib/html/README.md +0 -9
- package/src/lib/html/build-document.ts +0 -151
- package/src/lib/html/escapers.ts +0 -66
- package/src/lib/html/html.ts +0 -56
- package/src/lib/html/hydration-data.ts +0 -19
- package/src/lib/html/index.ts +0 -5
- package/src/lib/html/tags.ts +0 -67
- package/src/lib/html/util.ts +0 -17
- package/src/lib/http/README.md +0 -11
- package/src/lib/http/accept.ts +0 -90
- package/src/lib/http/context.ts +0 -42
- package/src/lib/http/headers.ts +0 -15
- package/src/lib/http/index.ts +0 -10
- package/src/lib/http/method.ts +0 -18
- package/src/lib/http/middleware.ts +0 -169
- package/src/lib/http/parser.ts +0 -101
- package/src/lib/http/path.ts +0 -82
- package/src/lib/http/request.ts +0 -256
- package/src/lib/http/response.ts +0 -122
- package/src/lib/http/route.ts +0 -60
- package/src/lib/http/router.ts +0 -117
- package/src/lib/http/security-headers.ts +0 -100
- package/src/lib/http/stream.ts +0 -57
- package/src/lib/http/types.ts +0 -16
- package/src/lib/http/url.ts +0 -23
- package/src/lib/nsid.ts +0 -10
- package/src/lib/redis.ts +0 -25
- package/src/lib/util/authorization-header.ts +0 -28
- package/src/lib/util/cast.ts +0 -18
- package/src/lib/util/color.ts +0 -168
- package/src/lib/util/crypto.ts +0 -32
- package/src/lib/util/date.ts +0 -7
- package/src/lib/util/error.ts +0 -7
- package/src/lib/util/function.ts +0 -39
- package/src/lib/util/locale.ts +0 -12
- package/src/lib/util/object.ts +0 -23
- package/src/lib/util/redirect-uri.ts +0 -46
- package/src/lib/util/time.ts +0 -49
- package/src/lib/util/type.ts +0 -182
- package/src/lib/util/ui8.ts +0 -14
- package/src/lib/util/well-known.ts +0 -8
- package/src/lib/util/zod-error.ts +0 -26
- package/src/lib/write-form-redirect.ts +0 -58
- package/src/lib/write-html.ts +0 -70
- package/src/metadata/build-metadata.ts +0 -141
- package/src/oauth-client.ts +0 -3
- package/src/oauth-dpop.ts +0 -2
- package/src/oauth-errors.ts +0 -26
- package/src/oauth-hooks.ts +0 -519
- package/src/oauth-middleware.ts +0 -53
- package/src/oauth-provider.ts +0 -1110
- package/src/oauth-store.ts +0 -12
- package/src/oauth-verifier.ts +0 -260
- package/src/replay/replay-manager.ts +0 -51
- package/src/replay/replay-store-memory.ts +0 -36
- package/src/replay/replay-store-redis.ts +0 -30
- package/src/replay/replay-store.ts +0 -44
- package/src/request/code.ts +0 -23
- package/src/request/request-data.ts +0 -36
- package/src/request/request-id.ts +0 -22
- package/src/request/request-manager.ts +0 -521
- package/src/request/request-store.ts +0 -60
- package/src/request/request-uri.ts +0 -42
- package/src/result/authorization-redirect-parameters.ts +0 -24
- package/src/result/authorization-result-authorize-page.ts +0 -16
- package/src/result/authorization-result-redirect.ts +0 -8
- package/src/router/assets/assets-manifest.ts +0 -117
- package/src/router/assets/assets.ts +0 -124
- package/src/router/assets/csrf.ts +0 -79
- package/src/router/assets/send-account-page.ts +0 -23
- package/src/router/assets/send-authorization-page.ts +0 -42
- package/src/router/assets/send-cookie-error-page.ts +0 -21
- package/src/router/assets/send-error-page.ts +0 -25
- package/src/router/assets/send-redirect.ts +0 -140
- package/src/router/create-account-page-middleware.ts +0 -88
- package/src/router/create-api-middleware.ts +0 -1051
- package/src/router/create-authorization-page-middleware.ts +0 -281
- package/src/router/create-oauth-middleware.ts +0 -257
- package/src/router/error-handler.ts +0 -6
- package/src/router/middleware-options.ts +0 -9
- package/src/signer/access-token-payload.ts +0 -25
- package/src/signer/api-token-payload.ts +0 -18
- package/src/signer/signer.ts +0 -121
- package/src/token/refresh-token.ts +0 -30
- package/src/token/token-claims.ts +0 -22
- package/src/token/token-data.ts +0 -40
- package/src/token/token-id.ts +0 -25
- package/src/token/token-manager.ts +0 -427
- package/src/token/token-store.ts +0 -88
- package/src/types/authorization-response-error.ts +0 -27
- package/src/types/color-hue.ts +0 -3
- package/src/types/email-otp.ts +0 -3
- package/src/types/email.ts +0 -26
- package/src/types/handle.ts +0 -23
- package/src/types/invite-code.ts +0 -4
- package/src/types/par-response-error.ts +0 -25
- package/src/types/password.ts +0 -4
- package/src/types/rgb-color.ts +0 -21
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
package/src/lib/http/request.ts
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
// eslint-disable-next-line import/default, import/no-named-as-default-member
|
|
3
|
-
import accept from '@hapi/accept'
|
|
4
|
-
// eslint-disable-next-line import/no-named-as-default-member
|
|
5
|
-
const { languages, mediaType } = accept
|
|
6
|
-
import {
|
|
7
|
-
CookieSerializeOptions,
|
|
8
|
-
parse as parseCookie,
|
|
9
|
-
serialize as serializeCookie,
|
|
10
|
-
} from 'cookie'
|
|
11
|
-
import forwarded from 'forwarded'
|
|
12
|
-
import createHttpError from 'http-errors'
|
|
13
|
-
import { appendHeader } from './headers.js'
|
|
14
|
-
import { UrlReference, urlMatch } from './url.js'
|
|
15
|
-
|
|
16
|
-
export function validateHeaderValue(
|
|
17
|
-
req: IncomingMessage,
|
|
18
|
-
name: keyof IncomingMessage['headers'],
|
|
19
|
-
allowedValues: readonly (string | null)[],
|
|
20
|
-
) {
|
|
21
|
-
const value = req.headers[name] ?? null
|
|
22
|
-
|
|
23
|
-
if (Array.isArray(value)) {
|
|
24
|
-
throw createHttpError(400, `Invalid ${name} header`)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!allowedValues.includes(value)) {
|
|
28
|
-
throw createHttpError(
|
|
29
|
-
400,
|
|
30
|
-
value
|
|
31
|
-
? `Forbidden ${name} header "${value}" (expected ${allowedValues})`
|
|
32
|
-
: `Missing ${name} header`,
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function validateFetchMode(
|
|
38
|
-
req: IncomingMessage,
|
|
39
|
-
expectedMode: readonly (
|
|
40
|
-
| null
|
|
41
|
-
| 'navigate'
|
|
42
|
-
| 'same-origin'
|
|
43
|
-
| 'no-cors'
|
|
44
|
-
| 'cors'
|
|
45
|
-
)[],
|
|
46
|
-
) {
|
|
47
|
-
validateHeaderValue(req, 'sec-fetch-mode', expectedMode)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function validateFetchDest(
|
|
51
|
-
req: IncomingMessage,
|
|
52
|
-
expectedDest: readonly (
|
|
53
|
-
| null
|
|
54
|
-
| 'document'
|
|
55
|
-
| 'embed'
|
|
56
|
-
| 'font'
|
|
57
|
-
| 'image'
|
|
58
|
-
| 'manifest'
|
|
59
|
-
| 'media'
|
|
60
|
-
| 'object'
|
|
61
|
-
| 'report'
|
|
62
|
-
| 'script'
|
|
63
|
-
| 'serviceworker'
|
|
64
|
-
| 'sharedworker'
|
|
65
|
-
| 'style'
|
|
66
|
-
| 'worker'
|
|
67
|
-
| 'xslt'
|
|
68
|
-
)[],
|
|
69
|
-
) {
|
|
70
|
-
validateHeaderValue(req, 'sec-fetch-dest', expectedDest)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function validateFetchSite(
|
|
74
|
-
req: IncomingMessage,
|
|
75
|
-
expectedSite: readonly (
|
|
76
|
-
| null
|
|
77
|
-
| 'same-origin'
|
|
78
|
-
| 'same-site'
|
|
79
|
-
| 'cross-site'
|
|
80
|
-
| 'none'
|
|
81
|
-
)[],
|
|
82
|
-
) {
|
|
83
|
-
validateHeaderValue(req, 'sec-fetch-site', expectedSite)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function validateReferrer(
|
|
87
|
-
req: IncomingMessage,
|
|
88
|
-
reference: UrlReference,
|
|
89
|
-
allowNull: true,
|
|
90
|
-
): URL | null
|
|
91
|
-
export function validateReferrer(
|
|
92
|
-
req: IncomingMessage,
|
|
93
|
-
reference: UrlReference,
|
|
94
|
-
allowNull?: false,
|
|
95
|
-
): URL
|
|
96
|
-
export function validateReferrer(
|
|
97
|
-
req: IncomingMessage,
|
|
98
|
-
reference: UrlReference,
|
|
99
|
-
allowNull = false,
|
|
100
|
-
) {
|
|
101
|
-
// @NOTE The header name "referer" is actually a misspelling of the word
|
|
102
|
-
// "referrer". https://en.wikipedia.org/wiki/HTTP_referer
|
|
103
|
-
const referrer = req.headers['referer']
|
|
104
|
-
const referrerUrl = referrer ? new URL(referrer) : null
|
|
105
|
-
if (referrerUrl ? !urlMatch(referrerUrl, reference) : !allowNull) {
|
|
106
|
-
throw createHttpError(400, `Invalid referrer ${referrer}`)
|
|
107
|
-
}
|
|
108
|
-
return referrerUrl
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function validateOrigin(
|
|
112
|
-
req: IncomingMessage,
|
|
113
|
-
expectedOrigin: string,
|
|
114
|
-
optional = true,
|
|
115
|
-
) {
|
|
116
|
-
const reqOrigin = req.headers['origin']
|
|
117
|
-
if (reqOrigin ? reqOrigin !== expectedOrigin : !optional) {
|
|
118
|
-
throw createHttpError(400, `Invalid origin ${reqOrigin}`)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export type { CookieSerializeOptions }
|
|
123
|
-
|
|
124
|
-
export function setCookie(
|
|
125
|
-
res: ServerResponse,
|
|
126
|
-
cookieName: string,
|
|
127
|
-
value: string,
|
|
128
|
-
options?: CookieSerializeOptions,
|
|
129
|
-
) {
|
|
130
|
-
appendHeader(res, 'Set-Cookie', serializeCookie(cookieName, value, options))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function getCookie(
|
|
134
|
-
req: IncomingMessage,
|
|
135
|
-
cookieName: string,
|
|
136
|
-
): string | undefined {
|
|
137
|
-
const cookies = parseHttpCookies(req)
|
|
138
|
-
return Object.hasOwn(cookies, cookieName) ? cookies[cookieName] : undefined
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function clearCookie(
|
|
142
|
-
res: ServerResponse,
|
|
143
|
-
cookieName: string,
|
|
144
|
-
options?: Omit<CookieSerializeOptions, 'maxAge' | 'expires'>,
|
|
145
|
-
) {
|
|
146
|
-
setCookie(res, cookieName, '', { ...options, maxAge: 0 })
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function parseHttpCookies(
|
|
150
|
-
req: IncomingMessage & { cookies?: any },
|
|
151
|
-
): Record<string, undefined | string> {
|
|
152
|
-
req.cookies ??= req.headers['cookie']
|
|
153
|
-
? { __proto__: null, ...parseCookie(req.headers['cookie']) }
|
|
154
|
-
: { __proto__: null }
|
|
155
|
-
return req.cookies
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export type ExtractRequestMetadataOptions = {
|
|
159
|
-
/**
|
|
160
|
-
* A function that determines whether a given IP address is trusted. The
|
|
161
|
-
* function is called with the IP addresses and its index in the list of
|
|
162
|
-
* forwarded addresses (starting from 0, 0 corresponding to the ip of the
|
|
163
|
-
* incoming HTTP connection, and the last item being the first proxied IP
|
|
164
|
-
* address in the proxy chain, deduced from the `X-Forwarded-For` header). The
|
|
165
|
-
* function should return `true` if the IP address is trusted, and `false`
|
|
166
|
-
* otherwise.
|
|
167
|
-
*
|
|
168
|
-
* @see {@link https://www.npmjs.com/package/proxy-addr} for a utility that
|
|
169
|
-
* allows you to create a trust function.
|
|
170
|
-
*/
|
|
171
|
-
trustProxy?: (addr: string, i: number) => boolean
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export type RequestMetadata = {
|
|
175
|
-
userAgent?: string
|
|
176
|
-
ipAddress: string
|
|
177
|
-
port: number
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function extractRequestMetadata(
|
|
181
|
-
req: IncomingMessage,
|
|
182
|
-
options?: ExtractRequestMetadataOptions,
|
|
183
|
-
): RequestMetadata {
|
|
184
|
-
const ip = extractIp(req, options)
|
|
185
|
-
return {
|
|
186
|
-
userAgent: req.headers['user-agent'],
|
|
187
|
-
ipAddress: ip,
|
|
188
|
-
port: extractPort(req, ip),
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function extractIp(
|
|
193
|
-
req: IncomingMessage,
|
|
194
|
-
options?: ExtractRequestMetadataOptions,
|
|
195
|
-
): string {
|
|
196
|
-
const trust = options?.trustProxy
|
|
197
|
-
if (trust) {
|
|
198
|
-
const ips = forwarded(req)
|
|
199
|
-
for (let i = 0; i < ips.length; i++) {
|
|
200
|
-
const isTrusted = trust(ips[i], i)
|
|
201
|
-
if (!isTrusted) return ips[i]
|
|
202
|
-
}
|
|
203
|
-
// Let's return the last ("furthest") IP address in the chain if all of them
|
|
204
|
-
// are trusted. Note that this may indicate an issue with either the trust
|
|
205
|
-
// function (too permissive), or the proxy configuration (one of them not
|
|
206
|
-
// setting the X-Forwarded-For header).
|
|
207
|
-
const ip = ips[ips.length - 1]
|
|
208
|
-
if (ip) return ip
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Express app compatibility (see "trust proxy" setting)
|
|
212
|
-
if ('ip' in req) {
|
|
213
|
-
const ip = req.ip
|
|
214
|
-
if (typeof ip === 'string') return ip
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const ip = req.socket.remoteAddress
|
|
218
|
-
if (ip) return ip
|
|
219
|
-
|
|
220
|
-
throw new Error('Could not determine IP address')
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function extractPort(req: IncomingMessage, ip: string): number {
|
|
224
|
-
if (ip !== req.socket.remoteAddress) {
|
|
225
|
-
// Trust the X-Forwarded-Port header only if the IP address was a trusted
|
|
226
|
-
// proxied IP.
|
|
227
|
-
const forwardedPort = req.headers['x-forwarded-port']
|
|
228
|
-
if (typeof forwardedPort === 'string') {
|
|
229
|
-
const port = Number(forwardedPort.trim())
|
|
230
|
-
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
231
|
-
throw new Error('Invalid forwarded port')
|
|
232
|
-
}
|
|
233
|
-
return port
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const port = req.socket.remotePort
|
|
238
|
-
if (port != null) return port
|
|
239
|
-
|
|
240
|
-
throw new Error('Could not determine port')
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function extractLocales(req: IncomingMessage) {
|
|
244
|
-
const acceptLanguage = req.headers['accept-language']
|
|
245
|
-
return acceptLanguage ? languages(acceptLanguage) : []
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function negotiateResponseContent<T extends string>(
|
|
249
|
-
req: IncomingMessage,
|
|
250
|
-
types: readonly T[],
|
|
251
|
-
): T | undefined {
|
|
252
|
-
const type = mediaType(req.headers['accept'], types)
|
|
253
|
-
if (type) return type as T
|
|
254
|
-
|
|
255
|
-
return undefined
|
|
256
|
-
}
|
package/src/lib/http/response.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { type Readable, pipeline } from 'node:stream'
|
|
3
|
-
import createHttpError from 'http-errors'
|
|
4
|
-
import { Awaitable } from '../util/type.js'
|
|
5
|
-
import { negotiateResponseContent } from './request.js'
|
|
6
|
-
import type { Handler, Middleware } from './types.js'
|
|
7
|
-
|
|
8
|
-
export function writeRedirect(
|
|
9
|
-
res: ServerResponse,
|
|
10
|
-
url: string,
|
|
11
|
-
status = 302,
|
|
12
|
-
): void {
|
|
13
|
-
res.writeHead(status, { Location: url }).end()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export type WriteResponseOptions = {
|
|
17
|
-
status?: number
|
|
18
|
-
contentType?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function writeStream(
|
|
22
|
-
res: ServerResponse,
|
|
23
|
-
stream: Readable,
|
|
24
|
-
{
|
|
25
|
-
status = 200,
|
|
26
|
-
contentType = 'application/octet-stream',
|
|
27
|
-
}: WriteResponseOptions = {},
|
|
28
|
-
): void {
|
|
29
|
-
res.statusCode = status
|
|
30
|
-
res.setHeader('content-type', contentType)
|
|
31
|
-
|
|
32
|
-
if (res.req.method === 'HEAD') {
|
|
33
|
-
res.end()
|
|
34
|
-
stream.destroy()
|
|
35
|
-
} else {
|
|
36
|
-
pipeline([stream, res], (_err: Error | null) => {
|
|
37
|
-
// The error will be propagated through the streams
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function writeBuffer(
|
|
43
|
-
res: ServerResponse,
|
|
44
|
-
chunk: string | Buffer,
|
|
45
|
-
opts: WriteResponseOptions,
|
|
46
|
-
): void {
|
|
47
|
-
if (opts?.status != null) res.statusCode = opts.status
|
|
48
|
-
res.setHeader('content-type', opts?.contentType || 'application/octet-stream')
|
|
49
|
-
res.end(chunk)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function toJsonBuffer(value: unknown): Buffer {
|
|
53
|
-
try {
|
|
54
|
-
return Buffer.from(JSON.stringify(value))
|
|
55
|
-
} catch (cause) {
|
|
56
|
-
throw new Error(`Failed to serialize as JSON`, { cause })
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function writeJson(
|
|
61
|
-
res: ServerResponse,
|
|
62
|
-
payload: unknown,
|
|
63
|
-
{ contentType = 'application/json', ...options }: WriteResponseOptions = {},
|
|
64
|
-
): void {
|
|
65
|
-
const buffer = toJsonBuffer(payload)
|
|
66
|
-
writeBuffer(res, buffer, { ...options, contentType })
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function staticJsonMiddleware(
|
|
70
|
-
value: unknown,
|
|
71
|
-
{ contentType = 'application/json', ...options }: WriteResponseOptions = {},
|
|
72
|
-
): Handler<unknown> {
|
|
73
|
-
const buffer = toJsonBuffer(value)
|
|
74
|
-
const staticOptions: WriteResponseOptions = { ...options, contentType }
|
|
75
|
-
return function (req, res) {
|
|
76
|
-
writeBuffer(res, buffer, staticOptions)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function cacheControlMiddleware(maxAge: number): Middleware<void> {
|
|
81
|
-
const header = `max-age=${maxAge}`
|
|
82
|
-
return function (req, res, next) {
|
|
83
|
-
res.setHeader('Cache-Control', header)
|
|
84
|
-
next()
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export type JsonResponse<P = unknown> = WriteResponseOptions & {
|
|
89
|
-
json: P
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function jsonHandler<
|
|
93
|
-
T,
|
|
94
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
95
|
-
Res extends ServerResponse = ServerResponse,
|
|
96
|
-
>(
|
|
97
|
-
buildJson: (this: T, req: Req, res: Res) => Awaitable<JsonResponse>,
|
|
98
|
-
): Middleware<T, Req, Res> {
|
|
99
|
-
return function (req, res, next) {
|
|
100
|
-
// Ensure we can agree on a content encoding & type before starting to
|
|
101
|
-
// build the JSON response.
|
|
102
|
-
if (negotiateResponseContent(req, ['application/json'])) {
|
|
103
|
-
// A middleware should not be async, so we wrap the async operation in a
|
|
104
|
-
// promise and return it.
|
|
105
|
-
void (async () => {
|
|
106
|
-
try {
|
|
107
|
-
const jsonResponse = await buildJson.call(this, req, res)
|
|
108
|
-
const { json, status = 200, ...options } = jsonResponse
|
|
109
|
-
writeJson(res, json, { ...options, status })
|
|
110
|
-
} catch (err) {
|
|
111
|
-
next(asError(err, 'Failed to build JSON response'))
|
|
112
|
-
}
|
|
113
|
-
})()
|
|
114
|
-
} else {
|
|
115
|
-
next(createHttpError(406, 'Unsupported media type'))
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function asError(cause: unknown, message: string): Error {
|
|
121
|
-
return cause instanceof Error ? cause : new Error(message, { cause })
|
|
122
|
-
}
|
package/src/lib/http/route.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { SubCtx, subCtx } from './context.js'
|
|
3
|
-
import { MethodMatcherInput, createMethodMatcher } from './method.js'
|
|
4
|
-
import { combineMiddlewares } from './middleware.js'
|
|
5
|
-
import { Params, Path, createPathMatcher } from './path.js'
|
|
6
|
-
import { Middleware } from './types.js'
|
|
7
|
-
|
|
8
|
-
export type RouteCtx<
|
|
9
|
-
T extends object | void,
|
|
10
|
-
P extends Params = Params,
|
|
11
|
-
> = SubCtx<T, { params: Readonly<P> }>
|
|
12
|
-
export type RouteMiddleware<
|
|
13
|
-
T extends object | void,
|
|
14
|
-
P extends Params,
|
|
15
|
-
Req = IncomingMessage,
|
|
16
|
-
Res = ServerResponse,
|
|
17
|
-
> = Middleware<RouteCtx<T, P>, Req, Res>
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @example
|
|
21
|
-
* ```ts
|
|
22
|
-
* createRoute<{ foo: string }>('GET', '/foo/:foo', function (req, res) {
|
|
23
|
-
* console.log(this.params.foo) // OK
|
|
24
|
-
* console.log(this.params.bar) // Error
|
|
25
|
-
* })
|
|
26
|
-
*
|
|
27
|
-
* createRoute<{ foo: string }>(['POST', 'PUT'], '/foo/:foo', function (req, res) {
|
|
28
|
-
* console.log(this.params.foo) // OK
|
|
29
|
-
* console.log(this.params.bar) // Error
|
|
30
|
-
* })
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
export function createRoute<
|
|
34
|
-
P extends Params = Params,
|
|
35
|
-
T extends object | void = void,
|
|
36
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
37
|
-
Res extends ServerResponse = ServerResponse,
|
|
38
|
-
>(
|
|
39
|
-
method: MethodMatcherInput,
|
|
40
|
-
path: Path<P>,
|
|
41
|
-
...mw: RouteMiddleware<T, P, Req, Res>[]
|
|
42
|
-
): Middleware<T, Req, Res> {
|
|
43
|
-
const paramsMatcher = createPathMatcher<P>(path)
|
|
44
|
-
const methodMatcher = createMethodMatcher(method)
|
|
45
|
-
|
|
46
|
-
const middleware = combineMiddlewares(mw, { skipKeyword: 'route' })
|
|
47
|
-
|
|
48
|
-
return function (req, res, next) {
|
|
49
|
-
if (methodMatcher(req)) {
|
|
50
|
-
const pathname = req.url?.split('?')[0] ?? '/'
|
|
51
|
-
const params = paramsMatcher(pathname)
|
|
52
|
-
if (params) {
|
|
53
|
-
const context = subCtx(this, { params })
|
|
54
|
-
return middleware.call(context, req, res, next)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return next()
|
|
59
|
-
}
|
|
60
|
-
}
|
package/src/lib/http/router.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IncomingHttpHeaders,
|
|
3
|
-
IncomingMessage,
|
|
4
|
-
ServerResponse,
|
|
5
|
-
} from 'node:http'
|
|
6
|
-
import { SubCtx, subCtx } from './context.js'
|
|
7
|
-
import { MethodMatcherInput } from './method.js'
|
|
8
|
-
import { combineMiddlewares } from './middleware.js'
|
|
9
|
-
import { Params, Path } from './path.js'
|
|
10
|
-
import { RouteMiddleware, createRoute } from './route.js'
|
|
11
|
-
import { Middleware } from './types.js'
|
|
12
|
-
|
|
13
|
-
export type RouterCtx<T extends object | void = void> = SubCtx<
|
|
14
|
-
T,
|
|
15
|
-
{ url: Readonly<URL>; headers: IncomingHttpHeaders }
|
|
16
|
-
>
|
|
17
|
-
|
|
18
|
-
export type RouterMiddleware<
|
|
19
|
-
T extends object | void = void,
|
|
20
|
-
Req = IncomingMessage,
|
|
21
|
-
Res = ServerResponse,
|
|
22
|
-
> = Middleware<RouterCtx<T>, Req, Res>
|
|
23
|
-
|
|
24
|
-
export type RouterConfig = {
|
|
25
|
-
/** Used to build the origin of the {@link RouterCtx['url']} context property */
|
|
26
|
-
protocol?: string
|
|
27
|
-
/** Used to build the origin of the {@link RouterCtx['url']} context property */
|
|
28
|
-
host?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export class Router<
|
|
32
|
-
T extends object | void = void,
|
|
33
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
34
|
-
Res extends ServerResponse = ServerResponse,
|
|
35
|
-
> {
|
|
36
|
-
private readonly middlewares: RouterMiddleware<T, Req, Res>[] = []
|
|
37
|
-
|
|
38
|
-
constructor(private readonly config?: RouterConfig) {}
|
|
39
|
-
|
|
40
|
-
use(...middlewares: RouterMiddleware<T, Req, Res>[]) {
|
|
41
|
-
this.middlewares.push(...middlewares)
|
|
42
|
-
return this
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
all<P extends Params = Params>(
|
|
46
|
-
path: Path<P>,
|
|
47
|
-
...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]
|
|
48
|
-
) {
|
|
49
|
-
return this.addRoute<P>('*', path, ...mw)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
get<P extends Params = Params>(
|
|
53
|
-
path: Path<P>,
|
|
54
|
-
...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]
|
|
55
|
-
) {
|
|
56
|
-
return this.addRoute<P>('GET', path, ...mw)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
post<P extends Params = Params>(
|
|
60
|
-
path: Path<P>,
|
|
61
|
-
...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]
|
|
62
|
-
) {
|
|
63
|
-
return this.addRoute<P>('POST', path, ...mw)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
options<P extends Params = Params>(
|
|
67
|
-
path: Path<P>,
|
|
68
|
-
...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]
|
|
69
|
-
) {
|
|
70
|
-
return this.addRoute<P>('OPTIONS', path, ...mw)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
addRoute<P extends Params>(
|
|
74
|
-
method: MethodMatcherInput,
|
|
75
|
-
path: Path<P>,
|
|
76
|
-
...mw: RouteMiddleware<RouterCtx<T>, P, Req, Res>[]
|
|
77
|
-
) {
|
|
78
|
-
return this.use(createRoute(method, path, ...mw))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @returns router middleware which dispatches a route matching the request.
|
|
83
|
-
*/
|
|
84
|
-
buildMiddleware(): Middleware<T, Req, Res> {
|
|
85
|
-
const { config } = this
|
|
86
|
-
|
|
87
|
-
// Calling next('router') from a middleware will skip all the remaining
|
|
88
|
-
// middlewares in the stack.
|
|
89
|
-
const middleware = combineMiddlewares(this.middlewares, {
|
|
90
|
-
skipKeyword: 'router',
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
return function (this, req, res, next) {
|
|
94
|
-
// Parse the URL using node's URL parser.
|
|
95
|
-
const url = extractUrl(req, config)
|
|
96
|
-
if (url instanceof Error) return next(url)
|
|
97
|
-
|
|
98
|
-
// Any error thrown here will be uncaught/unhandled (a middleware should
|
|
99
|
-
// never throw)
|
|
100
|
-
const context = subCtx(this, { url, headers: req.headers })
|
|
101
|
-
middleware.call(context, req, res, next)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function extractUrl(req: IncomingMessage, config?: RouterConfig): URL | Error {
|
|
107
|
-
try {
|
|
108
|
-
const protocol = config?.protocol || 'https:'
|
|
109
|
-
const host = config?.host || req.headers.host || 'localhost'
|
|
110
|
-
const pathname = req.url || '/'
|
|
111
|
-
return new URL(pathname, `${protocol}//${host}`)
|
|
112
|
-
} catch (cause) {
|
|
113
|
-
const error =
|
|
114
|
-
cause instanceof Error ? cause : new Error('Invalid URL', { cause })
|
|
115
|
-
return Object.assign(error, { status: 400, statusCode: 400 })
|
|
116
|
-
}
|
|
117
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import type { ServerResponse } from 'node:http'
|
|
2
|
-
import { type CspConfig, buildCsp } from '../csp/index.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy COEP on MDN}
|
|
6
|
-
*/
|
|
7
|
-
export enum CrossOriginEmbedderPolicy {
|
|
8
|
-
unsafeNone = 'unsafe-none',
|
|
9
|
-
requireCorp = 'require-corp',
|
|
10
|
-
credentialless = 'credentialless',
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy CORP on MDN}
|
|
15
|
-
*/
|
|
16
|
-
export enum CrossOriginResourcePolicy {
|
|
17
|
-
sameSite = 'same-site',
|
|
18
|
-
sameOrigin = 'same-origin',
|
|
19
|
-
crossOrigin = 'cross-origin',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy COOP on MDN}
|
|
24
|
-
*/
|
|
25
|
-
export enum CrossOriginOpenerPolicy {
|
|
26
|
-
unsafeNone = 'unsafe-none',
|
|
27
|
-
sameOriginAllowPopups = 'same-origin-allow-popups',
|
|
28
|
-
sameOrigin = 'same-origin',
|
|
29
|
-
noopenerAllowPopups = 'noopener-allow-popups',
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type HTTPStrictTransportSecurityConfig = {
|
|
33
|
-
maxAge: number
|
|
34
|
-
includeSubDomains?: boolean
|
|
35
|
-
preload?: boolean
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type SecurityHeadersOptions = {
|
|
39
|
-
/**
|
|
40
|
-
* Defaults to `default-src: 'none'`. Use an empty object to disable CSP.
|
|
41
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy CSP on MDN}
|
|
42
|
-
*/
|
|
43
|
-
csp?: CspConfig
|
|
44
|
-
coep?: CrossOriginEmbedderPolicy
|
|
45
|
-
corp?: CrossOriginResourcePolicy
|
|
46
|
-
coop?: CrossOriginOpenerPolicy
|
|
47
|
-
/**
|
|
48
|
-
* Defaults to 2 years. Use `false` to disable HSTS.
|
|
49
|
-
*/
|
|
50
|
-
hsts?: HTTPStrictTransportSecurityConfig | false
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function* buildSecurityHeaders({
|
|
54
|
-
csp = { 'default-src': ["'none'"] },
|
|
55
|
-
coep = CrossOriginEmbedderPolicy.requireCorp,
|
|
56
|
-
corp = CrossOriginResourcePolicy.sameOrigin,
|
|
57
|
-
coop = CrossOriginOpenerPolicy.sameOrigin,
|
|
58
|
-
hsts = { maxAge: 63072000 },
|
|
59
|
-
}: SecurityHeadersOptions): Generator<[string, string], void, unknown> {
|
|
60
|
-
// @NOTE Never set CSP through http-equiv meta as not all directives will
|
|
61
|
-
// be honored. Always set it through the Content-Security-Policy header.
|
|
62
|
-
const cspString = buildCsp(csp)
|
|
63
|
-
if (cspString) {
|
|
64
|
-
yield ['Content-Security-Policy', cspString]
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
yield ['Cross-Origin-Embedder-Policy', coep]
|
|
68
|
-
yield ['Cross-Origin-Resource-Policy', corp]
|
|
69
|
-
yield ['Cross-Origin-Opener-Policy', coop]
|
|
70
|
-
|
|
71
|
-
if (hsts) {
|
|
72
|
-
yield ['Strict-Transport-Security', buildHstsValue(hsts)]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// @TODO make these headers configurable (?)
|
|
76
|
-
yield ['Permissions-Policy', 'otp-credentials=*, document-domain=()']
|
|
77
|
-
yield ['Referrer-Policy', 'same-origin']
|
|
78
|
-
yield ['X-Frame-Options', 'DENY']
|
|
79
|
-
yield ['X-Content-Type-Options', 'nosniff']
|
|
80
|
-
yield ['X-XSS-Protection', '0']
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function setSecurityHeaders(
|
|
84
|
-
res: ServerResponse,
|
|
85
|
-
options: SecurityHeadersOptions,
|
|
86
|
-
): void {
|
|
87
|
-
for (const [header, value] of buildSecurityHeaders(options)) {
|
|
88
|
-
// Only set the header if it is not already set
|
|
89
|
-
if (!res.hasHeader(header)) {
|
|
90
|
-
res.setHeader(header, value)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildHstsValue(config: HTTPStrictTransportSecurityConfig): string {
|
|
96
|
-
let value = `max-age=${config.maxAge}`
|
|
97
|
-
if (config.includeSubDomains) value += '; includeSubDomains'
|
|
98
|
-
if (config.preload) value += '; preload'
|
|
99
|
-
return value
|
|
100
|
-
}
|