@atproto/oauth-provider 0.2.16 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (230) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/account/account-store.d.ts +1 -1
  3. package/dist/account/account-store.d.ts.map +1 -1
  4. package/dist/account/account-store.js +6 -9
  5. package/dist/account/account-store.js.map +1 -1
  6. package/dist/account/account.d.ts +1 -1
  7. package/dist/account/account.d.ts.map +1 -1
  8. package/dist/assets/app/bundle-manifest.json +2 -2
  9. package/dist/assets/app/main.js +1 -1
  10. package/dist/assets/app/main.js.map +1 -1
  11. package/dist/assets/assets-middleware.d.ts.map +1 -1
  12. package/dist/assets/assets-middleware.js.map +1 -1
  13. package/dist/assets/index.d.ts.map +1 -1
  14. package/dist/assets/index.js +7 -6
  15. package/dist/assets/index.js.map +1 -1
  16. package/dist/client/client-auth.d.ts +1 -1
  17. package/dist/client/client-auth.d.ts.map +1 -1
  18. package/dist/client/client-auth.js +1 -1
  19. package/dist/client/client-auth.js.map +1 -1
  20. package/dist/client/client-manager.d.ts +2 -2
  21. package/dist/client/client-manager.d.ts.map +1 -1
  22. package/dist/client/client-manager.js +8 -10
  23. package/dist/client/client-manager.js.map +1 -1
  24. package/dist/client/client-store.d.ts.map +1 -1
  25. package/dist/client/client-store.js.map +1 -1
  26. package/dist/client/client-utils.d.ts.map +1 -1
  27. package/dist/client/client-utils.js.map +1 -1
  28. package/dist/client/client.d.ts +1 -1
  29. package/dist/client/client.d.ts.map +1 -1
  30. package/dist/client/client.js +1 -1
  31. package/dist/client/client.js.map +1 -1
  32. package/dist/device/device-data.d.ts +7 -8
  33. package/dist/device/device-data.d.ts.map +1 -1
  34. package/dist/device/device-data.js +3 -2
  35. package/dist/device/device-data.js.map +1 -1
  36. package/dist/device/device-id.d.ts.map +1 -1
  37. package/dist/device/device-id.js.map +1 -1
  38. package/dist/device/device-manager.d.ts +104 -19
  39. package/dist/device/device-manager.d.ts.map +1 -1
  40. package/dist/device/device-manager.js +44 -31
  41. package/dist/device/device-manager.js.map +1 -1
  42. package/dist/device/session-id.d.ts.map +1 -1
  43. package/dist/device/session-id.js.map +1 -1
  44. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  45. package/dist/dpop/dpop-manager.js.map +1 -1
  46. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  47. package/dist/dpop/dpop-nonce.js.map +1 -1
  48. package/dist/errors/invalid-client-metadata-error.js +1 -1
  49. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  50. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  51. package/dist/errors/invalid-token-error.js +1 -1
  52. package/dist/errors/invalid-token-error.js.map +1 -1
  53. package/dist/errors/www-authenticate-error.d.ts.map +1 -1
  54. package/dist/errors/www-authenticate-error.js.map +1 -1
  55. package/dist/lib/http/accept.d.ts +2 -1
  56. package/dist/lib/http/accept.d.ts.map +1 -1
  57. package/dist/lib/http/accept.js.map +1 -1
  58. package/dist/lib/http/method.d.ts +1 -1
  59. package/dist/lib/http/method.d.ts.map +1 -1
  60. package/dist/lib/http/middleware.d.ts +1 -1
  61. package/dist/lib/http/middleware.d.ts.map +1 -1
  62. package/dist/lib/http/request.d.ts +10 -1
  63. package/dist/lib/http/request.d.ts.map +1 -1
  64. package/dist/lib/http/request.js +40 -2
  65. package/dist/lib/http/request.js.map +1 -1
  66. package/dist/lib/http/response.d.ts +3 -2
  67. package/dist/lib/http/response.d.ts.map +1 -1
  68. package/dist/lib/http/response.js.map +1 -1
  69. package/dist/lib/http/route.d.ts +2 -1
  70. package/dist/lib/http/route.d.ts.map +1 -1
  71. package/dist/lib/http/route.js.map +1 -1
  72. package/dist/lib/http/router.d.ts +2 -1
  73. package/dist/lib/http/router.d.ts.map +1 -1
  74. package/dist/lib/http/router.js.map +1 -1
  75. package/dist/lib/http/stream.d.ts +1 -1
  76. package/dist/lib/http/stream.d.ts.map +1 -1
  77. package/dist/lib/http/stream.js +1 -1
  78. package/dist/lib/http/stream.js.map +1 -1
  79. package/dist/lib/http/types.d.ts +0 -1
  80. package/dist/lib/http/types.d.ts.map +1 -1
  81. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  82. package/dist/lib/util/authorization-header.js +1 -1
  83. package/dist/lib/util/authorization-header.js.map +1 -1
  84. package/dist/lib/util/function.d.ts +8 -0
  85. package/dist/lib/util/function.d.ts.map +1 -1
  86. package/dist/lib/util/function.js +1 -1
  87. package/dist/lib/util/function.js.map +1 -1
  88. package/dist/lib/util/hostname.d.ts.map +1 -1
  89. package/dist/lib/util/time.d.ts +7 -1
  90. package/dist/lib/util/time.d.ts.map +1 -1
  91. package/dist/lib/util/time.js +23 -12
  92. package/dist/lib/util/time.js.map +1 -1
  93. package/dist/metadata/build-metadata.d.ts.map +1 -1
  94. package/dist/metadata/build-metadata.js.map +1 -1
  95. package/dist/oauth-hooks.d.ts +56 -4
  96. package/dist/oauth-hooks.d.ts.map +1 -1
  97. package/dist/oauth-hooks.js +8 -0
  98. package/dist/oauth-hooks.js.map +1 -1
  99. package/dist/oauth-provider.d.ts +13 -10
  100. package/dist/oauth-provider.d.ts.map +1 -1
  101. package/dist/oauth-provider.js +36 -58
  102. package/dist/oauth-provider.js.map +1 -1
  103. package/dist/oauth-verifier.d.ts +1 -1
  104. package/dist/oauth-verifier.d.ts.map +1 -1
  105. package/dist/oauth-verifier.js.map +1 -1
  106. package/dist/output/build-authorize-data.d.ts.map +1 -1
  107. package/dist/output/build-authorize-data.js.map +1 -1
  108. package/dist/output/build-error-payload.d.ts.map +1 -1
  109. package/dist/output/build-error-payload.js +1 -1
  110. package/dist/output/build-error-payload.js.map +1 -1
  111. package/dist/output/output-manager.d.ts +1 -1
  112. package/dist/output/output-manager.d.ts.map +1 -1
  113. package/dist/output/output-manager.js.map +1 -1
  114. package/dist/output/send-authorize-redirect.d.ts +1 -1
  115. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  116. package/dist/output/send-authorize-redirect.js.map +1 -1
  117. package/dist/output/send-web-page.d.ts +1 -1
  118. package/dist/output/send-web-page.d.ts.map +1 -1
  119. package/dist/output/send-web-page.js.map +1 -1
  120. package/dist/replay/replay-store-redis.d.ts.map +1 -1
  121. package/dist/replay/replay-store-redis.js.map +1 -1
  122. package/dist/request/code.d.ts.map +1 -1
  123. package/dist/request/code.js.map +1 -1
  124. package/dist/request/request-data.d.ts.map +1 -1
  125. package/dist/request/request-data.js.map +1 -1
  126. package/dist/request/request-id.d.ts.map +1 -1
  127. package/dist/request/request-id.js.map +1 -1
  128. package/dist/request/request-info.d.ts +1 -1
  129. package/dist/request/request-info.d.ts.map +1 -1
  130. package/dist/request/request-manager.d.ts +2 -1
  131. package/dist/request/request-manager.d.ts.map +1 -1
  132. package/dist/request/request-manager.js +10 -2
  133. package/dist/request/request-manager.js.map +1 -1
  134. package/dist/request/request-store-memory.d.ts +1 -1
  135. package/dist/request/request-store-memory.d.ts.map +1 -1
  136. package/dist/request/request-store-redis.d.ts +1 -1
  137. package/dist/request/request-store-redis.d.ts.map +1 -1
  138. package/dist/request/request-store-redis.js.map +1 -1
  139. package/dist/request/request-uri.d.ts.map +1 -1
  140. package/dist/request/request-uri.js.map +1 -1
  141. package/dist/signer/signed-token-payload.d.ts +1 -1
  142. package/dist/signer/signed-token-payload.d.ts.map +1 -1
  143. package/dist/signer/signed-token-payload.js +2 -5
  144. package/dist/signer/signed-token-payload.js.map +1 -1
  145. package/dist/signer/signer.d.ts +1 -1
  146. package/dist/signer/signer.d.ts.map +1 -1
  147. package/dist/signer/signer.js.map +1 -1
  148. package/dist/token/refresh-token.d.ts.map +1 -1
  149. package/dist/token/refresh-token.js.map +1 -1
  150. package/dist/token/token-claims.d.ts +1 -1
  151. package/dist/token/token-claims.d.ts.map +1 -1
  152. package/dist/token/token-claims.js +2 -5
  153. package/dist/token/token-claims.js.map +1 -1
  154. package/dist/token/token-data.d.ts.map +1 -1
  155. package/dist/token/token-id.d.ts.map +1 -1
  156. package/dist/token/token-id.js.map +1 -1
  157. package/dist/token/token-manager.d.ts +3 -2
  158. package/dist/token/token-manager.d.ts.map +1 -1
  159. package/dist/token/token-manager.js +40 -25
  160. package/dist/token/token-manager.js.map +1 -1
  161. package/dist/token/verify-token-claims.d.ts.map +1 -1
  162. package/dist/token/verify-token-claims.js.map +1 -1
  163. package/package.json +13 -12
  164. package/rollup.config.js +4 -5
  165. package/src/account/account-store.ts +1 -2
  166. package/src/account/account.ts +1 -1
  167. package/src/assets/app/hooks/use-api.ts +1 -2
  168. package/src/assets/app/lib/api.ts +0 -1
  169. package/src/assets/assets-middleware.ts +0 -1
  170. package/src/assets/index.ts +2 -3
  171. package/src/client/client-auth.ts +1 -2
  172. package/src/client/client-manager.ts +22 -25
  173. package/src/client/client-store.ts +0 -1
  174. package/src/client/client-utils.ts +0 -1
  175. package/src/client/client.ts +13 -14
  176. package/src/device/device-data.ts +3 -3
  177. package/src/device/device-id.ts +0 -1
  178. package/src/device/device-manager.ts +94 -79
  179. package/src/device/session-id.ts +0 -1
  180. package/src/dpop/dpop-manager.ts +0 -2
  181. package/src/dpop/dpop-nonce.ts +0 -1
  182. package/src/errors/invalid-client-metadata-error.ts +1 -1
  183. package/src/errors/invalid-token-error.ts +1 -2
  184. package/src/errors/www-authenticate-error.ts +0 -1
  185. package/src/lib/http/accept.ts +2 -7
  186. package/src/lib/http/method.ts +1 -1
  187. package/src/lib/http/middleware.ts +1 -1
  188. package/src/lib/http/request.ts +66 -4
  189. package/src/lib/http/response.ts +3 -3
  190. package/src/lib/http/route.ts +2 -1
  191. package/src/lib/http/router.ts +2 -1
  192. package/src/lib/http/stream.ts +4 -5
  193. package/src/lib/http/types.ts +0 -1
  194. package/src/lib/util/authorization-header.ts +1 -2
  195. package/src/lib/util/function.ts +19 -2
  196. package/src/lib/util/hostname.ts +1 -1
  197. package/src/lib/util/time.ts +35 -18
  198. package/src/metadata/build-metadata.ts +0 -1
  199. package/src/oauth-hooks.ts +73 -14
  200. package/src/oauth-provider.ts +77 -37
  201. package/src/oauth-verifier.ts +1 -2
  202. package/src/output/build-authorize-data.ts +0 -1
  203. package/src/output/build-error-payload.ts +1 -2
  204. package/src/output/output-manager.ts +3 -4
  205. package/src/output/send-authorize-redirect.ts +1 -2
  206. package/src/output/send-web-page.ts +3 -4
  207. package/src/replay/replay-manager.ts +1 -1
  208. package/src/replay/replay-store-redis.ts +0 -1
  209. package/src/request/code.ts +0 -1
  210. package/src/request/request-data.ts +0 -1
  211. package/src/request/request-id.ts +0 -1
  212. package/src/request/request-info.ts +1 -1
  213. package/src/request/request-manager.ts +16 -6
  214. package/src/request/request-store-memory.ts +1 -1
  215. package/src/request/request-store-redis.ts +1 -2
  216. package/src/request/request-uri.ts +0 -1
  217. package/src/signer/signed-token-payload.ts +1 -2
  218. package/src/signer/signer.ts +1 -2
  219. package/src/token/refresh-token.ts +0 -1
  220. package/src/token/token-claims.ts +1 -2
  221. package/src/token/token-data.ts +0 -1
  222. package/src/token/token-id.ts +0 -1
  223. package/src/token/token-manager.ts +53 -25
  224. package/src/token/verify-token-claims.ts +0 -1
  225. package/tsconfig.backend.tsbuildinfo +1 -1
  226. package/dist/device/device-details.d.ts +0 -16
  227. package/dist/device/device-details.d.ts.map +0 -1
  228. package/dist/device/device-details.js +0 -34
  229. package/dist/device/device-details.js.map +0 -1
  230. package/src/device/device-details.ts +0 -43
@@ -1,6 +1,18 @@
1
+ import { Jwks, Keyset, jwksSchema } from '@atproto/jwk'
2
+ import {
3
+ OAuthAuthorizationServerMetadata,
4
+ OAuthClientIdDiscoverable,
5
+ OAuthClientIdLoopback,
6
+ OAuthClientMetadata,
7
+ OAuthClientMetadataInput,
8
+ isLoopbackHost,
9
+ isOAuthClientIdDiscoverable,
10
+ isOAuthClientIdLoopback,
11
+ oauthClientMetadataSchema,
12
+ } from '@atproto/oauth-types'
1
13
  import {
2
- bindFetch,
3
14
  Fetch,
15
+ bindFetch,
4
16
  fetchJsonProcessor,
5
17
  fetchJsonZodProcessor,
6
18
  fetchOkProcessor,
@@ -11,19 +23,6 @@ import {
11
23
  GetCachedOptions,
12
24
  SimpleStore,
13
25
  } from '@atproto-labs/simple-store'
14
- import { Jwks, jwksSchema, Keyset } from '@atproto/jwk'
15
- import {
16
- isLoopbackHost,
17
- isOAuthClientIdDiscoverable,
18
- isOAuthClientIdLoopback,
19
- OAuthAuthorizationServerMetadata,
20
- OAuthClientIdDiscoverable,
21
- OAuthClientIdLoopback,
22
- OAuthClientMetadata,
23
- OAuthClientMetadataInput,
24
- oauthClientMetadataSchema,
25
- } from '@atproto/oauth-types'
26
-
27
26
  import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
28
27
  import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
29
28
  import { callAsync } from '../lib/util/function.js'
@@ -111,17 +110,15 @@ export class ClientManager {
111
110
  })
