@atproto/oauth-provider 0.19.7 → 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.
Files changed (165) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }