@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/accept.ts
DELETED
|
@@ -1,90 +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 { mediaType } = accept
|
|
6
|
-
import { SubCtx, subCtx } from './context.js'
|
|
7
|
-
import { Middleware, NextFunction } from './types.js'
|
|
8
|
-
|
|
9
|
-
type View<
|
|
10
|
-
T extends object | void,
|
|
11
|
-
D,
|
|
12
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
13
|
-
Res extends ServerResponse = ServerResponse,
|
|
14
|
-
> = (
|
|
15
|
-
this: SubCtx<T, { data: D }>,
|
|
16
|
-
req: Req,
|
|
17
|
-
res: Res,
|
|
18
|
-
next: NextFunction,
|
|
19
|
-
) => void | PromiseLike<void>
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* app.use(
|
|
25
|
-
* acceptMiddleware(
|
|
26
|
-
* async function (req, res) {
|
|
27
|
-
* return { hello: 'world' }
|
|
28
|
-
* },
|
|
29
|
-
* {
|
|
30
|
-
* '': 'application/json', // Fallback to JSON
|
|
31
|
-
* 'text/plain': function (req, res) {
|
|
32
|
-
* res.writeHead(200).end(this.data.hello)
|
|
33
|
-
* },
|
|
34
|
-
* 'application/json': function (req, res) {
|
|
35
|
-
* res.writeHead(200).end(JSON.stringify(this.data))
|
|
36
|
-
* }
|
|
37
|
-
* }
|
|
38
|
-
* )
|
|
39
|
-
* )
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export function acceptMiddleware<
|
|
43
|
-
D,
|
|
44
|
-
T extends object | void = void,
|
|
45
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
46
|
-
Res extends ServerResponse = ServerResponse,
|
|
47
|
-
>(
|
|
48
|
-
controller: (this: T, req: Req, res: Res) => D | PromiseLike<D>,
|
|
49
|
-
views: Record<string, string | View<T, D, Req, Res>>,
|
|
50
|
-
fallback: Middleware<T> = (req, res, _next) => void res.writeHead(406).end(),
|
|
51
|
-
): (this: T, req: Req, res: Res, next: NextFunction) => Promise<void> {
|
|
52
|
-
const viewsMap = new Map(Object.entries(views))
|
|
53
|
-
const preferences = Array.from(viewsMap.keys()).filter(Boolean)
|
|
54
|
-
|
|
55
|
-
// Make sure that every view is either a function or a string that points to a
|
|
56
|
-
// function.
|
|
57
|
-
for (const type of viewsMap.keys()) {
|
|
58
|
-
const view = viewsMap.get(type)
|
|
59
|
-
if (typeof view === 'string' && typeof viewsMap.get(view) !== 'function') {
|
|
60
|
-
throw new Error(`Invalid view "${view}" for media type "${type}"`)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return async function (req, res, next) {
|
|
65
|
-
try {
|
|
66
|
-
const type = req.headers['accept']
|
|
67
|
-
? mediaType(req.headers['accept'], preferences) || undefined
|
|
68
|
-
: '' // indicate that the client accepts anything
|
|
69
|
-
|
|
70
|
-
let view = type != null ? viewsMap.get(type) : undefined
|
|
71
|
-
|
|
72
|
-
if (typeof view === 'string') view = viewsMap.get(view)
|
|
73
|
-
if (typeof view === 'string') throw new Error('Invalid view') // should not happen
|
|
74
|
-
|
|
75
|
-
if (view) {
|
|
76
|
-
const data = await controller.call(this, req, res)
|
|
77
|
-
const ctx = subCtx(this, { data })
|
|
78
|
-
if (type) res.setHeader('Content-Type', type)
|
|
79
|
-
|
|
80
|
-
await view.call(ctx, req, res, next)
|
|
81
|
-
} else {
|
|
82
|
-
// media negotiation failed
|
|
83
|
-
await fallback.call(this, req, res, next)
|
|
84
|
-
}
|
|
85
|
-
} catch (err) {
|
|
86
|
-
if (!res.headersSent) res.removeHeader('Content-Type')
|
|
87
|
-
next(err)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
package/src/lib/http/context.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export type SubCtx<Parent extends object | void, Child extends object> = Child &
|
|
2
|
-
Omit<Parent, keyof Child>
|
|
3
|
-
|
|
4
|
-
export function subCtx<Parent extends object | void, Child extends object>(
|
|
5
|
-
parent: Parent,
|
|
6
|
-
child: Child,
|
|
7
|
-
): SubCtx<Parent, Child> {
|
|
8
|
-
const proto = typeof parent === 'object' ? parent : null
|
|
9
|
-
const entries = Object.entries(child)
|
|
10
|
-
|
|
11
|
-
// Optimization for small objects
|
|
12
|
-
switch (entries.length) {
|
|
13
|
-
case 0:
|
|
14
|
-
return Object.create(proto)
|
|
15
|
-
case 1: {
|
|
16
|
-
const e0 = entries[0]
|
|
17
|
-
return Object.create(proto, {
|
|
18
|
-
[e0[0]]: valueDescriptor(e0[1]),
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
case 2: {
|
|
22
|
-
const e0 = entries[0]
|
|
23
|
-
const e1 = entries[1]
|
|
24
|
-
return Object.create(proto, {
|
|
25
|
-
[e0[0]]: valueDescriptor(e0[1]),
|
|
26
|
-
[e1[0]]: valueDescriptor(e1[1]),
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return Object.create(proto, Object.fromEntries(entries.map(entryToEntryDesc)))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function entryToEntryDesc(
|
|
35
|
-
entry: [string, unknown],
|
|
36
|
-
): [string, PropertyDescriptor] {
|
|
37
|
-
return [entry[0], valueDescriptor(entry[1])]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function valueDescriptor(value: unknown): PropertyDescriptor {
|
|
41
|
-
return { value, enumerable: true, writable: false }
|
|
42
|
-
}
|
package/src/lib/http/headers.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ServerResponse } from 'node:http'
|
|
2
|
-
|
|
3
|
-
export function appendHeader(
|
|
4
|
-
res: ServerResponse,
|
|
5
|
-
header: string,
|
|
6
|
-
value: string | readonly string[],
|
|
7
|
-
): void {
|
|
8
|
-
const existing = res.getHeader(header)
|
|
9
|
-
if (existing == null) {
|
|
10
|
-
res.setHeader(header, value)
|
|
11
|
-
} else {
|
|
12
|
-
const arr = Array.isArray(existing) ? existing : [String(existing)]
|
|
13
|
-
res.setHeader(header, arr.concat(value))
|
|
14
|
-
}
|
|
15
|
-
}
|
package/src/lib/http/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export * from './accept.js'
|
|
2
|
-
export * from './context.js'
|
|
3
|
-
export * from './headers.js'
|
|
4
|
-
export * from './middleware.js'
|
|
5
|
-
export * from './parser.js'
|
|
6
|
-
export * from './request.js'
|
|
7
|
-
export * from './response.js'
|
|
8
|
-
export * from './router.js'
|
|
9
|
-
export * from './stream.js'
|
|
10
|
-
export * from './types.js'
|
package/src/lib/http/method.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage } from 'node:http'
|
|
2
|
-
|
|
3
|
-
export type MethodMatcherInput = string | Iterable<string> | MethodMatcher
|
|
4
|
-
export type MethodMatcher = (req: IncomingMessage) => boolean
|
|
5
|
-
|
|
6
|
-
export function createMethodMatcher(method: MethodMatcherInput): MethodMatcher {
|
|
7
|
-
if (method === '*') return () => true
|
|
8
|
-
if (typeof method === 'function') return method
|
|
9
|
-
|
|
10
|
-
if (typeof method === 'string') {
|
|
11
|
-
method = method.toUpperCase()
|
|
12
|
-
return (req) => req.method === method
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const set = new Set(Array.from(method, (m) => m.toUpperCase()))
|
|
16
|
-
if (set.size === 0) return () => false
|
|
17
|
-
return (req) => req.method != null && set.has(req.method)
|
|
18
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import { invokeOnce } from '../util/function.js'
|
|
3
|
-
import { writeJson } from './response.js'
|
|
4
|
-
import { Handler, Middleware, NextFunction } from './types.js'
|
|
5
|
-
|
|
6
|
-
const isNonNullable = <X>(x: X): x is NonNullable<X> => x != null
|
|
7
|
-
|
|
8
|
-
export function combineMiddlewares<M extends Middleware<any, any, any>>(
|
|
9
|
-
middlewares: Iterable<null | undefined | M>,
|
|
10
|
-
options?: { skipKeyword?: string },
|
|
11
|
-
): M
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Combine express/connect like middlewares (that can be async) into a single
|
|
15
|
-
* middleware.
|
|
16
|
-
*/
|
|
17
|
-
export function combineMiddlewares(
|
|
18
|
-
middlewares: Iterable<null | undefined | Middleware<unknown>>,
|
|
19
|
-
{ skipKeyword }: { skipKeyword?: string } = {},
|
|
20
|
-
): Middleware<unknown> {
|
|
21
|
-
const middlewaresArray = Array.from(middlewares).filter(isNonNullable)
|
|
22
|
-
|
|
23
|
-
// Optimization: if there are no middlewares, return a noop middleware.
|
|
24
|
-
if (middlewaresArray.length === 0) return (req, res, next) => void next()
|
|
25
|
-
if (middlewaresArray.length === 1) return middlewaresArray[0]
|
|
26
|
-
|
|
27
|
-
return function (req, res, next) {
|
|
28
|
-
let i = 0
|
|
29
|
-
const nextMiddleware = (err?: unknown) => {
|
|
30
|
-
if (err) {
|
|
31
|
-
if (skipKeyword && err === skipKeyword) next()
|
|
32
|
-
else next(err)
|
|
33
|
-
} else if (i >= middlewaresArray.length) {
|
|
34
|
-
next()
|
|
35
|
-
} else {
|
|
36
|
-
const currentMiddleware = middlewaresArray[i++]!
|
|
37
|
-
const currentNext = invokeOnce(nextMiddleware)
|
|
38
|
-
currentMiddleware.call(this, req, res, currentNext)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
nextMiddleware()
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type AsHandler<M extends Middleware<any, any, any>> =
|
|
46
|
-
M extends Middleware<infer T, infer Req, infer Res>
|
|
47
|
-
? Handler<T, Req, Res>
|
|
48
|
-
: never
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Convert a middleware in a function that can be used as both a middleware and
|
|
52
|
-
* and handler.
|
|
53
|
-
*/
|
|
54
|
-
export function asHandler<M extends Middleware<any, any, any>>(
|
|
55
|
-
middleware: M,
|
|
56
|
-
options?: FinalHandlerOptions,
|
|
57
|
-
) {
|
|
58
|
-
return function (
|
|
59
|
-
this,
|
|
60
|
-
req,
|
|
61
|
-
res,
|
|
62
|
-
next = invokeOnce(createFinalHandler(req, res, options)),
|
|
63
|
-
) {
|
|
64
|
-
return middleware.call(this, req, res, next)
|
|
65
|
-
} as AsHandler<M>
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export const DEV_MODE = process.env['NODE_ENV'] === 'development'
|
|
69
|
-
|
|
70
|
-
export type FinalHandlerOptions = {
|
|
71
|
-
debug?: boolean
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function createFinalHandler(
|
|
75
|
-
req: IncomingMessage,
|
|
76
|
-
res: ServerResponse,
|
|
77
|
-
options?: FinalHandlerOptions,
|
|
78
|
-
): NextFunction {
|
|
79
|
-
return (err) => {
|
|
80
|
-
if (err != null && (options?.debug ?? DEV_MODE)) {
|
|
81
|
-
console.error(err)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (res.headersSent) {
|
|
85
|
-
// If an error occurred, and headers were sent, we can't know that the
|
|
86
|
-
// whole response body was sent. So we can't safely reuse the socket.
|
|
87
|
-
if (err) req.socket.destroy()
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const { status, ...payload } = buildFallbackPayload(req, err)
|
|
93
|
-
|
|
94
|
-
res.setHeader('Content-Security-Policy', "default-src 'none'")
|
|
95
|
-
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
96
|
-
|
|
97
|
-
writeJson(res, payload, { status })
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function buildFallbackPayload(
|
|
102
|
-
req: IncomingMessage,
|
|
103
|
-
err: unknown,
|
|
104
|
-
): {
|
|
105
|
-
status: number
|
|
106
|
-
error: string
|
|
107
|
-
error_description: string
|
|
108
|
-
stack?: undefined | string
|
|
109
|
-
} {
|
|
110
|
-
const status = err ? getErrorStatusCode(err) : 404
|
|
111
|
-
const expose = getProp(err, 'expose', 'boolean') ?? status < 500
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
status,
|
|
115
|
-
error: err
|
|
116
|
-
? expose
|
|
117
|
-
? getProp(err, 'code', 'string') ??
|
|
118
|
-
getProp(err, 'error', 'string') ??
|
|
119
|
-
'unknown_error'
|
|
120
|
-
: 'system_error'
|
|
121
|
-
: 'not_found',
|
|
122
|
-
error_description:
|
|
123
|
-
err instanceof Error
|
|
124
|
-
? expose
|
|
125
|
-
? getProp(err, 'error_description', 'string') ||
|
|
126
|
-
String(err.message) ||
|
|
127
|
-
'Unknown error'
|
|
128
|
-
: 'System error'
|
|
129
|
-
: `Cannot ${req.method} ${req.url}`,
|
|
130
|
-
stack: DEV_MODE && err instanceof Error ? err.stack : undefined,
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function getErrorStatusCode(err: NonNullable<unknown>): number {
|
|
135
|
-
const status =
|
|
136
|
-
getProp(err, 'status', 'number') ?? getProp(err, 'statusCode', 'number')
|
|
137
|
-
return status != null && status >= 400 && status < 600 ? status : 500
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// eslint-disable-next-line
|
|
141
|
-
function getProp(obj: unknown, key: string, t: 'function'): Function | undefined
|
|
142
|
-
function getProp(obj: unknown, key: string, t: 'string'): string | undefined
|
|
143
|
-
function getProp(obj: unknown, key: string, t: 'number'): number | undefined
|
|
144
|
-
function getProp(obj: unknown, key: string, t: 'boolean'): boolean | undefined
|
|
145
|
-
function getProp(obj: unknown, key: string, t: 'object'): object | undefined
|
|
146
|
-
function getProp(obj: unknown, key: string, t: 'symbol'): symbol | undefined
|
|
147
|
-
function getProp(obj: unknown, key: string, t: 'bigint'): bigint | undefined
|
|
148
|
-
function getProp(obj: unknown, key: string, t: 'undefined'): undefined
|
|
149
|
-
function getProp(
|
|
150
|
-
obj: unknown,
|
|
151
|
-
key: string,
|
|
152
|
-
type:
|
|
153
|
-
| 'string'
|
|
154
|
-
| 'number'
|
|
155
|
-
| 'boolean'
|
|
156
|
-
| 'object'
|
|
157
|
-
| 'function'
|
|
158
|
-
| 'symbol'
|
|
159
|
-
| 'bigint'
|
|
160
|
-
| 'undefined',
|
|
161
|
-
): unknown
|
|
162
|
-
|
|
163
|
-
function getProp(obj: unknown, key: string, type: string): unknown {
|
|
164
|
-
if (obj != null && typeof obj === 'object' && key in obj) {
|
|
165
|
-
const value = (obj as Record<string, unknown>)[key]
|
|
166
|
-
if (typeof value === type) return value
|
|
167
|
-
}
|
|
168
|
-
return undefined
|
|
169
|
-
}
|
package/src/lib/http/parser.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { parse as parseJson } from '@hapi/bourne'
|
|
2
|
-
import { type as hapiContentType } from '@hapi/content'
|
|
3
|
-
import createHttpError from 'http-errors'
|
|
4
|
-
|
|
5
|
-
export type JsonScalar = string | number | boolean | null
|
|
6
|
-
export type Json = JsonScalar | Json[] | { [_ in string]?: Json }
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Parse a content-type string into its components.
|
|
10
|
-
*
|
|
11
|
-
* @throws {TypeError} If the content-type is invalid.
|
|
12
|
-
*/
|
|
13
|
-
export function parseContentType(type: unknown): ContentType {
|
|
14
|
-
if (typeof type !== 'string') {
|
|
15
|
-
throw createHttpError(
|
|
16
|
-
415,
|
|
17
|
-
`Invalid content-type: ${type == null ? String(type) : typeof type}`,
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
return hapiContentType(type)
|
|
23
|
-
} catch (err) {
|
|
24
|
-
// De-boomify the error
|
|
25
|
-
throw createHttpError(
|
|
26
|
-
415,
|
|
27
|
-
err instanceof Error ? err.message : 'Invalid content-type',
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type ContentType = {
|
|
33
|
-
mime: string
|
|
34
|
-
charset?: string
|
|
35
|
-
boundary?: string
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type Parser<T extends string = string, R = unknown> = {
|
|
39
|
-
readonly name: string
|
|
40
|
-
readonly test: (mime: string) => mime is T
|
|
41
|
-
readonly parse: (buffer: Buffer, type: ContentType) => R
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type ParserName<P extends Parser> = P extends { readonly name: infer N }
|
|
45
|
-
? N
|
|
46
|
-
: never
|
|
47
|
-
export type ParserType<P extends Parser> = P extends Parser<infer T> ? T : never
|
|
48
|
-
export type ParserResult<P extends Parser> = ReturnType<P['parse']>
|
|
49
|
-
|
|
50
|
-
export type ParserForType<P extends Parser, T> =
|
|
51
|
-
P extends Parser<infer U> ? (U extends T ? P : never) : never
|
|
52
|
-
|
|
53
|
-
export const parsers = [
|
|
54
|
-
{
|
|
55
|
-
name: 'json',
|
|
56
|
-
test: (mime): mime is `application/json` | `application/${string}+json` => {
|
|
57
|
-
return /^application\/(?:.+\+)?json$/.test(mime)
|
|
58
|
-
},
|
|
59
|
-
parse: (buffer, { charset }): Json => {
|
|
60
|
-
if (charset != null && !/^utf-?8$/i.test(charset)) {
|
|
61
|
-
throw createHttpError(415, 'Unsupported charset')
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
return parseJson(buffer.toString())
|
|
65
|
-
} catch (err) {
|
|
66
|
-
throw createHttpError(400, 'Invalid JSON', { cause: err })
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'urlencoded',
|
|
72
|
-
test: (mime): mime is 'application/x-www-form-urlencoded' => {
|
|
73
|
-
return mime === 'application/x-www-form-urlencoded'
|
|
74
|
-
},
|
|
75
|
-
parse: (buffer, { charset }): { [_ in string]?: string } => {
|
|
76
|
-
if (charset != null && !/^utf-?8$/i.test(charset)) {
|
|
77
|
-
throw createHttpError(415, 'Unsupported charset')
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
if (!buffer.length) return {}
|
|
81
|
-
const params = new URLSearchParams(buffer.toString())
|
|
82
|
-
if (params.has('__proto__')) throw new TypeError('Invalid key')
|
|
83
|
-
return Object.fromEntries(params)
|
|
84
|
-
} catch (err) {
|
|
85
|
-
throw createHttpError(400, 'Invalid URL-encoded data', { cause: err })
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: 'bytes',
|
|
91
|
-
test: (mime): mime is 'application/octet-stream' => {
|
|
92
|
-
return mime === 'application/octet-stream'
|
|
93
|
-
},
|
|
94
|
-
parse: (buffer): Buffer => buffer,
|
|
95
|
-
},
|
|
96
|
-
] as const satisfies Parser[]
|
|
97
|
-
|
|
98
|
-
export type KnownParser = (typeof parsers)[number]
|
|
99
|
-
|
|
100
|
-
export type KnownNames = KnownParser['name']
|
|
101
|
-
export type KnownTypes = ParserType<KnownParser>
|
package/src/lib/http/path.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
export type PathMatcher<P extends Params> = (pathname: string) => P | undefined
|
|
2
|
-
|
|
3
|
-
type StringPath<P extends Params> = string extends keyof P
|
|
4
|
-
? `/${string}`
|
|
5
|
-
: keyof P extends never
|
|
6
|
-
? `/${string}` | ``
|
|
7
|
-
: {
|
|
8
|
-
[K in keyof P]: K extends string
|
|
9
|
-
?
|
|
10
|
-
| `${`/:${K}` | `/${string}/:${K}`}${StringPath<Omit<P, K>>}`
|
|
11
|
-
| `${StringPath<Omit<P, K>>}${`/:${K}` | `/:${K}/${string}`}`
|
|
12
|
-
: never
|
|
13
|
-
}[keyof P]
|
|
14
|
-
|
|
15
|
-
export type Path<P extends Params> =
|
|
16
|
-
| string
|
|
17
|
-
| StringPath<P>
|
|
18
|
-
| RegExp
|
|
19
|
-
| PathMatcher<P>
|
|
20
|
-
export type Params = Record<string, undefined | string>
|
|
21
|
-
|
|
22
|
-
export function createPathMatcher<P extends Params = Params>(
|
|
23
|
-
refPath: Path<P>,
|
|
24
|
-
): PathMatcher<P> {
|
|
25
|
-
if (typeof refPath === 'string') {
|
|
26
|
-
// Create a path matcher for a path with parameters (like /foo/:fooId/bar/:barId).
|
|
27
|
-
if (refPath.includes('/:')) {
|
|
28
|
-
const refParts = refPath
|
|
29
|
-
.slice(1)
|
|
30
|
-
.split('/')
|
|
31
|
-
.map((part, i) => [part, i] as const)
|
|
32
|
-
const refPartsLength = refParts.length
|
|
33
|
-
|
|
34
|
-
const staticParts = refParts.filter(([p]) => !p.startsWith(':'))
|
|
35
|
-
const paramParts = refParts
|
|
36
|
-
// Extract parameters, ignoring those with no name (like /foo/:/bar).
|
|
37
|
-
.filter(([p]) => p.startsWith(':') && p.length > 1)
|
|
38
|
-
.map(([p, i]) => [p.slice(1), i] as const)
|
|
39
|
-
|
|
40
|
-
return (reqPath: string) => {
|
|
41
|
-
const reqParts = reqPath.slice(1).split('/')
|
|
42
|
-
|
|
43
|
-
if (reqParts.length !== refPartsLength) return undefined
|
|
44
|
-
|
|
45
|
-
// Make sure all static parts match.
|
|
46
|
-
for (let i = 0; i < staticParts.length; i++) {
|
|
47
|
-
const value = staticParts[i]![0]
|
|
48
|
-
const idx = staticParts[i]![1]
|
|
49
|
-
|
|
50
|
-
if (value !== reqParts[idx]) return undefined
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Then extract the parameters.
|
|
54
|
-
const params: Record<string, string> = {}
|
|
55
|
-
for (let i = 0; i < paramParts.length; i++) {
|
|
56
|
-
const name = paramParts[i]![0]
|
|
57
|
-
const idx = paramParts[i]![1]
|
|
58
|
-
|
|
59
|
-
const value = reqParts[idx]
|
|
60
|
-
|
|
61
|
-
// Empty parameter values are not allowed.
|
|
62
|
-
if (!value) return undefined
|
|
63
|
-
|
|
64
|
-
params[name] = value
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return params as P
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return (reqPath: string) => (reqPath === refPath ? ({} as P) : undefined)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (refPath instanceof RegExp) {
|
|
75
|
-
return (reqPath: string) => {
|
|
76
|
-
const match = reqPath.match(refPath)
|
|
77
|
-
return match ? ((match.groups || {}) as P) : undefined
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return refPath
|
|
82
|
-
}
|