112
111
  : undefined
113
112
 
114
- const partialInfo = this.hooks.onClientInfo
115
- ? await callAsync(this.hooks.onClientInfo, clientId, {
116
- metadata,
117
- jwks,
118
- }).catch((err) => {
119
- throw InvalidClientMetadataError.from(
120
- err,
121
- `Rejected client information for "${clientId}"`,
122
- )
123
- })
124
- : undefined
113
+ const partialInfo = await callAsync(this.hooks.getClientInfo, clientId, {
114
+ metadata,
115
+ jwks,
116
+ }).catch((err) => {
117
+ throw InvalidClientMetadataError.from(
118
+ err,
119
+ `Rejected client information for "${clientId}"`,
120
+ )
121
+ })
125
122
 
126
123
  const isFirstParty = partialInfo?.isFirstParty ?? false
127
124
  const isTrusted = partialInfo?.isTrusted ?? isFirstParty
@@ -1,5 +1,4 @@
1
1
  import { OAuthClientMetadata } from '@atproto/oauth-types'
2
-
3
2
  import { Awaitable } from '../lib/util/type.js'
4
3
  import { ClientId } from './client-id.js'
5
4
 
@@ -2,7 +2,6 @@ import {
2
2
  OAuthClientIdDiscoverable,
3
3
  parseOAuthDiscoverableClientId,
4
4
  } from '@atproto/oauth-types'
