@atproto/oauth-provider 0.2.16 → 0.3.0

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 (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