@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +33 -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,57 +0,0 @@
1
- import type { IncomingMessage } from 'node:http'
2
- import { Readable } from 'node:stream'
3
- import createHttpError from 'http-errors'
4
- import { decodeStream, streamToNodeBuffer } from '@atproto/common'
5
- import {
6
- KnownNames,
7
- KnownParser,
8
- ParserResult,
9
- parseContentType,
10
- parsers,
11
- } from './parser.js'
12
-
13
- export function decodeHttpRequest(req: IncomingMessage): Readable {
14
- try {
15
- return decodeStream(req, req.headers['content-encoding'])
16
- } catch (cause) {
17
- const message =
18
- cause instanceof TypeError ? cause.message : `Invalid content-encoding`
19
- throw createHttpError(415, message, { cause })
20
- }
21
- }
22
-
23
- /**
24
- * Generic method that parses a stream of unknown nature (HTTP request/response,
25
- * socket, file, etc.), but of known mime type, into a parsed object.
26
- *
27
- * @throws {TypeError} If the content-type is not valid or supported.
28
- */
29
-
30
- export async function parseHttpRequest<A extends readonly KnownNames[]>(
31
- req: IncomingMessage,
32
- allow: A,
33
- ) {
34
- const type = parseContentType(
35
- req.headers['content-type'] ?? 'application/octet-stream',
36
- )
37
-
38
- const parser = parsers.find(
39
- (parser) => allow.includes(parser.name) && parser.test(type.mime),
40
- )
41
-
42
- if (!parser) {
43
- throw createHttpError(415, `Unsupported content-type: ${type.mime}`)
44
- }
45
-
46
- const stream = decodeHttpRequest(req)
47
- const buffer = await streamToNodeBuffer(stream)
48
- return parser.parse(buffer, type) as ParserResult<
49
- Extract<KnownParser, { name: A[number] }>
50
- >
51
- }
52
-
53
- export async function flushStream(stream: AsyncIterable<any>): Promise<void> {
54
- for await (const _ of stream) {
55
- // Consume the stream to completion
56
- }
57
- }
@@ -1,16 +0,0 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http'
2
-
3
- export type NextFunction = (err?: unknown) => void
4
-
5
- export type Middleware<
6
- T = void,
7
- Req = IncomingMessage,
8
- Res = ServerResponse,
9
- > = (this: T, req: Req, res: Res, next: NextFunction) => void
10
-
11
- export type Handler<T = void, Req = IncomingMessage, Res = ServerResponse> = (
12
- this: T,
13
- req: Req,
14
- res: Res,
15
- next?: NextFunction,
16
- ) => void
@@ -1,23 +0,0 @@
1
- export type UrlReference = {
2
- origin?: string
3
- pathname?: string
4
- searchParams?: Iterable<readonly [string, string]> // compatible with URLSearchParams
5
- }
6
-
7
- export function urlMatch(url: URL, reference: UrlReference) {
8
- if (reference.origin !== undefined) {
9
- if (url.origin !== reference.origin) return false
10
- }
11
-
12
- if (reference.pathname !== undefined) {
13
- if (url.pathname !== reference.pathname) return false
14
- }
15
-
16
- if (reference.searchParams !== undefined) {
17
- for (const [key, value] of reference.searchParams) {
18
- if (url.searchParams.get(key) !== value) return false
19
- }
20
- }
21
-
22
- return true
23
- }
package/src/lib/nsid.ts DELETED
@@ -1,10 +0,0 @@
1
- import { NSID } from '@atproto/syntax'
2
- export { NSID }
3
-
4
- export function parseNSID(value: string): NSID | null {
5
- try {
6
- return NSID.parse(value)
7
- } catch {
8
- return null
9
- }
10
- }
package/src/lib/redis.ts DELETED
@@ -1,25 +0,0 @@
1
- import { Redis, type RedisOptions } from 'ioredis'
2
-
3
- export type { Redis, RedisOptions }
4
-
5
- export type CreateRedisOptions = Redis | RedisOptions | string
6
-
7
- export function createRedis(options: CreateRedisOptions): Redis {
8
- if (typeof options === 'string') {
9
- const url = new URL(
10
- options.startsWith('redis://') ? options : `redis://${options}`,
11
- )
12
-
13
- return new Redis({
14
- host: url.hostname,
15
- port: parseInt(url.port, 10),
16
- password: url.password,
17
- })
18
- } else if ('on' in options && 'call' in options && 'acl' in options) {
19
- // Not using "instanceof" here in case the options is an instance of another
20
- // version of ioredis (Redis is both a class and an interface).
21
- return options
22
- } else {
23
- return new Redis(options)
24
- }
25
- }
@@ -1,28 +0,0 @@
1
- import { z } from 'zod'
2
- import {
3
- oauthAccessTokenSchema,
4
- oauthTokenTypeSchema,
5
- } from '@atproto/oauth-types'
6
- import { InvalidRequestError } from '../../errors/invalid-request-error.js'
7
- import { WWWAuthenticateError } from '../../errors/www-authenticate-error.js'
8
-
9
- export const authorizationHeaderSchema = z.tuple([
10
- oauthTokenTypeSchema,
11
- oauthAccessTokenSchema,
12
- ])
13
-
14
- export const parseAuthorizationHeader = (header: unknown) => {
15
- if (typeof header !== 'string') {
16
- throw new WWWAuthenticateError(
17
- 'invalid_request',
18
- 'Authorization header required',
19
- { Bearer: {}, DPoP: {} },
20
- )
21
- }
22
-
23
- const parsed = authorizationHeaderSchema.safeParse(header.split(' '))
24
- if (!parsed.success) {
25
- throw new InvalidRequestError('Invalid authorization header')
26
- }
27
- return parsed.data
28
- }
@@ -1,18 +0,0 @@
1
- export function asArray<T>(value: T | T[]): T[] {
2
- if (value == null) return []
3
- return Array.isArray(value) ? value : [value]
4
- }
5
-
6
- export function asURL(value: string | { toString: () => string }): URL {
7
- return new URL(value)
8
- }
9
-
10
- export function ifURL(
11
- value: string | { toString: () => string },
12
- ): URL | undefined {
13
- try {
14
- return asURL(value)
15
- } catch {
16
- return undefined
17
- }
18
- }
@@ -1,168 +0,0 @@
1
- import { parseUi8Dec, parseUi8Hex } from './ui8.js'
2
-
3
- export type RgbColor = { r: number; g: number; b: number }
4
- export type HslColor = { h: number; s: number; l: number }
5
- export type RgbaColor = { r: number; g: number; b: number; a: number }
6
- export type HslaColor = { h: number; s: number; l: number; a: number }
7
-
8
- export type Color = RgbColor | HslColor | RgbaColor | HslaColor
9
-
10
- export function parseColor(color: string): RgbColor | RgbaColor {
11
- if (color.startsWith('#')) {
12
- return parseHexColor(color)
13
- }
14
-
15
- if (color.startsWith('rgba(')) {
16
- return parseRgbaColor(color)
17
- }
18
-
19
- if (color.startsWith('rgb(')) {
20
- return parseRgbColor(color)
21
- }
22
-
23
- // Should never happen (as long as the input is a validated WebColor)
24
- throw new TypeError(`Invalid color value: ${color}`)
25
- }
26
-
27
- export function parseHexColor(v: string): RgbColor | RgbaColor {
28
- // parseInt('az', 16) does not return NaN so we need to check the format
29
- if (!/^#[0-9a-f]+$/i.test(v)) {
30
- throw new TypeError(`Invalid hex color value: ${v}`)
31
- }
32
-
33
- if (v.length === 4 || v.length === 5) {
34
- const r = parseUi8Hex(v[1].repeat(2))
35
- const g = parseUi8Hex(v[2].repeat(2))
36
- const b = parseUi8Hex(v[3].repeat(2))
37
- const a = v.length > 4 ? parseUi8Hex(v[4].repeat(2)) : undefined
38
- return a == null ? { r, g, b } : { r, g, b, a }
39
- }
40
-
41
- if (v.length === 7 || v.length === 9) {
42
- const r = parseUi8Hex(v.slice(1, 3))
43
- const g = parseUi8Hex(v.slice(3, 5))
44
- const b = parseUi8Hex(v.slice(5, 7))
45
- const a = v.length > 8 ? parseUi8Hex(v.slice(7, 9)) : undefined
46
- return a == null ? { r, g, b } : { r, g, b, a }
47
- }
48
-
49
- throw new TypeError(`Invalid hex color value: ${v}`)
50
- }
51
-
52
- export function parseRgbColor(v: string): RgbColor {
53
- const matches = v.match(/^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/)
54
- if (!matches) throw new TypeError(`Invalid rgb color value: ${v}`)
55
-
56
- const r = parseUi8Dec(matches[1])
57
- const g = parseUi8Dec(matches[2])
58
- const b = parseUi8Dec(matches[3])
59
- return { r, g, b }
60
- }
61
-
62
- export function parseRgbaColor(v: string): RgbaColor {
63
- const matches = v.match(
64
- /^\s*rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/,
65
- )
66
- if (!matches) throw new TypeError(`Invalid rgba color value: ${v}`)
67
-
68
- const r = parseUi8Dec(matches[1])
69
- const g = parseUi8Dec(matches[2])
70
- const b = parseUi8Dec(matches[3])
71
- const a = parseUi8Dec(matches[4])
72
- return { r, g, b, a }
73
- }
74
-
75
- /**
76
- * Return the color that has the best contrast with the reference color.
77
- */
78
- export function pickContrastColor(ref: RgbColor, a: RgbColor, b: RgbColor) {
79
- return computeContrastRatio(ref, a) > computeContrastRatio(ref, b) ? a : b
80
- }
81
-
82
- export function hslToRgb({ h, s, l }: HslColor): RgbColor
83
- export function hslToRgb({ h, s, l, a }: HslaColor): RgbaColor
84
- export function hslToRgb(input: HslaColor | HslColor): RgbColor | RgbaColor {
85
- const { h, s, l } = input
86
-
87
- // Achromatic (gray)
88
- if (s === 0) {
89
- const gray = Math.round(l * 255)
90
- return 'a' in input
91
- ? { r: gray, g: gray, b: gray, a: input.a }
92
- : { r: gray, g: gray, b: gray }
93
- }
94
-
95
- const hueToRgb = (p: number, q: number, t: number): number => {
96
- if (t < 0) t += 1
97
- if (t > 1) t -= 1
98
- if (t < 1 / 6) return p + (q - p) * 6 * t
99
- if (t < 1 / 2) return q
100
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
101
- return p
102
- }
103
-
104
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s
105
- const p = 2 * l - q
106
- const hNorm = h / 360
107
-
108
- const r = Math.round(hueToRgb(p, q, hNorm + 1 / 3) * 255)
109
- const g = Math.round(hueToRgb(p, q, hNorm) * 255)
110
- const b = Math.round(hueToRgb(p, q, hNorm - 1 / 3) * 255)
111
-
112
- return 'a' in input ? { r, g, b, a: input.a } : { r, g, b }
113
- }
114
-
115
- /**
116
- * @see {@link https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef}
117
- */
118
- function relativeLuminance(color: HslColor | RgbColor) {
119
- const { r, g, b } = 'h' in color ? hslToRgb(color) : color
120
- return rgbLum(r) * 0.2126 + rgbLum(g) * 0.7152 + rgbLum(b) * 0.0722
121
- }
122
-
123
- function rgbLum(value) {
124
- const rgb = value / 255
125
- return rgb < 0.03928 ? rgb / 12.92 : Math.pow((rgb + 0.055) / 1.055, 2.4)
126
- }
127
-
128
- /**
129
- * @see {@link https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef}
130
- */
131
- function computeContrastRatio(a: RgbColor, b: RgbColor) {
132
- const aLum = relativeLuminance(a)
133
- const bLum = relativeLuminance(b)
134
- const [lighter, darker] = aLum > bLum ? [aLum, bLum] : [bLum, aLum]
135
- return (lighter + 0.05) / (darker + 0.05)
136
- }
137
-
138
- export function extractHue(input: RgbColor): number {
139
- const r = input.r / 255
140
- const g = input.g / 255
141
- const b = input.b / 255
142
-
143
- const max = Math.max(r, g, b)
144
- const min = Math.min(r, g, b)
145
-
146
- const chroma = max - min
147
-
148
- switch (max) {
149
- case min:
150
- return 0 // Achromatic
151
- case r: {
152
- const segment = (g - b) / chroma
153
- const shift = segment < 0 ? 360 / 60 : 0 / 60
154
- return 60 * (segment + shift)
155
- }
156
- case g: {
157
- const segment = (b - r) / chroma
158
- const shift = 120 / 60
159
- return 60 * (segment + shift)
160
- }
161
- // "default" needed for type safety. In practice, should be same as "case b:"
162
- default: {
163
- const segment = (r - g) / chroma
164
- const shift = 240 / 60
165
- return 60 * (segment + shift)
166
- }
167
- }
168
- }
@@ -1,32 +0,0 @@
1
- import { randomBytes } from 'node:crypto'
2
-
3
- export async function randomBuffer(bytesLength = 16) {
4
- return new Promise<Buffer>((resolve, reject) => {
5
- randomBytes(bytesLength, (err, buf) => {
6
- if (err) return reject(err)
7
- resolve(buf)
8
- })
9
- })
10
- }
11
-
12
- export async function randomHexId(bytesLength = 16) {
13
- const buffer = await randomBuffer(bytesLength)
14
- return buffer.toString('hex')
15
- }
16
-
17
- // Basically all algorithms supported by "jose"'s jwtVerify().
18
- // @TODO Is there a way to get this list from the runtime instead of hardcoding it?
19
- export const VERIFY_ALGOS = [
20
- 'RS256',
21
- 'RS384',
22
- 'RS512',
23
-
24
- 'PS256',
25
- 'PS384',
26
- 'PS512',
27
-
28
- 'ES256',
29
- 'ES256K',
30
- 'ES384',
31
- 'ES512',
32
- ] as const
@@ -1,7 +0,0 @@
1
- export function dateToEpoch(date: Date = new Date()) {
2
- return Math.floor(date.getTime() / 1000)
3
- }
4
-
5
- export function dateToRelativeSeconds(date: Date) {
6
- return Math.floor((date.getTime() - Date.now()) / 1000)
7
- }
@@ -1,7 +0,0 @@
1
- import { ZodError } from 'zod'
2
- import { formatZodError } from './zod-error.js'
3
-
4
- export function formatError(err: unknown, prefix: string): string {
5
- if (err instanceof ZodError) return formatZodError(err, prefix)
6
- return prefix
7
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * This function serves two purposes:
3
- * - It ensures that the return value is a Promise, even if the function returns
4
- * a "thenable" (i.e. a Promise-like object).
5
- * - It allows to avoid assigning a `this` context to the function, which is
6
- * particularly useful when the function is a member of a "private" object.
7
- */
8
- export async function callAsync<F extends (...args: any[]) => unknown>(
9
- fn: F,
10
- ...args: Parameters<F>
11
- ): Promise<Awaited<ReturnType<F>>>
12
- export async function callAsync<F extends (...args: any[]) => unknown>(
13
- fn?: F,
14
- ...args: Parameters<F>
15
- ): Promise<Awaited<ReturnType<F>> | undefined>
16
- export async function callAsync<F extends (...args: any[]) => unknown>(
17
- fn?: F,
18
- ...args: Parameters<F>
19
- ): Promise<unknown> {
20
- return fn?.(...args)
21
- }
22
-
23
- export function invokeOnce<T extends (this: any, ...a: any[]) => any>(
24
- fn: T,
25
- ): T {
26
- let fnNullable: T | null = fn
27
- return function (...args) {
28
- if (fnNullable) {
29
- const fn = fnNullable
30
- fnNullable = null
31
- return fn.call(this, ...args)
32
- }
33
- throw new Error('Function called multiple times')
34
- } as T
35
- }
36
-
37
- export function includedIn<T>(this: readonly T[], value: T): boolean {
38
- return this.includes(value)
39
- }
@@ -1,12 +0,0 @@
1
- import { z } from 'zod'
2
-
3
- export const localeSchema = z
4
- .string()
5
- .regex(/^[a-z]{2,3}(-[A-Z]{2})?$/, 'Invalid locale')
6
- export type Locale = z.infer<typeof localeSchema>
7
-
8
- export const multiLangStringSchema = z.record(
9
- localeSchema,
10
- z.string().optional(),
11
- )
12
- export type MultiLangString = z.infer<typeof multiLangStringSchema>
@@ -1,23 +0,0 @@
1
- export function mergeDefaults<T extends object>(
2
- defaults: T,
3
- ...overrides: (T | undefined)[]
4
- ): T {
5
- // @NOTE Not using the spread operator here because TS allows "undefined"
6
- // values to be spread, which can lead to defaults being overwritten with
7
- // "undefined". This function ensures that only defined values in "options"
8
- // will overwrite the corresponding values in "defaults".
9
- if (!overrides.length) return defaults
10
- if (!overrides.some(Boolean)) return defaults
11
- const result: T = { ...defaults } as T
12
- for (const options of overrides) {
13
- if (options) {
14
- for (const key in options) {
15
- const value = options[key]
16
- if (value !== undefined) {
17
- result[key] = value
18
- }
19
- }
20
- }
21
- }
22
- return result
23
- }
@@ -1,46 +0,0 @@
1
- import { isLoopbackHost } from '@atproto/oauth-types'
2
-
3
- /**
4
- *
5
- * @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-8.4}
6
- * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-8.4.2}
7
- */
8
- export function compareRedirectUri(
9
- allowed_uri: string,
10
- request_uri: string,
11
- ): boolean {
12
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
13
- //
14
- // > Authorization servers MUST require clients to register their complete
15
- // > redirect URI (including the path component) and reject authorization
16
- // > requests that specify a redirect URI that doesn't exactly match the
17
- // > one that was registered; the exception is loopback redirects, where
18
- // > an exact match is required except for the port URI component.
19
- if (allowed_uri === request_uri) return true
20
-
21
- // https://datatracker.ietf.org/doc/html/rfc8252#section-7.3
22
- const allowedUri = new URL(allowed_uri)
23
- if (isLoopbackHost(allowedUri.hostname)) {
24
- const requestUri = new URL(request_uri)
25
-
26
- return (
27
- // > The authorization server MUST allow any port to be specified at the
28
- // > time of the request for loopback IP redirect URIs, to accommodate
29
- // > clients that obtain an available ephemeral port from the operating
30
- // > system at the time of the request
31
- //
32
- // Note: We only apply this rule if the allowed URI does not have a port
33
- // specified.
34
- (!allowedUri.port || allowedUri.port === requestUri.port) &&
35
- allowedUri.hostname === requestUri.hostname &&
36
- allowedUri.pathname === requestUri.pathname &&
37
- allowedUri.protocol === requestUri.protocol &&
38
- allowedUri.search === requestUri.search &&
39
- allowedUri.hash === requestUri.hash &&
40
- allowedUri.username === requestUri.username &&
41
- allowedUri.password === requestUri.password
42
- )
43
- }
44
-
45
- return false
46
- }
@@ -1,49 +0,0 @@
1
- import { setTimeout as sleep } from 'node:timers/promises'
2
- import { Awaitable } from './type.js'
3
-
4
- export function onOvertimeDefault(options: {
5
- start: number
6
- end: number
7
- elapsed: number
8
- time: number
9
- }): void {
10
- console.warn(
11
- `constantTime: execution time was ${options.elapsed}ms (which is greater than ${options.time}ms). You should increase the "time" to properly defend against timing attacks.`,
12
- )
13
- }
14
-
15
- /**
16
- * Utility function to protect against timing attacks.
17
- */
18
- export async function constantTime<R>(
19
- time: number,
20
- fn: () => Awaitable<R>,
21
- onOvertime = onOvertimeDefault,
22
- ): Promise<R> {
23
- if (!Number.isFinite(time) || time <= 0) {
24
- throw new TypeError(`"time" must be a positive number`)
25
- }
26
-
27
- const start = Date.now()
28
- try {
29
- return await fn()
30
- } finally {
31
- const end = Date.now()
32
- const elapsed = end - start
33
-
34
- const remaining = time - elapsed
35
- if (remaining >= 0) {
36
- // Happy path, execution time was smaller than "time"
37
- await sleep(remaining)
38
- } else {
39
- // The function execution took longer than "time"
40
- onOvertime({ start, end, elapsed, time })
41
-
42
- // Sleep until the next multiple of "time" to mitigate any attack
43
- const multiplier = Math.ceil(elapsed / time)
44
- const remaining = multiplier * time - elapsed
45
-
46
- await sleep(remaining)
47
- }
48
- }
49
- }