5
-
6
5
  import { InvalidClientIdError } from '../errors/invalid-client-id-error.js'
7
6
  import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
8
7
  import { isInternetHost } from '../lib/util/hostname.js'
@@ -1,26 +1,25 @@
1
- import { Jwks } from '@atproto/jwk'
2
- import {
3
- CLIENT_ASSERTION_TYPE_JWT_BEARER,
4
- OAuthAuthorizationRequestParameters,
5
- OAuthClientCredentials,
6
- OAuthClientMetadata,
7
- OAuthRedirectUri,
8
- } from '@atproto/oauth-types'
9
1
  import {
10
- UnsecuredJWT,
11
- createLocalJWKSet,
12
- createRemoteJWKSet,
13
- errors,
14
- jwtVerify,
15
2
  type JWTPayload,
16
3
  type JWTVerifyGetKey,
17
4
  type JWTVerifyOptions,
18
5
  type JWTVerifyResult,
19
6
  type KeyLike,
20
7
  type ResolvedKey,
8
+ UnsecuredJWT,
21
9
  type UnsecuredResult,
10
+ createLocalJWKSet,
11
+ createRemoteJWKSet,
12
+ errors,
13
+ jwtVerify,
22
14
  } from 'jose'
23
-
15
+ import { Jwks } from '@atproto/jwk'
16
+ import {
17
+ CLIENT_ASSERTION_TYPE_JWT_BEARER,
18
+ OAuthAuthorizationRequestParameters,
19
+ OAuthClientCredentials,
20
+ OAuthClientMetadata,
21
+ OAuthRedirectUri,
22
+ } from '@atproto/oauth-types'
24
23
  import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
25
24
  import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
26
25
  import { InvalidClientError } from '../errors/invalid-client-error.js'
@@ -1,11 +1,11 @@
1
1
  import { z } from 'zod'
2
-
3
- import { deviceDetailsSchema } from './device-details.js'
4
2
  import { sessionIdSchema } from './session-id.js'
5
3
 
6
- export const deviceDataSchema = deviceDetailsSchema.extend({
4
+ export const deviceDataSchema = z.object({
7
5
  sessionId: sessionIdSchema,
8
6
  lastSeenAt: z.date(),
7
+ userAgent: z.string().nullable(),
8
+ ipAddress: z.string(),
9
9
  })
10
10
 
11
11
  export type DeviceData = z.infer<typeof deviceDataSchema>
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod'
2
-
3
2
  import { DEVICE_ID_BYTES_LENGTH, DEVICE_ID_PREFIX } from '../constants.js'
4
3
  import { randomHexId } from '../lib/util/crypto.js'
5
4
 
@@ -1,87 +1,89 @@
1
- import { IncomingMessage, ServerResponse } from 'node:http'
2
-
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
3
2
  import { serialize as serializeCookie } from 'cookie'
4
- import type Keygrip from 'keygrip'
5
3
  import { z } from 'zod'
6
-
7
- import { appendHeader, parseHttpCookies } from '../lib/http/index.js'
8
-
9
4
  import { SESSION_FIXATION_MAX_AGE } from '../constants.js'
5
+ import { appendHeader, parseHttpCookies } from '../lib/http/index.js'
6
+ import { RequestMetadata, extractRequestMetadata } from '../lib/http/request.js'
10
7
  import { DeviceData } from './device-data.js'
11
- import { extractDeviceDetails } from './device-details.js'
12
8
  import { DeviceId, deviceIdSchema, generateDeviceId } from './device-id.js'
13
9
  import { DeviceStore } from './device-store.js'
14
10
  import { generateSessionId, sessionIdSchema } from './session-id.js'
15
11
 
16
- export const DEFAULT_OPTIONS = {
12
+ /**
13
+ * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}
14
+ */
15
+ export const keygripSchema = z.object({
16
+ sign: z.function().args(z.any()).returns(z.string()),
17
+ verify: z.function().args(z.any(), z.string()).returns(z.boolean()),
18
+ index: z.function().args(z.any(), z.string()).returns(z.number()),
19
+ })
20
+
21
+ export const deviceManagerOptionsSchema = z.object({
17
22
  /**
18
23
  * Controls whether the IP address is read from the `X-Forwarded-For` header
19
24
  * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).
20
25
  *
21
26
  * @default true // (nowadays, most requests are proxied)
22
27
  */
23
- trustProxy: true,
24
-
28
+ trustProxy: z.boolean().default(true),
25
29
  /**
26
30
  * Amount of time (in ms) after which session IDs will be rotated
27
31
  *
28
32
  * @default 300e3 // (5 minutes)
29
33
  */
30
- rotationRate: 5 * 60e3,
31
-
34
+ rotationRate: z.number().default(300e3),
32
35
  /**
33
36
  * Cookie options
34
37
  */
35
- cookie: {
36
- keys: undefined as undefined | Keygrip,
37
-
38
- /**
39
- * Name of the cookie used to identify the device
40
- *
41
- * @default 'session-id'
42
- */
43
- device: 'device-id',
44
-
45
- /**
46
- * Name of the cookie used to identify the session
47
- *
48
- * @default 'session-id'
49
- */
50
- session: 'session-id',
51
-
52
- /**
53
- * Url path for the cookie
54
- *
55
- * @default '/oauth/authorize'
56
- */
57
- path: '/oauth/authorize',
58
-
59
- /**
60
- * Amount of time (in ms) after which the session cookie will expire.
61
- * If set to `null`, the cookie will be a session cookie (deleted when the
62
- * browser is closed).
63
- *
64
- * @default 10 * 365.2 * 24 * 60 * 60e3 // 10 years (in ms)
65
- */
66
- age: <number | null>(10 * 365.2 * 24 * 60 * 60e3),
67
-
68
- /**
69
- * Controls whether the cookie is only sent over HTTPS (if `true`), or also
70
- * over HTTP (if `false`). This should **NOT** be set to `false` in
71
- * production.
72
- */
73
- secure: true,
74
-
75
- /**
76
- * Controls whether the cookie is sent along with cross-site requests.
77
- *
78
- * @default 'lax'
79
- */
80
- sameSite: 'lax' as 'lax' | 'strict',
81
- },
82
- }
38
+ cookie: z
39
+ .object({
40
+ keys: keygripSchema.optional(),
41
+ /**
42
+ * Name of the cookie used to identify the device
43
+ *
44
+ * @default 'session-id'
45
+ */
46
+ device: z.string().default('device-id'),
47
+ /**
48
+ * Name of the cookie used to identify the session
49
+ *
50
+ * @default 'session-id'
51
+ */
52
+ session: z.string().default('session-id'),
53
+ /**
54
+ * Url path for the cookie
55
+ *
56
+ * @default '/oauth/authorize'
57
+ */
58
+ path: z.string().default('/oauth/authorize'),
59
+ /**
60
+ * Amount of time (in ms) after which the session cookie will expire.
61
+ * If set to `null`, the cookie will be a session cookie (deleted when the
62
+ * browser is closed).
63
+ *
64
+ * @default 10 years
65
+ */
66
+ age: z
67
+ .number()
68
+ .nullable()
69
+ .default(10 * 365.2 * 24 * 60 * 60e3),
70
+ /**
71
+ * Controls whether the cookie is only sent over HTTPS (if `true`), or also
72
+ * over HTTP (if `false`). This should **NOT** be set to `false` in
73
+ * production.
74
+ */
75
+ secure: z.boolean().default(true),
76
+ /**
77
+ * Controls whether the cookie is sent along with cross-site requests.
78
+ *
79
+ * @default 'lax'
80
+ */
81
+ sameSite: z.enum(['lax', 'strict']).default('lax'),
82
+ })
83
+ .default({}),
84
+ })
83
85
 
84
- export type DeviceDeviceManagerOptions = typeof DEFAULT_OPTIONS
86
+ export type DeviceManagerOptions = z.input<typeof deviceManagerOptionsSchema>
85
87
 
86
88
  const cookieValueSchema = z.tuple([deviceIdSchema, sessionIdSchema])
87
89
  type CookieValue = z.infer<typeof cookieValueSchema>
@@ -92,16 +94,23 @@ type CookieValue = z.infer<typeof cookieValueSchema>
92
94
  * identify the session.
93
95
  */
94
96
  export class DeviceManager {
97
+ private readonly options: z.infer<typeof deviceManagerOptionsSchema>
98
+
95
99
  constructor(
96
100
  private readonly store: DeviceStore,
97
- private readonly options: DeviceDeviceManagerOptions = DEFAULT_OPTIONS,
98
- ) {}
101
+ options: DeviceManagerOptions = {},
102
+ ) {
103
+ this.options = deviceManagerOptionsSchema.parse(options)
104
+ }
99
105
 
100
106
  public async load(
101
107
  req: IncomingMessage,
102
108
  res: ServerResponse,
103
109
  forceRotate = false,
104
- ): Promise<{ deviceId: DeviceId }> {
110
+ ): Promise<{
111
+ deviceId: DeviceId
112
+ deviceMetadata: RequestMetadata
113
+ }> {
105
114
  const cookie = await this.getCookie(req)
106
115
  if (cookie) {
107
116
  return this.refresh(
@@ -118,8 +127,11 @@ export class DeviceManager {
118
127
  private async create(
119
128
  req: IncomingMessage,
120
129
  res: ServerResponse,
121
- ): Promise<{ deviceId: DeviceId }> {
122
- const { userAgent, ipAddress } = this.getDeviceDetails(req)
130
+ ): Promise<{
131
+ deviceId: DeviceId
132
+ deviceMetadata: RequestMetadata
133
+ }> {
134
+ const deviceMetadata = this.getRequestMetadata(req)
123
135
 
124
136
  const [deviceId, sessionId] = await Promise.all([
125
137
  generateDeviceId(),
@@ -129,13 +141,13 @@ export class DeviceManager {
129
141
  await this.store.createDevice(deviceId, {
130
142
  sessionId,
131
143
  lastSeenAt: new Date(),
132
- userAgent,
133
- ipAddress,
144
+ userAgent: deviceMetadata.userAgent,
145
+ ipAddress: deviceMetadata.ipAddress,
134
146
  })
135
147
 
136
148
  this.setCookie(res, [deviceId, sessionId])
137
149
 
138
- return { deviceId }
150
+ return { deviceId, deviceMetadata }
139
151
  }
140
152
 
141
153
  private async refresh(
@@ -143,7 +155,10 @@ export class DeviceManager {
143
155
  res: ServerResponse,
144
156
  [deviceId, sessionId]: CookieValue,
145
157
  forceRotate = false,
146
- ): Promise<{ deviceId: DeviceId }> {
158
+ ): Promise<{
159
+ deviceId: DeviceId
160
+ deviceMetadata: RequestMetadata
161
+ }> {
147
162
  const data = await this.store.readDevice(deviceId)
148
163
  if (!data) return this.create(req, res)
149
164
 
@@ -162,24 +177,24 @@ export class DeviceManager {
162
177
  }
163
178
  }
164
179
 
165
- const details = this.getDeviceDetails(req)
180
+ const deviceMetadata = this.getRequestMetadata(req)
166
181
 
167
182
  if (
168
183
  forceRotate ||
169
- details.ipAddress !== data.ipAddress ||
170
- details.userAgent !== data.userAgent ||
184
+ deviceMetadata.ipAddress !== data.ipAddress ||
185
+ deviceMetadata.userAgent !== data.userAgent ||
171
186
  age > this.options.rotationRate
172
187
  ) {
173
188
  await this.rotate(req, res, deviceId, {
174
- ipAddress: details.ipAddress,
175
- userAgent: details.userAgent || data.userAgent,
189
+ ipAddress: deviceMetadata.ipAddress,
190
+ userAgent: deviceMetadata.userAgent || data.userAgent,
176
191
  })
177
192
  }
178
193
 
179
- return { deviceId }
194
+ return { deviceId, deviceMetadata }
180
195
  }
181
196
 
182
- public async rotate(
197
+ private async rotate(
183
198
  req: IncomingMessage,
184
199
  res: ServerResponse,
185
200
  deviceId: DeviceId,
@@ -287,7 +302,7 @@ export class DeviceManager {
287
302
  }
288
303
  }
289
304
 
290
- private getDeviceDetails(req: IncomingMessage) {
291
- return extractDeviceDetails(req, this.options.trustProxy)
305
+ public getRequestMetadata(req: IncomingMessage) {
306
+ return extractRequestMetadata(req, this.options)
292
307
  }
293
308
  }
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod'
2
-
3
2
  import { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js'
4
3
  import { randomHexId } from '../lib/util/crypto.js'
5
4
 
@@ -1,7 +1,5 @@
1
1
  import { createHash } from 'node:crypto'
2
-
3
2
  import { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'
4
-
5
3
  import { DPOP_NONCE_MAX_AGE } from '../constants.js'
6
4
  import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'
7
5
  import { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'
@@ -1,5 +1,4 @@
1
1
  import { createHmac, randomBytes } from 'node:crypto'
2
-
3
2
  import { DPOP_NONCE_MAX_AGE } from '../constants.js'
4
3
 
5
4
  function numTo64bits(num: number) {
@@ -1,5 +1,5 @@
1
- import { FetchError } from '@atproto-labs/fetch'
2
1
  import { ZodError } from 'zod'
2
+ import { FetchError } from '@atproto-labs/fetch'
3
3
  import { OAuthError } from './oauth-error.js'
4
4
 
5
5
  /**
@@ -1,7 +1,6 @@
1
- import { JwtVerifyError } from '@atproto/jwk'
2
1
  import { errors } from 'jose'
3
2
  import { ZodError } from 'zod'
4
-
3
+ import { JwtVerifyError } from '@atproto/jwk'
5
4
  import { OAuthError } from './oauth-error.js'
6
5
  import { WWWAuthenticateError } from './www-authenticate-error.js'
7
6
 
@@ -1,5 +1,4 @@
1
1
  import { VERIFY_ALGOS } from '../lib/util/crypto.js'
2
-
3
2
  import { OAuthError } from './oauth-error.js'
4
3
 
5
4
  export type WWWAuthenticateParams = Record<string, string | undefined>
@@ -1,12 +1,7 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
1
2
  import { mediaType } from '@hapi/accept'
2
-
3
3
  import { SubCtx, subCtx } from './context.js'
4
- import {
5
- IncomingMessage,
6
- Middleware,
7
- NextFunction,
8
- ServerResponse,
9
- } from './types.js'
4
+ import { Middleware, NextFunction } from './types.js'
10
5
 
11
6
  type View<
12
7
  T,
@@ -1,4 +1,4 @@
1
- import { IncomingMessage } from './types.js'
1
+ import type { IncomingMessage } from 'node:http'
2
2
 
3
3
  export type MethodMatcherInput = string | Iterable<string> | MethodMatcher
4
4
  export type MethodMatcher = (req: IncomingMessage) => boolean
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
2
  import { writeJson } from './response.js'
3
- import { Middleware, Handler, NextFunction } from './types.js'
3
+ import { Handler, Middleware, NextFunction } from './types.js'
4
4
 
5
5
  export function combineMiddlewares<M extends Middleware<any, any, any>>(
6
6
  middlewares: Iterable<null | undefined | M>,
@@ -1,10 +1,9 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import type { IncomingMessage, ServerResponse } from 'node:http'
1
3
  import { parse as parseCookie, serialize as serializeCookie } from 'cookie'
2
- import { randomBytes } from 'crypto'
3
4
  import createHttpError from 'http-errors'
4
-
5
5
  import { appendHeader } from './response.js'
6
- import { IncomingMessage, ServerResponse } from './types.js'
7
- import { urlMatch, UrlReference } from './url.js'
6
+ import { UrlReference, urlMatch } from './url.js'
8
7
 
9
8
  export function validateHeaderValue(
10
9
  req: IncomingMessage,
@@ -163,3 +162,66 @@ export function parseHttpCookies(
163
162
  ? ((req as any).cookies = parseCookie(req.headers['cookie']))
164
163
  : null
165
164
  }
165
+
166
+ export type ExtractRequestMetadataOptions = {
167
+ trustProxy?: boolean
168
+ }
169
+
170
+ export type RequestMetadata = {
171
+ userAgent: string | null
172
+ ipAddress: string
173
+ port: number
174
+ }
175
+
176
+ export function extractRequestMetadata(
177
+ req: IncomingMessage,
178
+ options?: ExtractRequestMetadataOptions,
179
+ ): RequestMetadata {
180
+ const userAgent = req.headers['user-agent'] || null
181
+ const ipAddress = extractIpAddress(req, options) || null
182
+ const port = extractPort(req, options)
183
+
184
+ if (ipAddress == null || port == null) {
185
+ throw new Error('Could not determine IP address')
186
+ }
187
+
188
+ return { userAgent, ipAddress, port }
189
+ }
190
+
191
+ function extractIpAddress(
192
+ req: IncomingMessage,
193
+ options?: ExtractRequestMetadataOptions,
194
+ ): string | undefined {
195
+ // Express app compatibility
196
+ if ('ip' in req && typeof req.ip === 'string') {
197
+ return req.ip
198
+ }
199
+
200
+ if (options?.trustProxy) {
201
+ const forwardedFor = req.headers['x-forwarded-for']
202
+ if (typeof forwardedFor === 'string') {
203
+ const firstForward = forwardedFor.split(',')[0]!.trim()
204
+ if (firstForward) return firstForward
205
+ }
206
+ }
207
+
208
+ return req.socket.remoteAddress
209
+ }
210
+
211
+ function extractPort(
212
+ req: IncomingMessage,
213
+ options?: ExtractRequestMetadataOptions,
214
+ ): number | undefined {
215
+ if (options?.trustProxy) {
216
+ const forwardedPort = req.headers['x-forwarded-port']
217
+ if (typeof forwardedPort === 'string') {
218
+ const port = Number(forwardedPort.trim())
219
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
220
+ throw new Error('Invalid forwarded port')
221
+ }
222
+ return port
223
+ }
224
+ }
225
+
226
+ return req.socket.remotePort
227
+ }
@@ -1,6 +1,6 @@
1
- import { Readable, pipeline } from 'node:stream'
2
-
3
- import { Handler, ServerResponse } from './types.js'
1
+ import type { ServerResponse } from 'node:http'
2
+ import { type Readable, pipeline } from 'node:stream'
3
+ import { Handler } from './types.js'
4
4
 
5
5
  export function appendHeader(
6
6
  res: ServerResponse,
@@ -1,8 +1,9 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
1
2
  import { SubCtx, subCtx } from './context.js'
2
3
  import { MethodMatcherInput, createMethodMatcher } from './method.js'
3
4
  import { combineMiddlewares } from './middleware.js'
4
5
  import { Params, Path, createPathMatcher } from './path.js'
5
- import { IncomingMessage, Middleware, ServerResponse } from './types.js'
6
+ import { Middleware } from './types.js'
6
7
 
7
8
  export type RouteCtx<T, P extends Params> = SubCtx<T, { params: Readonly<P> }>
8
9
  export type RouteMiddleware<
@@ -1,9 +1,10 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
1
2
  import { SubCtx, subCtx } from './context.js'
2
3
  import { MethodMatcherInput } from './method.js'
3
4
  import { asHandler, combineMiddlewares } from './middleware.js'
4
5
  import { Params, Path } from './path.js'
5
6
  import { RouteMiddleware, createRoute } from './route.js'
6
- import { IncomingMessage, Middleware, ServerResponse } from './types.js'
7
+ import { Middleware } from './types.js'
7
8
 
8
9
  export type RouterCtx<T> = SubCtx<T, { url: Readonly<URL> }>
9
10
  export type RouterMiddleware<
@@ -1,13 +1,12 @@
1
- import { decodeStream, streamToNodeBuffer } from '@atproto/common'
2
- import createHttpError from 'http-errors'
3
- import { IncomingMessage } from 'node:http'
1
+ import type { IncomingMessage } from 'node:http'
4
2
  import { Readable } from 'node:stream'
5
-
3
+ import createHttpError from 'http-errors'
4
+ import { decodeStream, streamToNodeBuffer } from '@atproto/common'
6
5
  import {
7
6
  KnownNames,
8
7
  KnownParser,
9
- parseContentType,
10
8
  ParserResult,
9
+ parseContentType,
11
10
  parsers,
12
11
  } from './parser.js'
13
12
 
@@ -1,5 +1,4 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http'
2
- export { IncomingMessage, ServerResponse }
3
2
 
4
3
  export type NextFunction = (err?: unknown) => void
5
4
 
@@ -1,9 +1,8 @@
1
+ import { z } from 'zod'
1
2
  import {
2
3
  oauthAccessTokenSchema,
3
4
  oauthTokenTypeSchema,
4
5
  } from '@atproto/oauth-types'
5
- import { z } from 'zod'
6
-
7
6
  import { InvalidRequestError } from '../../errors/invalid-request-error.js'
8
7
  import { WWWAuthenticateError } from '../../errors/www-authenticate-error.js'
9
8