@atproto/oauth-provider 0.16.3 → 0.17.0-next.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 (321) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/access-token/access-token-mode.js +2 -5
  3. package/dist/access-token/access-token-mode.js.map +1 -1
  4. package/dist/account/account-manager.js +25 -33
  5. package/dist/account/account-manager.js.map +1 -1
  6. package/dist/account/account-store.js +11 -32
  7. package/dist/account/account-store.js.map +1 -1
  8. package/dist/account/sign-in-data.js +9 -12
  9. package/dist/account/sign-in-data.js.map +1 -1
  10. package/dist/account/sign-up-input.js +14 -17
  11. package/dist/account/sign-up-input.js.map +1 -1
  12. package/dist/client/client-auth.js +1 -2
  13. package/dist/client/client-data.js +1 -2
  14. package/dist/client/client-id.js +2 -5
  15. package/dist/client/client-id.js.map +1 -1
  16. package/dist/client/client-info.js +1 -2
  17. package/dist/client/client-manager.js +86 -97
  18. package/dist/client/client-manager.js.map +1 -1
  19. package/dist/client/client-store.js +7 -26
  20. package/dist/client/client-store.js.map +1 -1
  21. package/dist/client/client-utils.js +10 -14
  22. package/dist/client/client-utils.js.map +1 -1
  23. package/dist/client/client.js +43 -53
  24. package/dist/client/client.js.map +1 -1
  25. package/dist/constants.js +28 -31
  26. package/dist/constants.js.map +1 -1
  27. package/dist/customization/branding.js +8 -11
  28. package/dist/customization/branding.js.map +1 -1
  29. package/dist/customization/build-customization-css.js +8 -11
  30. package/dist/customization/build-customization-css.js.map +1 -1
  31. package/dist/customization/build-customization-data.js +1 -4
  32. package/dist/customization/build-customization-data.js.map +1 -1
  33. package/dist/customization/colors.js +11 -14
  34. package/dist/customization/colors.js.map +1 -1
  35. package/dist/customization/customization.js +8 -11
  36. package/dist/customization/customization.js.map +1 -1
  37. package/dist/customization/links.js +7 -10
  38. package/dist/customization/links.js.map +1 -1
  39. package/dist/device/device-data.js +7 -10
  40. package/dist/device/device-data.js.map +1 -1
  41. package/dist/device/device-id.js +11 -16
  42. package/dist/device/device-id.js.map +1 -1
  43. package/dist/device/device-manager.js +32 -38
  44. package/dist/device/device-manager.js.map +1 -1
  45. package/dist/device/device-store.js +7 -25
  46. package/dist/device/device-store.js.map +1 -1
  47. package/dist/device/session-id.js +9 -13
  48. package/dist/device/session-id.js.map +1 -1
  49. package/dist/dpop/dpop-manager.d.ts +3 -3
  50. package/dist/dpop/dpop-manager.js +38 -43
  51. package/dist/dpop/dpop-manager.js.map +1 -1
  52. package/dist/dpop/dpop-nonce.d.ts +2 -2
  53. package/dist/dpop/dpop-nonce.d.ts.map +1 -1
  54. package/dist/dpop/dpop-nonce.js +14 -18
  55. package/dist/dpop/dpop-nonce.js.map +1 -1
  56. package/dist/dpop/dpop-proof.js +1 -2
  57. package/dist/errors/access-denied-error.js +2 -6
  58. package/dist/errors/access-denied-error.js.map +1 -1
  59. package/dist/errors/account-selection-required-error.js +2 -6
  60. package/dist/errors/account-selection-required-error.js.map +1 -1
  61. package/dist/errors/authorization-error.js +7 -12
  62. package/dist/errors/authorization-error.js.map +1 -1
  63. package/dist/errors/consent-required-error.js +2 -6
  64. package/dist/errors/consent-required-error.js.map +1 -1
  65. package/dist/errors/error-parser.js +14 -18
  66. package/dist/errors/error-parser.js.map +1 -1
  67. package/dist/errors/handle-unavailable-error.js +2 -7
  68. package/dist/errors/handle-unavailable-error.js.map +1 -1
  69. package/dist/errors/invalid-authorization-details-error.js +2 -6
  70. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  71. package/dist/errors/invalid-client-error.js +2 -6
  72. package/dist/errors/invalid-client-error.js.map +1 -1
  73. package/dist/errors/invalid-client-id-error.js +2 -6
  74. package/dist/errors/invalid-client-id-error.js.map +1 -1
  75. package/dist/errors/invalid-client-metadata-error.js +7 -11
  76. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  77. package/dist/errors/invalid-credentials-error.js +2 -7
  78. package/dist/errors/invalid-credentials-error.js.map +1 -1
  79. package/dist/errors/invalid-dpop-key-binding-error.js +2 -6
  80. package/dist/errors/invalid-dpop-key-binding-error.js.map +1 -1
  81. package/dist/errors/invalid-dpop-proof-error.js +2 -6
  82. package/dist/errors/invalid-dpop-proof-error.js.map +1 -1
  83. package/dist/errors/invalid-grant-error.js +2 -6
  84. package/dist/errors/invalid-grant-error.js.map +1 -1
  85. package/dist/errors/invalid-invite-code-error.d.ts +1 -1
  86. package/dist/errors/invalid-invite-code-error.d.ts.map +1 -1
  87. package/dist/errors/invalid-invite-code-error.js +2 -6
  88. package/dist/errors/invalid-invite-code-error.js.map +1 -1
  89. package/dist/errors/invalid-redirect-uri-error.js +2 -6
  90. package/dist/errors/invalid-redirect-uri-error.js.map +1 -1
  91. package/dist/errors/invalid-request-error.js +3 -7
  92. package/dist/errors/invalid-request-error.js.map +1 -1
  93. package/dist/errors/invalid-scope-error.js +2 -6
  94. package/dist/errors/invalid-scope-error.js.map +1 -1
  95. package/dist/errors/invalid-token-error.js +10 -15
  96. package/dist/errors/invalid-token-error.js.map +1 -1
  97. package/dist/errors/login-required-error.js +2 -6
  98. package/dist/errors/login-required-error.js.map +1 -1
  99. package/dist/errors/oauth-error.js +1 -9
  100. package/dist/errors/oauth-error.js.map +1 -1
  101. package/dist/errors/second-authentication-factor-required-error.js +2 -8
  102. package/dist/errors/second-authentication-factor-required-error.js.map +1 -1
  103. package/dist/errors/unauthorized-client-error.js +2 -6
  104. package/dist/errors/unauthorized-client-error.js.map +1 -1
  105. package/dist/errors/use-dpop-nonce-error.js +4 -8
  106. package/dist/errors/use-dpop-nonce-error.js.map +1 -1
  107. package/dist/errors/www-authenticate-error.js +4 -9
  108. package/dist/errors/www-authenticate-error.js.map +1 -1
  109. package/dist/index.js +14 -30
  110. package/dist/index.js.map +1 -1
  111. package/dist/lexicon/lexicon-data.js +1 -2
  112. package/dist/lexicon/lexicon-getter.js +6 -10
  113. package/dist/lexicon/lexicon-getter.js.map +1 -1
  114. package/dist/lexicon/lexicon-manager.js +10 -30
  115. package/dist/lexicon/lexicon-manager.js.map +1 -1
  116. package/dist/lexicon/lexicon-store.js +5 -10
  117. package/dist/lexicon/lexicon-store.js.map +1 -1
  118. package/dist/lib/csp/index.js +3 -8
  119. package/dist/lib/csp/index.js.map +1 -1
  120. package/dist/lib/hcaptcha.js +33 -43
  121. package/dist/lib/hcaptcha.js.map +1 -1
  122. package/dist/lib/html/build-document.js +19 -24
  123. package/dist/lib/html/build-document.js.map +1 -1
  124. package/dist/lib/html/escapers.js +10 -16
  125. package/dist/lib/html/escapers.js.map +1 -1
  126. package/dist/lib/html/html.js +1 -5
  127. package/dist/lib/html/html.js.map +1 -1
  128. package/dist/lib/html/hydration-data.js +6 -10
  129. package/dist/lib/html/hydration-data.js.map +1 -1
  130. package/dist/lib/html/index.js +3 -19
  131. package/dist/lib/html/index.js.map +1 -1
  132. package/dist/lib/html/tags.js +14 -23
  133. package/dist/lib/html/tags.js.map +1 -1
  134. package/dist/lib/html/util.js +1 -4
  135. package/dist/lib/html/util.js.map +1 -1
  136. package/dist/lib/http/accept.d.ts.map +1 -1
  137. package/dist/lib/http/accept.js +8 -8
  138. package/dist/lib/http/accept.js.map +1 -1
  139. package/dist/lib/http/context.js +1 -4
  140. package/dist/lib/http/context.js.map +1 -1
  141. package/dist/lib/http/headers.js +1 -4
  142. package/dist/lib/http/headers.js.map +1 -1
  143. package/dist/lib/http/index.js +10 -26
  144. package/dist/lib/http/index.js.map +1 -1
  145. package/dist/lib/http/method.js +1 -4
  146. package/dist/lib/http/method.js.map +1 -1
  147. package/dist/lib/http/middleware.js +11 -17
  148. package/dist/lib/http/middleware.js.map +1 -1
  149. package/dist/lib/http/parser.js +13 -20
  150. package/dist/lib/http/parser.js.map +1 -1
  151. package/dist/lib/http/path.js +1 -4
  152. package/dist/lib/http/path.js.map +1 -1
  153. package/dist/lib/http/request.d.ts.map +1 -1
  154. package/dist/lib/http/request.js +32 -47
  155. package/dist/lib/http/request.js.map +1 -1
  156. package/dist/lib/http/response.js +14 -27
  157. package/dist/lib/http/response.js.map +1 -1
  158. package/dist/lib/http/route.js +9 -12
  159. package/dist/lib/http/route.js.map +1 -1
  160. package/dist/lib/http/router.js +8 -13
  161. package/dist/lib/http/router.js.map +1 -1
  162. package/dist/lib/http/security-headers.js +10 -15
  163. package/dist/lib/http/security-headers.js.map +1 -1
  164. package/dist/lib/http/stream.js +12 -20
  165. package/dist/lib/http/stream.js.map +1 -1
  166. package/dist/lib/http/types.js +1 -2
  167. package/dist/lib/http/url.js +1 -4
  168. package/dist/lib/http/url.js.map +1 -1
  169. package/dist/lib/nsid.js +4 -8
  170. package/dist/lib/nsid.js.map +1 -1
  171. package/dist/lib/redis.js +4 -7
  172. package/dist/lib/redis.js.map +1 -1
  173. package/dist/lib/util/authorization-header.js +11 -15
  174. package/dist/lib/util/authorization-header.js.map +1 -1
  175. package/dist/lib/util/cast.js +3 -8
  176. package/dist/lib/util/cast.js.map +1 -1
  177. package/dist/lib/util/color.js +23 -32
  178. package/dist/lib/util/color.js.map +1 -1
  179. package/dist/lib/util/crypto.js +5 -10
  180. package/dist/lib/util/crypto.js.map +1 -1
  181. package/dist/lib/util/date.js +2 -6
  182. package/dist/lib/util/date.js.map +1 -1
  183. package/dist/lib/util/error.js +5 -8
  184. package/dist/lib/util/error.js.map +1 -1
  185. package/dist/lib/util/function.js +3 -8
  186. package/dist/lib/util/function.js.map +1 -1
  187. package/dist/lib/util/locale.js +3 -6
  188. package/dist/lib/util/locale.js.map +1 -1
  189. package/dist/lib/util/object.js +1 -4
  190. package/dist/lib/util/object.js.map +1 -1
  191. package/dist/lib/util/redirect-uri.js +3 -6
  192. package/dist/lib/util/redirect-uri.js.map +1 -1
  193. package/dist/lib/util/time.js +5 -9
  194. package/dist/lib/util/time.js.map +1 -1
  195. package/dist/lib/util/type.d.ts.map +1 -1
  196. package/dist/lib/util/type.js +1 -5
  197. package/dist/lib/util/type.js.map +1 -1
  198. package/dist/lib/util/ui8.js +3 -8
  199. package/dist/lib/util/ui8.js.map +1 -1
  200. package/dist/lib/util/well-known.js +1 -4
  201. package/dist/lib/util/well-known.js.map +1 -1
  202. package/dist/lib/util/zod-error.js +4 -8
  203. package/dist/lib/util/zod-error.js.map +1 -1
  204. package/dist/lib/write-form-redirect.js +9 -12
  205. package/dist/lib/write-form-redirect.js.map +1 -1
  206. package/dist/lib/write-html.js +12 -15
  207. package/dist/lib/write-html.js.map +1 -1
  208. package/dist/metadata/build-metadata.js +9 -12
  209. package/dist/metadata/build-metadata.js.map +1 -1
  210. package/dist/oauth-client.js +2 -18
  211. package/dist/oauth-client.js.map +1 -1
  212. package/dist/oauth-dpop.js +2 -18
  213. package/dist/oauth-dpop.js.map +1 -1
  214. package/dist/oauth-errors.js +24 -42
  215. package/dist/oauth-errors.js.map +1 -1
  216. package/dist/oauth-hooks.js +8 -15
  217. package/dist/oauth-hooks.js.map +1 -1
  218. package/dist/oauth-middleware.js +13 -16
  219. package/dist/oauth-middleware.js.map +1 -1
  220. package/dist/oauth-provider.js +108 -125
  221. package/dist/oauth-provider.js.map +1 -1
  222. package/dist/oauth-store.js +7 -23
  223. package/dist/oauth-store.js.map +1 -1
  224. package/dist/oauth-verifier.js +41 -53
  225. package/dist/oauth-verifier.js.map +1 -1
  226. package/dist/oidc/sub.js +2 -5
  227. package/dist/oidc/sub.js.map +1 -1
  228. package/dist/replay/replay-manager.js +6 -11
  229. package/dist/replay/replay-manager.js.map +1 -1
  230. package/dist/replay/replay-store-memory.js +5 -7
  231. package/dist/replay/replay-store-memory.js.map +1 -1
  232. package/dist/replay/replay-store-redis.js +3 -8
  233. package/dist/replay/replay-store-redis.js.map +1 -1
  234. package/dist/replay/replay-store.js +3 -8
  235. package/dist/replay/replay-store.js.map +1 -1
  236. package/dist/request/code.js +10 -15
  237. package/dist/request/code.js.map +1 -1
  238. package/dist/request/request-data.js +1 -5
  239. package/dist/request/request-data.js.map +1 -1
  240. package/dist/request/request-id.js +9 -13
  241. package/dist/request/request-id.js.map +1 -1
  242. package/dist/request/request-manager.js +61 -71
  243. package/dist/request/request-manager.js.map +1 -1
  244. package/dist/request/request-store.js +9 -27
  245. package/dist/request/request-store.js.map +1 -1
  246. package/dist/request/request-uri.js +17 -23
  247. package/dist/request/request-uri.js.map +1 -1
  248. package/dist/result/authorization-redirect-parameters.js +1 -2
  249. package/dist/result/authorization-result-authorize-page.js +1 -2
  250. package/dist/result/authorization-result-redirect.js +1 -2
  251. package/dist/router/assets/assets-manifest.d.ts.map +1 -1
  252. package/dist/router/assets/assets-manifest.js +14 -15
  253. package/dist/router/assets/assets-manifest.js.map +1 -1
  254. package/dist/router/assets/assets.d.ts.map +1 -1
  255. package/dist/router/assets/assets.js +25 -27
  256. package/dist/router/assets/assets.js.map +1 -1
  257. package/dist/router/assets/csrf.js +16 -25
  258. package/dist/router/assets/csrf.js.map +1 -1
  259. package/dist/router/assets/send-account-page.js +3 -6
  260. package/dist/router/assets/send-account-page.js.map +1 -1
  261. package/dist/router/assets/send-authorization-page.js +3 -6
  262. package/dist/router/assets/send-authorization-page.js.map +1 -1
  263. package/dist/router/assets/send-cookie-error-page.js +3 -6
  264. package/dist/router/assets/send-cookie-error-page.js.map +1 -1
  265. package/dist/router/assets/send-error-page.js +6 -9
  266. package/dist/router/assets/send-error-page.js.map +1 -1
  267. package/dist/router/assets/send-redirect.js +12 -20
  268. package/dist/router/assets/send-redirect.js.map +1 -1
  269. package/dist/router/create-account-page-middleware.js +11 -14
  270. package/dist/router/create-account-page-middleware.js.map +1 -1
  271. package/dist/router/create-api-middleware.js +83 -90
  272. package/dist/router/create-api-middleware.js.map +1 -1
  273. package/dist/router/create-authorization-page-middleware.js +43 -46
  274. package/dist/router/create-authorization-page-middleware.js.map +1 -1
  275. package/dist/router/create-oauth-middleware.js +31 -34
  276. package/dist/router/create-oauth-middleware.js.map +1 -1
  277. package/dist/router/error-handler.js +1 -2
  278. package/dist/router/middleware-options.js +1 -2
  279. package/dist/signer/access-token-payload.js +12 -15
  280. package/dist/signer/access-token-payload.js.map +1 -1
  281. package/dist/signer/api-token-payload.js +8 -11
  282. package/dist/signer/api-token-payload.js.map +1 -1
  283. package/dist/signer/signer.js +11 -17
  284. package/dist/signer/signer.js.map +1 -1
  285. package/dist/token/refresh-token.js +10 -15
  286. package/dist/token/refresh-token.js.map +1 -1
  287. package/dist/token/token-claims.js +1 -2
  288. package/dist/token/token-data.js +1 -2
  289. package/dist/token/token-id.js +10 -15
  290. package/dist/token/token-id.js.map +1 -1
  291. package/dist/token/token-manager.js +40 -51
  292. package/dist/token/token-manager.js.map +1 -1
  293. package/dist/token/token-store.js +7 -25
  294. package/dist/token/token-store.js.map +1 -1
  295. package/dist/types/authorization-response-error.js +8 -12
  296. package/dist/types/authorization-response-error.js.map +1 -1
  297. package/dist/types/color-hue.js +2 -5
  298. package/dist/types/color-hue.js.map +1 -1
  299. package/dist/types/email-otp.js +2 -5
  300. package/dist/types/email-otp.js.map +1 -1
  301. package/dist/types/email.js +6 -9
  302. package/dist/types/email.js.map +1 -1
  303. package/dist/types/handle.js +6 -9
  304. package/dist/types/handle.js.map +1 -1
  305. package/dist/types/invite-code.js +2 -5
  306. package/dist/types/invite-code.js.map +1 -1
  307. package/dist/types/par-response-error.js +5 -9
  308. package/dist/types/par-response-error.js.map +1 -1
  309. package/dist/types/password.js +3 -6
  310. package/dist/types/password.js.map +1 -1
  311. package/dist/types/rgb-color.js +7 -10
  312. package/dist/types/rgb-color.js.map +1 -1
  313. package/package.json +20 -22
  314. package/src/dpop/dpop-nonce.ts +1 -1
  315. package/src/errors/invalid-invite-code-error.ts +1 -1
  316. package/src/lib/http/accept.ts +4 -1
  317. package/src/lib/http/request.ts +4 -1
  318. package/src/lib/util/type.ts +0 -1
  319. package/src/router/assets/assets-manifest.ts +3 -1
  320. package/src/router/assets/assets.ts +2 -0
  321. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,42 +1,39 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DeviceManager = exports.deviceManagerOptionsSchema = exports.keygripSchema = void 0;
4
- const zod_1 = require("zod");
5
- const constants_js_1 = require("../constants.js");
6
- const index_js_1 = require("../lib/http/index.js");
7
- const request_js_1 = require("../lib/http/request.js");
8
- const device_id_js_1 = require("./device-id.js");
9
- const session_id_js_1 = require("./session-id.js");
1
+ import { z } from 'zod';
2
+ import { SESSION_FIXATION_MAX_AGE } from '../constants.js';
3
+ import { parseHttpCookies } from '../lib/http/index.js';
4
+ import { extractRequestMetadata, setCookie, } from '../lib/http/request.js';
5
+ import { deviceIdSchema, generateDeviceId } from './device-id.js';
6
+ import { generateSessionId, sessionIdSchema } from './session-id.js';
10
7
  /**
11
8
  * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}
12
9
  */
13
- exports.keygripSchema = zod_1.z.object({
14
- sign: zod_1.z.function().args(zod_1.z.any()).returns(zod_1.z.string()),
15
- verify: zod_1.z.function().args(zod_1.z.any(), zod_1.z.string()).returns(zod_1.z.boolean()),
16
- index: zod_1.z.function().args(zod_1.z.any(), zod_1.z.string()).returns(zod_1.z.number()),
10
+ export const keygripSchema = z.object({
11
+ sign: z.function().args(z.any()).returns(z.string()),
12
+ verify: z.function().args(z.any(), z.string()).returns(z.boolean()),
13
+ index: z.function().args(z.any(), z.string()).returns(z.number()),
17
14
  });
18
- exports.deviceManagerOptionsSchema = zod_1.z.object({
15
+ export const deviceManagerOptionsSchema = z.object({
19
16
  /**
20
17
  * Controls whether the IP address is read from the `X-Forwarded-For` header
21
18
  * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).
22
19
  */
23
- trustProxy: zod_1.z
20
+ trustProxy: z
24
21
  .function()
25
- .args(zod_1.z.string(), zod_1.z.number())
26
- .returns(zod_1.z.boolean())
22
+ .args(z.string(), z.number())
23
+ .returns(z.boolean())
27
24
  .optional(),
28
25
  /**
29
26
  * Amount of time (in ms) after which session IDs will be rotated
30
27
  *
31
28
  * @default 300e3 // (5 minutes)
32
29
  */
33
- rotationRate: zod_1.z.number().default(300e3),
30
+ rotationRate: z.number().default(300e3),
34
31
  /**
35
32
  * Cookie options
36
33
  */
37
- cookie: zod_1.z
34
+ cookie: z
38
35
  .object({
39
- keys: exports.keygripSchema.optional(),
36
+ keys: keygripSchema.optional(),
40
37
  /**
41
38
  * Amount of time (in ms) after which the session cookie will expire.
42
39
  * If set to `null`, the cookie will be a session cookie (deleted when the
@@ -44,7 +41,7 @@ exports.deviceManagerOptionsSchema = zod_1.z.object({
44
41
  *
45
42
  * @default 10 years
46
43
  */
47
- age: zod_1.z
44
+ age: z
48
45
  .number()
49
46
  .nullable()
50
47
  .default(10 * 365.2 * 24 * 60 * 60e3),
@@ -53,13 +50,13 @@ exports.deviceManagerOptionsSchema = zod_1.z.object({
53
50
  * over HTTP (if `false`). This should **NOT** be set to `false` in
54
51
  * production.
55
52
  */
56
- secure: zod_1.z.boolean().default(true),
53
+ secure: z.boolean().default(true),
57
54
  /**
58
55
  * Controls whether the cookie is sent along with cross-site requests.
59
56
  *
60
57
  * @default 'lax'
61
58
  */
62
- sameSite: zod_1.z.enum(['lax', 'strict']).default('lax'),
59
+ sameSite: z.enum(['lax', 'strict']).default('lax'),
63
60
  })
64
61
  .default({}),
65
62
  });
@@ -68,12 +65,10 @@ exports.deviceManagerOptionsSchema = zod_1.z.object({
68
65
  * relies on a {@link DeviceStore} to persist session data and a cookie to
69
66
  * identify the session.
70
67
  */
71
- class DeviceManager {
72
- store;
73
- options;
68
+ export class DeviceManager {
74
69
  constructor(store, options = {}) {
75
70
  this.store = store;
76
- this.options = exports.deviceManagerOptionsSchema.parse(options);
71
+ this.options = deviceManagerOptionsSchema.parse(options);
77
72
  }
78
73
  async hasSession(req) {
79
74
  const cookies = await this.getCookies(req);
@@ -91,8 +86,8 @@ class DeviceManager {
91
86
  async create(req, res) {
92
87
  const deviceMetadata = this.getRequestMetadata(req);
93
88
  const [deviceId, sessionId] = await Promise.all([
94
- (0, device_id_js_1.generateDeviceId)(),
95
- (0, session_id_js_1.generateSessionId)(),
89
+ generateDeviceId(),
90
+ generateSessionId(),
96
91
  ]);
97
92
  await this.store.createDevice(deviceId, {
98
93
  sessionId,
@@ -110,7 +105,7 @@ class DeviceManager {
110
105
  const lastSeenAt = new Date(data.lastSeenAt);
111
106
  const age = Date.now() - lastSeenAt.getTime();
112
107
  if (sessionId !== data.sessionId) {
113
- if (age <= constants_js_1.SESSION_FIXATION_MAX_AGE) {
108
+ if (age <= SESSION_FIXATION_MAX_AGE) {
114
109
  // The cookie was probably rotated by a concurrent request. Let's
115
110
  // update the cookie with the new sessionId.
116
111
  forceRotate = true;
@@ -135,7 +130,7 @@ class DeviceManager {
135
130
  return { deviceId, deviceMetadata };
136
131
  }
137
132
  async rotate(req, res, deviceId, data) {
138
- const sessionId = await (0, session_id_js_1.generateSessionId)();
133
+ const sessionId = await generateSessionId();
139
134
  await this.store.updateDevice(deviceId, {
140
135
  ...data,
141
136
  sessionId,
@@ -144,9 +139,9 @@ class DeviceManager {
144
139
  await this.setCookies(req, res, { deviceId, sessionId });
145
140
  }
146
141
  async getCookies(req) {
147
- const cookies = (0, index_js_1.parseHttpCookies)(req);
148
- const device = this.parseCookie(cookies, `dev-id`, device_id_js_1.deviceIdSchema);
149
- const session = this.parseCookie(cookies, `ses-id`, session_id_js_1.sessionIdSchema);
142
+ const cookies = parseHttpCookies(req);
143
+ const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema);
144
+ const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema);
150
145
  const deviceId = device?.value;
151
146
  const sessionId = session?.value;
152
147
  // Silently ignore invalid cookies
@@ -197,15 +192,14 @@ class DeviceManager {
197
192
  secure: this.options.cookie.secure !== false,
198
193
  sameSite: this.options.cookie.sameSite,
199
194
  };
200
- (0, request_js_1.setCookie)(res, name, value || '', cookieOptions);
195
+ setCookie(res, name, value || '', cookieOptions);
201
196
  if (this.options.cookie.keys) {
202
197
  const hash = value ? this.options.cookie.keys.sign(value) : '';
203
- (0, request_js_1.setCookie)(res, `${name}:hash`, hash, cookieOptions);
198
+ setCookie(res, `${name}:hash`, hash, cookieOptions);
204
199
  }
205
200
  }
206
201
  getRequestMetadata(req) {
207
- return (0, request_js_1.extractRequestMetadata)(req, this.options);
202
+ return extractRequestMetadata(req, this.options);
208
203
  }
209
204
  }
210
- exports.DeviceManager = DeviceManager;
211
205
  //# sourceMappingURL=device-manager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"device-manager.js","sourceRoot":"","sources":["../../src/device/device-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { z } from 'zod'\nimport { SESSION_FIXATION_MAX_AGE } from '../constants.js'\nimport { parseHttpCookies } from '../lib/http/index.js'\nimport {\n RequestMetadata,\n extractRequestMetadata,\n setCookie,\n} from '../lib/http/request.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId, deviceIdSchema, generateDeviceId } from './device-id.js'\nimport { DeviceStore } from './device-store.js'\nimport { generateSessionId, sessionIdSchema } from './session-id.js'\n\n/**\n * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}\n */\nexport const keygripSchema = z.object({\n sign: z.function().args(z.any()).returns(z.string()),\n verify: z.function().args(z.any(), z.string()).returns(z.boolean()),\n index: z.function().args(z.any(), z.string()).returns(z.number()),\n})\n\nexport const deviceManagerOptionsSchema = z.object({\n /**\n * Controls whether the IP address is read from the `X-Forwarded-For` header\n * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).\n */\n trustProxy: z\n .function()\n .args<[addr: z.ZodString, i: z.ZodNumber]>(z.string(), z.number())\n .returns(z.boolean())\n .optional(),\n\n /**\n * Amount of time (in ms) after which session IDs will be rotated\n *\n * @default 300e3 // (5 minutes)\n */\n rotationRate: z.number().default(300e3),\n /**\n * Cookie options\n */\n cookie: z\n .object({\n keys: keygripSchema.optional(),\n /**\n * Amount of time (in ms) after which the session cookie will expire.\n * If set to `null`, the cookie will be a session cookie (deleted when the\n * browser is closed).\n *\n * @default 10 years\n */\n age: z\n .number()\n .nullable()\n .default(10 * 365.2 * 24 * 60 * 60e3),\n /**\n * Controls whether the cookie is only sent over HTTPS (if `true`), or also\n * over HTTP (if `false`). This should **NOT** be set to `false` in\n * production.\n */\n secure: z.boolean().default(true),\n /**\n * Controls whether the cookie is sent along with cross-site requests.\n *\n * @default 'lax'\n */\n sameSite: z.enum(['lax', 'strict']).default('lax'),\n })\n .default({}),\n})\n\nexport type DeviceManagerOptions = z.input<typeof deviceManagerOptionsSchema>\n\ntype CookieValue = {\n deviceId: DeviceId\n sessionId: string\n}\n\nexport type DeviceInfo = {\n deviceId: DeviceId\n deviceMetadata: RequestMetadata\n}\n\n/**\n * This class provides an abstraction for keeping track of DEVICE sessions. It\n * relies on a {@link DeviceStore} to persist session data and a cookie to\n * identify the session.\n */\nexport class DeviceManager {\n private readonly options: z.output<typeof deviceManagerOptionsSchema>\n\n constructor(\n private readonly store: DeviceStore,\n options: DeviceManagerOptions = {},\n ) {\n this.options = deviceManagerOptionsSchema.parse(options)\n }\n\n public async hasSession(req: IncomingMessage): Promise<boolean> {\n const cookies = await this.getCookies(req)\n return cookies !== null\n }\n\n public async load(\n req: IncomingMessage,\n res: ServerResponse,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const cookie = await this.getCookies(req)\n if (cookie) {\n return this.refresh(\n req,\n res,\n cookie.value,\n forceRotate || cookie.mustRotate,\n )\n } else {\n return this.create(req, res)\n }\n }\n\n private async create(\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<DeviceInfo> {\n const deviceMetadata = this.getRequestMetadata(req)\n\n const [deviceId, sessionId] = await Promise.all([\n generateDeviceId(),\n generateSessionId(),\n ] as const)\n\n await this.store.createDevice(deviceId, {\n sessionId,\n lastSeenAt: new Date(),\n userAgent: deviceMetadata.userAgent ?? null,\n ipAddress: deviceMetadata.ipAddress,\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n\n return { deviceId, deviceMetadata }\n }\n\n private async refresh(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const data = await this.store.readDevice(deviceId)\n if (!data) return this.create(req, res)\n\n const lastSeenAt = new Date(data.lastSeenAt)\n const age = Date.now() - lastSeenAt.getTime()\n\n if (sessionId !== data.sessionId) {\n if (age <= SESSION_FIXATION_MAX_AGE) {\n // The cookie was probably rotated by a concurrent request. Let's\n // update the cookie with the new sessionId.\n forceRotate = true\n } else {\n // Something's wrong. Let's create a new session.\n await this.store.deleteDevice(deviceId)\n return this.create(req, res)\n }\n }\n\n const deviceMetadata = this.getRequestMetadata(req)\n\n const shouldRotate =\n forceRotate ||\n deviceMetadata.ipAddress !== data.ipAddress ||\n deviceMetadata.userAgent !== data.userAgent ||\n age > this.options.rotationRate\n\n if (shouldRotate) {\n await this.rotate(req, res, deviceId, {\n ipAddress: deviceMetadata.ipAddress,\n userAgent: deviceMetadata.userAgent || data.userAgent,\n })\n }\n\n return { deviceId, deviceMetadata }\n }\n\n private async rotate(\n req: IncomingMessage,\n res: ServerResponse,\n deviceId: DeviceId,\n data?: Partial<Omit<DeviceData, 'sessionId' | 'lastSeenAt'>>,\n ): Promise<void> {\n const sessionId = await generateSessionId()\n\n await this.store.updateDevice(deviceId, {\n ...data,\n sessionId,\n lastSeenAt: new Date(),\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n }\n\n private async getCookies(\n req: IncomingMessage,\n ): Promise<{ value: CookieValue; mustRotate: boolean } | null> {\n const cookies = parseHttpCookies(req)\n\n const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema)\n const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema)\n\n const deviceId = device?.value\n const sessionId = session?.value\n\n // Silently ignore invalid cookies\n if (!deviceId || !sessionId) {\n // If the device cookie is still present, let's cleanup the DB\n if (deviceId) await this.store.deleteDevice(deviceId)\n\n return null\n }\n\n return {\n value: { deviceId, sessionId },\n mustRotate: device.mustRotate || session.mustRotate,\n }\n }\n\n private parseCookie<T>(\n cookies: Record<string, string | undefined>,\n name: string,\n schema: z.ZodType<T> | z.ZodEffects<z.ZodTypeAny, T, string>,\n ): null | { value: T; mustRotate: boolean } {\n const rawValue = Object.hasOwn(cookies, name) ? cookies[name] : null\n if (!rawValue) return null\n\n const result = schema.safeParse(rawValue)\n if (!result.success) return null\n\n const value = result.data\n\n if (this.options.cookie.keys) {\n const hashName = `${name}:hash`\n\n const hash = Object.hasOwn(cookies, hashName) ? cookies[hashName] : null\n if (!hash) return null\n\n const idx = this.options.cookie.keys.index(rawValue, hash)\n if (idx < 0) return null\n\n return { value, mustRotate: idx !== 0 }\n }\n\n return { value, mustRotate: false }\n }\n\n private async setCookies(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n ) {\n this.writeCookie(res, `dev-id`, deviceId)\n this.writeCookie(res, `ses-id`, sessionId)\n }\n\n private writeCookie(res: ServerResponse, name: string, value?: string) {\n const cookieOptions = {\n maxAge: value\n ? this.options.cookie.age == null\n ? undefined\n : this.options.cookie.age / 1000\n : 0,\n httpOnly: true,\n path: '/',\n secure: this.options.cookie.secure !== false,\n sameSite: this.options.cookie.sameSite,\n } as const\n\n setCookie(res, name, value || '', cookieOptions)\n\n if (this.options.cookie.keys) {\n const hash = value ? this.options.cookie.keys.sign(value) : ''\n setCookie(res, `${name}:hash`, hash, cookieOptions)\n }\n }\n\n public getRequestMetadata(req: IncomingMessage) {\n return extractRequestMetadata(req, this.options)\n }\n}\n"]}
1
+ {"version":3,"file":"device-manager.js","sourceRoot":"","sources":["../../src/device/device-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport { z } from 'zod'\nimport { SESSION_FIXATION_MAX_AGE } from '../constants.js'\nimport { parseHttpCookies } from '../lib/http/index.js'\nimport {\n RequestMetadata,\n extractRequestMetadata,\n setCookie,\n} from '../lib/http/request.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId, deviceIdSchema, generateDeviceId } from './device-id.js'\nimport { DeviceStore } from './device-store.js'\nimport { generateSessionId, sessionIdSchema } from './session-id.js'\n\n/**\n * @see {@link https://www.npmjs.com/package/keygrip | Keygrip}\n */\nexport const keygripSchema = z.object({\n sign: z.function().args(z.any()).returns(z.string()),\n verify: z.function().args(z.any(), z.string()).returns(z.boolean()),\n index: z.function().args(z.any(), z.string()).returns(z.number()),\n})\n\nexport const deviceManagerOptionsSchema = z.object({\n /**\n * Controls whether the IP address is read from the `X-Forwarded-For` header\n * (if `true`), or from the `req.socket.remoteAddress` property (if `false`).\n */\n trustProxy: z\n .function()\n .args<[addr: z.ZodString, i: z.ZodNumber]>(z.string(), z.number())\n .returns(z.boolean())\n .optional(),\n\n /**\n * Amount of time (in ms) after which session IDs will be rotated\n *\n * @default 300e3 // (5 minutes)\n */\n rotationRate: z.number().default(300e3),\n /**\n * Cookie options\n */\n cookie: z\n .object({\n keys: keygripSchema.optional(),\n /**\n * Amount of time (in ms) after which the session cookie will expire.\n * If set to `null`, the cookie will be a session cookie (deleted when the\n * browser is closed).\n *\n * @default 10 years\n */\n age: z\n .number()\n .nullable()\n .default(10 * 365.2 * 24 * 60 * 60e3),\n /**\n * Controls whether the cookie is only sent over HTTPS (if `true`), or also\n * over HTTP (if `false`). This should **NOT** be set to `false` in\n * production.\n */\n secure: z.boolean().default(true),\n /**\n * Controls whether the cookie is sent along with cross-site requests.\n *\n * @default 'lax'\n */\n sameSite: z.enum(['lax', 'strict']).default('lax'),\n })\n .default({}),\n})\n\nexport type DeviceManagerOptions = z.input<typeof deviceManagerOptionsSchema>\n\ntype CookieValue = {\n deviceId: DeviceId\n sessionId: string\n}\n\nexport type DeviceInfo = {\n deviceId: DeviceId\n deviceMetadata: RequestMetadata\n}\n\n/**\n * This class provides an abstraction for keeping track of DEVICE sessions. It\n * relies on a {@link DeviceStore} to persist session data and a cookie to\n * identify the session.\n */\nexport class DeviceManager {\n private readonly options: z.output<typeof deviceManagerOptionsSchema>\n\n constructor(\n private readonly store: DeviceStore,\n options: DeviceManagerOptions = {},\n ) {\n this.options = deviceManagerOptionsSchema.parse(options)\n }\n\n public async hasSession(req: IncomingMessage): Promise<boolean> {\n const cookies = await this.getCookies(req)\n return cookies !== null\n }\n\n public async load(\n req: IncomingMessage,\n res: ServerResponse,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const cookie = await this.getCookies(req)\n if (cookie) {\n return this.refresh(\n req,\n res,\n cookie.value,\n forceRotate || cookie.mustRotate,\n )\n } else {\n return this.create(req, res)\n }\n }\n\n private async create(\n req: IncomingMessage,\n res: ServerResponse,\n ): Promise<DeviceInfo> {\n const deviceMetadata = this.getRequestMetadata(req)\n\n const [deviceId, sessionId] = await Promise.all([\n generateDeviceId(),\n generateSessionId(),\n ] as const)\n\n await this.store.createDevice(deviceId, {\n sessionId,\n lastSeenAt: new Date(),\n userAgent: deviceMetadata.userAgent ?? null,\n ipAddress: deviceMetadata.ipAddress,\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n\n return { deviceId, deviceMetadata }\n }\n\n private async refresh(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n forceRotate = false,\n ): Promise<DeviceInfo> {\n const data = await this.store.readDevice(deviceId)\n if (!data) return this.create(req, res)\n\n const lastSeenAt = new Date(data.lastSeenAt)\n const age = Date.now() - lastSeenAt.getTime()\n\n if (sessionId !== data.sessionId) {\n if (age <= SESSION_FIXATION_MAX_AGE) {\n // The cookie was probably rotated by a concurrent request. Let's\n // update the cookie with the new sessionId.\n forceRotate = true\n } else {\n // Something's wrong. Let's create a new session.\n await this.store.deleteDevice(deviceId)\n return this.create(req, res)\n }\n }\n\n const deviceMetadata = this.getRequestMetadata(req)\n\n const shouldRotate =\n forceRotate ||\n deviceMetadata.ipAddress !== data.ipAddress ||\n deviceMetadata.userAgent !== data.userAgent ||\n age > this.options.rotationRate\n\n if (shouldRotate) {\n await this.rotate(req, res, deviceId, {\n ipAddress: deviceMetadata.ipAddress,\n userAgent: deviceMetadata.userAgent || data.userAgent,\n })\n }\n\n return { deviceId, deviceMetadata }\n }\n\n private async rotate(\n req: IncomingMessage,\n res: ServerResponse,\n deviceId: DeviceId,\n data?: Partial<Omit<DeviceData, 'sessionId' | 'lastSeenAt'>>,\n ): Promise<void> {\n const sessionId = await generateSessionId()\n\n await this.store.updateDevice(deviceId, {\n ...data,\n sessionId,\n lastSeenAt: new Date(),\n })\n\n await this.setCookies(req, res, { deviceId, sessionId })\n }\n\n private async getCookies(\n req: IncomingMessage,\n ): Promise<{ value: CookieValue; mustRotate: boolean } | null> {\n const cookies = parseHttpCookies(req)\n\n const device = this.parseCookie(cookies, `dev-id`, deviceIdSchema)\n const session = this.parseCookie(cookies, `ses-id`, sessionIdSchema)\n\n const deviceId = device?.value\n const sessionId = session?.value\n\n // Silently ignore invalid cookies\n if (!deviceId || !sessionId) {\n // If the device cookie is still present, let's cleanup the DB\n if (deviceId) await this.store.deleteDevice(deviceId)\n\n return null\n }\n\n return {\n value: { deviceId, sessionId },\n mustRotate: device.mustRotate || session.mustRotate,\n }\n }\n\n private parseCookie<T>(\n cookies: Record<string, string | undefined>,\n name: string,\n schema: z.ZodType<T> | z.ZodEffects<z.ZodTypeAny, T, string>,\n ): null | { value: T; mustRotate: boolean } {\n const rawValue = Object.hasOwn(cookies, name) ? cookies[name] : null\n if (!rawValue) return null\n\n const result = schema.safeParse(rawValue)\n if (!result.success) return null\n\n const value = result.data\n\n if (this.options.cookie.keys) {\n const hashName = `${name}:hash`\n\n const hash = Object.hasOwn(cookies, hashName) ? cookies[hashName] : null\n if (!hash) return null\n\n const idx = this.options.cookie.keys.index(rawValue, hash)\n if (idx < 0) return null\n\n return { value, mustRotate: idx !== 0 }\n }\n\n return { value, mustRotate: false }\n }\n\n private async setCookies(\n req: IncomingMessage,\n res: ServerResponse,\n { deviceId, sessionId }: CookieValue,\n ) {\n this.writeCookie(res, `dev-id`, deviceId)\n this.writeCookie(res, `ses-id`, sessionId)\n }\n\n private writeCookie(res: ServerResponse, name: string, value?: string) {\n const cookieOptions = {\n maxAge: value\n ? this.options.cookie.age == null\n ? undefined\n : this.options.cookie.age / 1000\n : 0,\n httpOnly: true,\n path: '/',\n secure: this.options.cookie.secure !== false,\n sameSite: this.options.cookie.sameSite,\n } as const\n\n setCookie(res, name, value || '', cookieOptions)\n\n if (this.options.cookie.keys) {\n const hash = value ? this.options.cookie.keys.sign(value) : ''\n setCookie(res, `${name}:hash`, hash, cookieOptions)\n }\n }\n\n public getRequestMetadata(req: IncomingMessage) {\n return extractRequestMetadata(req, this.options)\n }\n}\n"]}
@@ -1,34 +1,16 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.isDeviceStore = void 0;
18
- exports.asDeviceStore = asDeviceStore;
19
- const type_js_1 = require("../lib/util/type.js");
1
+ import { buildInterfaceChecker } from '../lib/util/type.js';
20
2
  // Export all types needed to implement the DeviceStore interface
21
- __exportStar(require("./device-data.js"), exports);
22
- __exportStar(require("./device-id.js"), exports);
23
- __exportStar(require("./session-id.js"), exports);
24
- exports.isDeviceStore = (0, type_js_1.buildInterfaceChecker)([
3
+ export * from './device-data.js';
4
+ export * from './device-id.js';
5
+ export * from './session-id.js';
6
+ export const isDeviceStore = buildInterfaceChecker([
25
7
  'createDevice',
26
8
  'readDevice',
27
9
  'updateDevice',
28
10
  'deleteDevice',
29
11
  ]);
30
- function asDeviceStore(implementation) {
31
- if (!implementation || !(0, exports.isDeviceStore)(implementation)) {
12
+ export function asDeviceStore(implementation) {
13
+ if (!implementation || !isDeviceStore(implementation)) {
32
14
  throw new Error('Invalid DeviceStore implementation');
33
15
  }
34
16
  return implementation;
@@ -1 +1 @@
1
- {"version":3,"file":"device-store.js","sourceRoot":"","sources":["../../src/device/device-store.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAyBA,sCAKC;AA9BD,iDAAsE;AAItE,iEAAiE;AACjE,mDAAgC;AAChC,iDAA8B;AAC9B,kDAA+B;AAWlB,QAAA,aAAa,GAAG,IAAA,+BAAqB,EAAc;IAC9D,cAAc;IACd,YAAY;IACZ,cAAc;IACd,cAAc;CACf,CAAC,CAAA;AAEF,SAAgB,aAAa,CAAI,cAAiB;IAChD,IAAI,CAAC,cAAc,IAAI,CAAC,IAAA,qBAAa,EAAC,cAAc,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,cAAc,CAAA;AACvB,CAAC","sourcesContent":["import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId } from './device-id.js'\n\n// Export all types needed to implement the DeviceStore interface\nexport * from './device-data.js'\nexport * from './device-id.js'\nexport * from './session-id.js'\n\nexport type { Awaitable }\n\nexport interface DeviceStore {\n createDevice(deviceId: DeviceId, data: DeviceData): Awaitable<void>\n readDevice(deviceId: DeviceId): Awaitable<DeviceData | null>\n updateDevice(deviceId: DeviceId, data: Partial<DeviceData>): Awaitable<void>\n deleteDevice(deviceId: DeviceId): Awaitable<void>\n}\n\nexport const isDeviceStore = buildInterfaceChecker<DeviceStore>([\n 'createDevice',\n 'readDevice',\n 'updateDevice',\n 'deleteDevice',\n])\n\nexport function asDeviceStore<V>(implementation: V): V & DeviceStore {\n if (!implementation || !isDeviceStore(implementation)) {\n throw new Error('Invalid DeviceStore implementation')\n }\n return implementation\n}\n"]}
1
+ {"version":3,"file":"device-store.js","sourceRoot":"","sources":["../../src/device/device-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAItE,iEAAiE;AACjE,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAW/B,MAAM,CAAC,MAAM,aAAa,GAAG,qBAAqB,CAAc;IAC9D,cAAc;IACd,YAAY;IACZ,cAAc;IACd,cAAc;CACf,CAAC,CAAA;AAEF,MAAM,UAAU,aAAa,CAAI,cAAiB;IAChD,IAAI,CAAC,cAAc,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,cAAc,CAAA;AACvB,CAAC","sourcesContent":["import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'\nimport { DeviceData } from './device-data.js'\nimport { DeviceId } from './device-id.js'\n\n// Export all types needed to implement the DeviceStore interface\nexport * from './device-data.js'\nexport * from './device-id.js'\nexport * from './session-id.js'\n\nexport type { Awaitable }\n\nexport interface DeviceStore {\n createDevice(deviceId: DeviceId, data: DeviceData): Awaitable<void>\n readDevice(deviceId: DeviceId): Awaitable<DeviceData | null>\n updateDevice(deviceId: DeviceId, data: Partial<DeviceData>): Awaitable<void>\n deleteDevice(deviceId: DeviceId): Awaitable<void>\n}\n\nexport const isDeviceStore = buildInterfaceChecker<DeviceStore>([\n 'createDevice',\n 'readDevice',\n 'updateDevice',\n 'deleteDevice',\n])\n\nexport function asDeviceStore<V>(implementation: V): V & DeviceStore {\n if (!implementation || !isDeviceStore(implementation)) {\n throw new Error('Invalid DeviceStore implementation')\n }\n return implementation\n}\n"]}
@@ -1,18 +1,14 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateSessionId = exports.sessionIdSchema = exports.SESSION_ID_LENGTH = void 0;
4
- const zod_1 = require("zod");
5
- const constants_js_1 = require("../constants.js");
6
- const crypto_js_1 = require("../lib/util/crypto.js");
7
- exports.SESSION_ID_LENGTH = constants_js_1.SESSION_ID_PREFIX.length + constants_js_1.SESSION_ID_BYTES_LENGTH * 2; // hex encoding
8
- exports.sessionIdSchema = zod_1.z
1
+ import { z } from 'zod';
2
+ import { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js';
3
+ import { randomHexId } from '../lib/util/crypto.js';
4
+ export const SESSION_ID_LENGTH = SESSION_ID_PREFIX.length + SESSION_ID_BYTES_LENGTH * 2; // hex encoding
5
+ export const sessionIdSchema = z
9
6
  .string()
10
- .length(exports.SESSION_ID_LENGTH)
11
- .refine((v) => v.startsWith(constants_js_1.SESSION_ID_PREFIX), {
7
+ .length(SESSION_ID_LENGTH)
8
+ .refine((v) => v.startsWith(SESSION_ID_PREFIX), {
12
9
  message: `Invalid session ID format`,
13
10
  });
14
- const generateSessionId = async () => {
15
- return `${constants_js_1.SESSION_ID_PREFIX}${await (0, crypto_js_1.randomHexId)(constants_js_1.SESSION_ID_BYTES_LENGTH)}`;
11
+ export const generateSessionId = async () => {
12
+ return `${SESSION_ID_PREFIX}${await randomHexId(SESSION_ID_BYTES_LENGTH)}`;
16
13
  };
17
- exports.generateSessionId = generateSessionId;
18
14
  //# sourceMappingURL=session-id.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-id.js","sourceRoot":"","sources":["../../src/device/session-id.ts"],"names":[],"mappings":";;;AAAA,6BAAuB;AACvB,kDAA4E;AAC5E,qDAAmD;AAEtC,QAAA,iBAAiB,GAC5B,gCAAiB,CAAC,MAAM,GAAG,sCAAuB,GAAG,CAAC,CAAA,CAAC,eAAe;AAE3D,QAAA,eAAe,GAAG,OAAC;KAC7B,MAAM,EAAE;KACR,MAAM,CAAC,yBAAiB,CAAC;KACzB,MAAM,CACL,CAAC,CAAC,EAA+C,EAAE,CACjD,CAAC,CAAC,UAAU,CAAC,gCAAiB,CAAC,EACjC;IACE,OAAO,EAAE,2BAA2B;CACrC,CACF,CAAA;AAEI,MAAM,iBAAiB,GAAG,KAAK,IAAwB,EAAE;IAC9D,OAAO,GAAG,gCAAiB,GAAG,MAAM,IAAA,uBAAW,EAAC,sCAAuB,CAAC,EAAE,CAAA;AAC5E,CAAC,CAAA;AAFY,QAAA,iBAAiB,qBAE7B","sourcesContent":["import { z } from 'zod'\nimport { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const SESSION_ID_LENGTH =\n SESSION_ID_PREFIX.length + SESSION_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const sessionIdSchema = z\n .string()\n .length(SESSION_ID_LENGTH)\n .refine(\n (v): v is `${typeof SESSION_ID_PREFIX}${string}` =>\n v.startsWith(SESSION_ID_PREFIX),\n {\n message: `Invalid session ID format`,\n },\n )\nexport type SessionId = z.infer<typeof sessionIdSchema>\nexport const generateSessionId = async (): Promise<SessionId> => {\n return `${SESSION_ID_PREFIX}${await randomHexId(SESSION_ID_BYTES_LENGTH)}`\n}\n"]}
1
+ {"version":3,"file":"session-id.js","sourceRoot":"","sources":["../../src/device/session-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,MAAM,CAAC,MAAM,iBAAiB,GAC5B,iBAAiB,CAAC,MAAM,GAAG,uBAAuB,GAAG,CAAC,CAAA,CAAC,eAAe;AAExE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,MAAM,CAAC,iBAAiB,CAAC;KACzB,MAAM,CACL,CAAC,CAAC,EAA+C,EAAE,CACjD,CAAC,CAAC,UAAU,CAAC,iBAAiB,CAAC,EACjC;IACE,OAAO,EAAE,2BAA2B;CACrC,CACF,CAAA;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,IAAwB,EAAE;IAC9D,OAAO,GAAG,iBAAiB,GAAG,MAAM,WAAW,CAAC,uBAAuB,CAAC,EAAE,CAAA;AAC5E,CAAC,CAAA","sourcesContent":["import { z } from 'zod'\nimport { SESSION_ID_BYTES_LENGTH, SESSION_ID_PREFIX } from '../constants.js'\nimport { randomHexId } from '../lib/util/crypto.js'\n\nexport const SESSION_ID_LENGTH =\n SESSION_ID_PREFIX.length + SESSION_ID_BYTES_LENGTH * 2 // hex encoding\n\nexport const sessionIdSchema = z\n .string()\n .length(SESSION_ID_LENGTH)\n .refine(\n (v): v is `${typeof SESSION_ID_PREFIX}${string}` =>\n v.startsWith(SESSION_ID_PREFIX),\n {\n message: `Invalid session ID format`,\n },\n )\nexport type SessionId = z.infer<typeof sessionIdSchema>\nexport const generateSessionId = async (): Promise<SessionId> => {\n return `${SESSION_ID_PREFIX}${await randomHexId(SESSION_ID_BYTES_LENGTH)}`\n}\n"]}
@@ -9,13 +9,13 @@ export declare const dpopManagerOptionsSchema: z.ZodObject<{
9
9
  * all nonces (typically useful when multiple instances are running). Leave
10
10
  * undefined to generate a random seed at startup.
11
11
  */
12
- dpopSecret: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<false>, z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<ArrayBuffer>, z.ZodTypeDef, Uint8Array<ArrayBuffer>>, Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>, z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>]>]>>;
12
+ dpopSecret: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<false>, z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>, Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>, z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>]>]>>;
13
13
  dpopRotationInterval: z.ZodOptional<z.ZodNumber>;
14
14
  }, "strip", z.ZodTypeAny, {
15
- dpopSecret?: false | Uint8Array<ArrayBufferLike> | Uint8Array<ArrayBuffer> | undefined;
15
+ dpopSecret?: false | Uint8Array<ArrayBufferLike> | undefined;
16
16
  dpopRotationInterval?: number | undefined;
17
17
  }, {
18
- dpopSecret?: string | false | Uint8Array<ArrayBuffer> | undefined;
18
+ dpopSecret?: string | false | Uint8Array<ArrayBufferLike> | undefined;
19
19
  dpopRotationInterval?: number | undefined;
20
20
  }>;
21
21
  export type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>;
@@ -1,35 +1,31 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DpopManager = exports.dpopManagerOptionsSchema = exports.DpopNonce = void 0;
4
- const node_crypto_1 = require("node:crypto");
5
- const jose_1 = require("jose");
6
- const zod_1 = require("zod");
7
- const jwk_1 = require("@atproto/jwk");
8
- const constants_js_1 = require("../constants.js");
9
- const invalid_dpop_proof_error_js_1 = require("../errors/invalid-dpop-proof-error.js");
10
- const use_dpop_nonce_error_js_1 = require("../errors/use-dpop-nonce-error.js");
11
- const cast_js_1 = require("../lib/util/cast.js");
12
- const dpop_nonce_js_1 = require("./dpop-nonce.js");
13
- Object.defineProperty(exports, "DpopNonce", { enumerable: true, get: function () { return dpop_nonce_js_1.DpopNonce; } });
14
- const { JOSEError } = jose_1.errors;
15
- exports.dpopManagerOptionsSchema = zod_1.z.object({
1
+ import { createHash } from 'node:crypto';
2
+ import { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose';
3
+ import { z } from 'zod';
4
+ import { ValidationError } from '@atproto/jwk';
5
+ import { DPOP_NONCE_MAX_AGE } from '../constants.js';
6
+ import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js';
7
+ import { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js';
8
+ import { ifURL } from '../lib/util/cast.js';
9
+ import { DpopNonce, dpopSecretSchema, rotationIntervalSchema, } from './dpop-nonce.js';
10
+ const { JOSEError } = errors;
11
+ export { DpopNonce };
12
+ export const dpopManagerOptionsSchema = z.object({
16
13
  /**
17
14
  * Set this to `false` to disable the use of nonces in DPoP proofs. Set this
18
15
  * to a secret Uint8Array or hex encoded string to use a predictable seed for
19
16
  * all nonces (typically useful when multiple instances are running). Leave
20
17
  * undefined to generate a random seed at startup.
21
18
  */
22
- dpopSecret: zod_1.z.union([zod_1.z.literal(false), dpop_nonce_js_1.dpopSecretSchema]).optional(),
23
- dpopRotationInterval: dpop_nonce_js_1.rotationIntervalSchema.optional(),
19
+ dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),
20
+ dpopRotationInterval: rotationIntervalSchema.optional(),
24
21
  });
25
- class DpopManager {
26
- dpopNonce;
22
+ export class DpopManager {
27
23
  constructor(options = {}) {
28
- const { dpopSecret, dpopRotationInterval } = exports.dpopManagerOptionsSchema.parse(options);
24
+ const { dpopSecret, dpopRotationInterval } = dpopManagerOptionsSchema.parse(options);
29
25
  this.dpopNonce =
30
26
  dpopSecret === false
31
27
  ? undefined
32
- : new dpop_nonce_js_1.DpopNonce(dpopSecret, dpopRotationInterval);
28
+ : new DpopNonce(dpopSecret, dpopRotationInterval);
33
29
  }
34
30
  nextNonce() {
35
31
  return this.dpopNonce?.next();
@@ -45,10 +41,10 @@ class DpopManager {
45
41
  const proof = extractProof(httpHeaders);
46
42
  if (!proof)
47
43
  return null;
48
- const { protectedHeader, payload } = await (0, jose_1.jwtVerify)(proof, jose_1.EmbeddedJWK, {
44
+ const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {
49
45
  typ: 'dpop+jwt',
50
46
  maxTokenAge: 10, // Will ensure presence & validity of "iat" claim
51
- clockTolerance: constants_js_1.DPOP_NONCE_MAX_AGE / 1e3,
47
+ clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,
52
48
  }).catch((err) => {
53
49
  throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof');
54
50
  });
@@ -64,17 +60,17 @@ class DpopManager {
64
60
  // we decide to drop legacy support.
65
61
  const { ath, htm, htu, jti, nonce } = payload;
66
62
  if (nonce !== undefined && typeof nonce !== 'string') {
67
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('Invalid DPoP "nonce" type');
63
+ throw new InvalidDpopProofError('Invalid DPoP "nonce" type');
68
64
  }
69
65
  if (!jti || typeof jti !== 'string') {
70
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "jti" missing');
66
+ throw new InvalidDpopProofError('DPoP "jti" missing');
71
67
  }
72
68
  // Note rfc9110#section-9.1 states that the method name is case-sensitive
73
69
  if (!htm || htm !== httpMethod) {
74
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "htm" mismatch');
70
+ throw new InvalidDpopProofError('DPoP "htm" mismatch');
75
71
  }
76
72
  if (!htu || typeof htu !== 'string') {
77
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('Invalid DPoP "htu" type');
73
+ throw new InvalidDpopProofError('Invalid DPoP "htu" type');
78
74
  }
79
75
  // > To reduce the likelihood of false negatives, servers SHOULD employ
80
76
  // > syntax-based normalization (Section 6.2.2 of [RFC3986]) and
@@ -83,27 +79,27 @@ class DpopManager {
83
79
  //
84
80
  // RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3
85
81
  if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {
86
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "htu" mismatch');
82
+ throw new InvalidDpopProofError('DPoP "htu" mismatch');
87
83
  }
88
84
  if (!nonce && this.dpopNonce) {
89
- throw new use_dpop_nonce_error_js_1.UseDpopNonceError();
85
+ throw new UseDpopNonceError();
90
86
  }
91
87
  if (nonce && !this.dpopNonce?.check(nonce)) {
92
- throw new use_dpop_nonce_error_js_1.UseDpopNonceError('DPoP "nonce" mismatch');
88
+ throw new UseDpopNonceError('DPoP "nonce" mismatch');
93
89
  }
94
90
  if (accessToken) {
95
- const accessTokenHash = (0, node_crypto_1.createHash)('sha256').update(accessToken).digest();
91
+ const accessTokenHash = createHash('sha256').update(accessToken).digest();
96
92
  if (ath !== accessTokenHash.toString('base64url')) {
97
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "ath" mismatch');
93
+ throw new InvalidDpopProofError('DPoP "ath" mismatch');
98
94
  }
99
95
  }
100
96
  else if (ath !== undefined) {
101
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "ath" claim not allowed');
97
+ throw new InvalidDpopProofError('DPoP "ath" claim not allowed');
102
98
  }
103
99
  // @NOTE we can assert there is a jwk because the jwtVerify used the
104
100
  // EmbeddedJWK key getter mechanism.
105
101
  const jwk = protectedHeader.jwk;
106
- const jkt = await (0, jose_1.calculateJwkThumbprint)(jwk, 'sha256').catch((err) => {
102
+ const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {
107
103
  throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt');
108
104
  });
109
105
  // @NOTE We freeze the proof to prevent accidental modification (esp. from
@@ -111,20 +107,19 @@ class DpopManager {
111
107
  return Object.freeze({ jti, jkt, htm, htu });
112
108
  }
113
109
  }
114
- exports.DpopManager = DpopManager;
115
110
  function extractProof(httpHeaders) {
116
111
  const dpopHeader = httpHeaders['dpop'];
117
112
  switch (typeof dpopHeader) {
118
113
  case 'string':
119
114
  if (dpopHeader)
120
115
  return dpopHeader;
121
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP header cannot be empty');
116
+ throw new InvalidDpopProofError('DPoP header cannot be empty');
122
117
  case 'object':
123
118
  // @NOTE the "0" case should never happen a node.js HTTP server will only
124
119
  // return an array if the header is set multiple times.
125
120
  if (dpopHeader.length === 1 && dpopHeader[0])
126
121
  return dpopHeader[0];
127
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP header must contain a single proof');
122
+ throw new InvalidDpopProofError('DPoP header must contain a single proof');
128
123
  default:
129
124
  return null;
130
125
  }
@@ -145,18 +140,18 @@ function normalizeHtuUrl(url) {
145
140
  return url.origin + url.pathname;
146
141
  }
147
142
  function parseHtu(htu) {
148
- const url = (0, cast_js_1.ifURL)(htu);
143
+ const url = ifURL(htu);
149
144
  if (!url) {
150
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "htu" is not a valid URL');
145
+ throw new InvalidDpopProofError('DPoP "htu" is not a valid URL');
151
146
  }
152
147
  // @NOTE the checks bellow can be removed once once jwtPayloadSchema is used
153
148
  // to validate the DPoP proof payload as it already performs these checks
154
149
  // (though the htuSchema).
155
150
  if (url.password || url.username) {
156
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "htu" must not contain credentials');
151
+ throw new InvalidDpopProofError('DPoP "htu" must not contain credentials');
157
152
  }
158
153
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
159
- throw new invalid_dpop_proof_error_js_1.InvalidDpopProofError('DPoP "htu" must be http or https');
154
+ throw new InvalidDpopProofError('DPoP "htu" must be http or https');
160
155
  }
161
156
  // @NOTE For legacy & backwards compatibility reason, we allow a query and
162
157
  // fragment in the DPoP proof's htu. This is not a standard behavior as the
@@ -165,9 +160,9 @@ function parseHtu(htu) {
165
160
  return normalizeHtuUrl(url);
166
161
  }
167
162
  function wrapInvalidDpopProofError(err, title) {
168
- const msg = err instanceof JOSEError || err instanceof jwk_1.ValidationError
163
+ const msg = err instanceof JOSEError || err instanceof ValidationError
169
164
  ? `${title}: ${err.message}`
170
165
  : title;
171
- return new invalid_dpop_proof_error_js_1.InvalidDpopProofError(msg, err);
166
+ return new InvalidDpopProofError(msg, err);
172
167
  }
173
168
  //# sourceMappingURL=dpop-manager.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dpop-manager.js","sourceRoot":"","sources":["../../src/dpop/dpop-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'\nimport { z } from 'zod'\nimport { ValidationError } from '@atproto/jwk'\nimport { DPOP_NONCE_MAX_AGE } from '../constants.js'\nimport { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'\nimport { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'\nimport { ifURL } from '../lib/util/cast.js'\nimport {\n DpopNonce,\n DpopSecret,\n dpopSecretSchema,\n rotationIntervalSchema,\n} from './dpop-nonce.js'\nimport { DpopProof } from './dpop-proof.js'\n\nconst { JOSEError } = errors\n\nexport { DpopNonce, type DpopSecret }\n\nexport const dpopManagerOptionsSchema = z.object({\n /**\n * Set this to `false` to disable the use of nonces in DPoP proofs. Set this\n * to a secret Uint8Array or hex encoded string to use a predictable seed for\n * all nonces (typically useful when multiple instances are running). Leave\n * undefined to generate a random seed at startup.\n */\n dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),\n dpopRotationInterval: rotationIntervalSchema.optional(),\n})\nexport type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>\n\nexport class DpopManager {\n protected readonly dpopNonce?: DpopNonce\n\n constructor(options: DpopManagerOptions = {}) {\n const { dpopSecret, dpopRotationInterval } =\n dpopManagerOptionsSchema.parse(options)\n this.dpopNonce =\n dpopSecret === false\n ? undefined\n : new DpopNonce(dpopSecret, dpopRotationInterval)\n }\n\n nextNonce(): string | undefined {\n return this.dpopNonce?.next()\n }\n\n /**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\n async checkProof(\n httpMethod: string,\n httpUrl: Readonly<URL>,\n httpHeaders: Record<string, undefined | string | string[]>,\n accessToken?: string,\n ): Promise<null | DpopProof> {\n // Fool proofing against use of empty string\n if (!httpMethod) {\n throw new TypeError('HTTP method is required')\n }\n\n const proof = extractProof(httpHeaders)\n if (!proof) return null\n\n const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {\n typ: 'dpop+jwt',\n maxTokenAge: 10, // Will ensure presence & validity of \"iat\" claim\n clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,\n }).catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof')\n })\n\n // @NOTE For legacy & backwards compatibility reason, we cannot use\n // `jwtPayloadSchema` here as it will reject DPoP proofs containing a query\n // or fragment component in the \"htu\" claim.\n\n // const { ath, htm, htu, jti, nonce } = await jwtPayloadSchema\n // .parseAsync(payload)\n // .catch((err) => {\n // throw buildInvalidDpopProofError('Invalid DPoP proof', err)\n // })\n\n // @TODO Uncomment previous lines (and remove redundant checks bellow) once\n // we decide to drop legacy support.\n const { ath, htm, htu, jti, nonce } = payload\n\n if (nonce !== undefined && typeof nonce !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"nonce\" type')\n }\n\n if (!jti || typeof jti !== 'string') {\n throw new InvalidDpopProofError('DPoP \"jti\" missing')\n }\n\n // Note rfc9110#section-9.1 states that the method name is case-sensitive\n if (!htm || htm !== httpMethod) {\n throw new InvalidDpopProofError('DPoP \"htm\" mismatch')\n }\n\n if (!htu || typeof htu !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"htu\" type')\n }\n\n // > To reduce the likelihood of false negatives, servers SHOULD employ\n // > syntax-based normalization (Section 6.2.2 of [RFC3986]) and\n // > scheme-based normalization (Section 6.2.3 of [RFC3986]) before\n // > comparing the htu claim.\n //\n // RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3\n if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {\n throw new InvalidDpopProofError('DPoP \"htu\" mismatch')\n }\n\n if (!nonce && this.dpopNonce) {\n throw new UseDpopNonceError()\n }\n\n if (nonce && !this.dpopNonce?.check(nonce)) {\n throw new UseDpopNonceError('DPoP \"nonce\" mismatch')\n }\n\n if (accessToken) {\n const accessTokenHash = createHash('sha256').update(accessToken).digest()\n if (ath !== accessTokenHash.toString('base64url')) {\n throw new InvalidDpopProofError('DPoP \"ath\" mismatch')\n }\n } else if (ath !== undefined) {\n throw new InvalidDpopProofError('DPoP \"ath\" claim not allowed')\n }\n\n // @NOTE we can assert there is a jwk because the jwtVerify used the\n // EmbeddedJWK key getter mechanism.\n const jwk = protectedHeader.jwk!\n const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt')\n })\n\n // @NOTE We freeze the proof to prevent accidental modification (esp. from\n // hooks).\n return Object.freeze({ jti, jkt, htm, htu })\n }\n}\n\nfunction extractProof(\n httpHeaders: Record<string, undefined | string | string[]>,\n): string | null {\n const dpopHeader = httpHeaders['dpop']\n switch (typeof dpopHeader) {\n case 'string':\n if (dpopHeader) return dpopHeader\n throw new InvalidDpopProofError('DPoP header cannot be empty')\n case 'object':\n // @NOTE the \"0\" case should never happen a node.js HTTP server will only\n // return an array if the header is set multiple times.\n if (dpopHeader.length === 1 && dpopHeader[0]) return dpopHeader[0]!\n throw new InvalidDpopProofError('DPoP header must contain a single proof')\n default:\n return null\n }\n}\n\n/**\n * Constructs the HTTP URI (htu) claim as defined in RFC9449.\n *\n * The htu claim is the normalized URL of the HTTP request, excluding the query\n * string and fragment. This function ensures that the URL is normalized by\n * removing the search and hash components, as well as by using an URL object to\n * simplify the pathname (e.g. removing dot segments).\n *\n * @returns The normalized URL as a string.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\nfunction normalizeHtuUrl(url: Readonly<URL>): string {\n // NodeJS's `URL` normalizes the pathname, so we can just use that.\n return url.origin + url.pathname\n}\n\nfunction parseHtu(htu: string): string {\n const url = ifURL(htu)\n if (!url) {\n throw new InvalidDpopProofError('DPoP \"htu\" is not a valid URL')\n }\n\n // @NOTE the checks bellow can be removed once once jwtPayloadSchema is used\n // to validate the DPoP proof payload as it already performs these checks\n // (though the htuSchema).\n\n if (url.password || url.username) {\n throw new InvalidDpopProofError('DPoP \"htu\" must not contain credentials')\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new InvalidDpopProofError('DPoP \"htu\" must be http or https')\n }\n\n // @NOTE For legacy & backwards compatibility reason, we allow a query and\n // fragment in the DPoP proof's htu. This is not a standard behavior as the\n // htu is not supposed to contain query or fragment.\n\n // NodeJS's `URL` normalizes the pathname.\n return normalizeHtuUrl(url)\n}\n\nfunction wrapInvalidDpopProofError(\n err: unknown,\n title: string,\n): InvalidDpopProofError {\n const msg =\n err instanceof JOSEError || err instanceof ValidationError\n ? `${title}: ${err.message}`\n : title\n return new InvalidDpopProofError(msg, err)\n}\n"]}
1
+ {"version":3,"file":"dpop-manager.js","sourceRoot":"","sources":["../../src/dpop/dpop-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,MAAM,CAAA;AAC7E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AACrE,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAC3C,OAAO,EACL,SAAS,EAET,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,iBAAiB,CAAA;AAGxB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,CAAA;AAE5B,OAAO,EAAE,SAAS,EAAmB,CAAA;AAErC,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C;;;;;OAKG;IACH,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpE,oBAAoB,EAAE,sBAAsB,CAAC,QAAQ,EAAE;CACxD,CAAC,CAAA;AAGF,MAAM,OAAO,WAAW;IAGtB,YAAY,UAA8B,EAAE;QAC1C,MAAM,EAAE,UAAU,EAAE,oBAAoB,EAAE,GACxC,wBAAwB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACzC,IAAI,CAAC,SAAS;YACZ,UAAU,KAAK,KAAK;gBAClB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,IAAI,SAAS,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAA;IACvD,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,CAAA;IAC/B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU,CACd,UAAkB,EAClB,OAAsB,EACtB,WAA0D,EAC1D,WAAoB;QAEpB,4CAA4C;QAC5C,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,SAAS,CAAC,yBAAyB,CAAC,CAAA;QAChD,CAAC;QAED,MAAM,KAAK,GAAG,YAAY,CAAC,WAAW,CAAC,CAAA;QACvC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAEvB,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE;YACvE,GAAG,EAAE,UAAU;YACf,WAAW,EAAE,EAAE,EAAE,iDAAiD;YAClE,cAAc,EAAE,kBAAkB,GAAG,GAAG;SACzC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACf,MAAM,yBAAyB,CAAC,GAAG,EAAE,6BAA6B,CAAC,CAAA;QACrE,CAAC,CAAC,CAAA;QAEF,mEAAmE;QACnE,2EAA2E;QAC3E,4CAA4C;QAE5C,+DAA+D;QAC/D,yBAAyB;QACzB,sBAAsB;QACtB,kEAAkE;QAClE,OAAO;QAEP,2EAA2E;QAC3E,oCAAoC;QACpC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,OAAO,CAAA;QAE7C,IAAI,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACrD,MAAM,IAAI,qBAAqB,CAAC,2BAA2B,CAAC,CAAA;QAC9D,CAAC;QAED,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YACpC,MAAM,IAAI,qBAAqB,CAAC,oBAAoB,CAAC,CAAA;QACvD,CAAC;QAED,yEAAyE;QACzE,IAAI,CAAC,GAAG,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,IAAI,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;QACxD,CAAC;QAED,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;YACpC,MAAM,IAAI,qBAAqB,CAAC,yBAAyB,CAAC,CAAA;QAC5D,CAAC;QAED,uEAAuE;QACvE,gEAAgE;QAChE,mEAAmE;QACnE,6BAA6B;QAC7B,EAAE;QACF,wGAAwG;QACxG,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;YACvD,MAAM,IAAI,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;QACxD,CAAC;QAED,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,iBAAiB,EAAE,CAAA;QAC/B,CAAC;QAED,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,iBAAiB,CAAC,uBAAuB,CAAC,CAAA;QACtD,CAAC;QAED,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,eAAe,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,EAAE,CAAA;YACzE,IAAI,GAAG,KAAK,eAAe,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClD,MAAM,IAAI,qBAAqB,CAAC,qBAAqB,CAAC,CAAA;YACxD,CAAC;QACH,CAAC;aAAM,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,qBAAqB,CAAC,8BAA8B,CAAC,CAAA;QACjE,CAAC;QAED,oEAAoE;QACpE,oCAAoC;QACpC,MAAM,GAAG,GAAG,eAAe,CAAC,GAAI,CAAA;QAChC,MAAM,GAAG,GAAG,MAAM,sBAAsB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACpE,MAAM,yBAAyB,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAA;QACjE,CAAC,CAAC,CAAA;QAEF,0EAA0E;QAC1E,UAAU;QACV,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAA;IAC9C,CAAC;CACF;AAED,SAAS,YAAY,CACnB,WAA0D;IAE1D,MAAM,UAAU,GAAG,WAAW,CAAC,MAAM,CAAC,CAAA;IACtC,QAAQ,OAAO,UAAU,EAAE,CAAC;QAC1B,KAAK,QAAQ;YACX,IAAI,UAAU;gBAAE,OAAO,UAAU,CAAA;YACjC,MAAM,IAAI,qBAAqB,CAAC,6BAA6B,CAAC,CAAA;QAChE,KAAK,QAAQ;YACX,yEAAyE;YACzE,uDAAuD;YACvD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC;gBAAE,OAAO,UAAU,CAAC,CAAC,CAAE,CAAA;YACnE,MAAM,IAAI,qBAAqB,CAAC,yCAAyC,CAAC,CAAA;QAC5E;YACE,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,eAAe,CAAC,GAAkB;IACzC,mEAAmE;IACnE,OAAO,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAA;AAClC,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;IACtB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,qBAAqB,CAAC,+BAA+B,CAAC,CAAA;IAClE,CAAC;IAED,4EAA4E;IAC5E,yEAAyE;IACzE,0BAA0B;IAE1B,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,qBAAqB,CAAC,yCAAyC,CAAC,CAAA;IAC5E,CAAC;IAED,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1D,MAAM,IAAI,qBAAqB,CAAC,kCAAkC,CAAC,CAAA;IACrE,CAAC;IAED,0EAA0E;IAC1E,2EAA2E;IAC3E,oDAAoD;IAEpD,0CAA0C;IAC1C,OAAO,eAAe,CAAC,GAAG,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,yBAAyB,CAChC,GAAY,EACZ,KAAa;IAEb,MAAM,GAAG,GACP,GAAG,YAAY,SAAS,IAAI,GAAG,YAAY,eAAe;QACxD,CAAC,CAAC,GAAG,KAAK,KAAK,GAAG,CAAC,OAAO,EAAE;QAC5B,CAAC,CAAC,KAAK,CAAA;IACX,OAAO,IAAI,qBAAqB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AAC5C,CAAC","sourcesContent":["import { createHash } from 'node:crypto'\nimport { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'\nimport { z } from 'zod'\nimport { ValidationError } from '@atproto/jwk'\nimport { DPOP_NONCE_MAX_AGE } from '../constants.js'\nimport { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'\nimport { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'\nimport { ifURL } from '../lib/util/cast.js'\nimport {\n DpopNonce,\n DpopSecret,\n dpopSecretSchema,\n rotationIntervalSchema,\n} from './dpop-nonce.js'\nimport { DpopProof } from './dpop-proof.js'\n\nconst { JOSEError } = errors\n\nexport { DpopNonce, type DpopSecret }\n\nexport const dpopManagerOptionsSchema = z.object({\n /**\n * Set this to `false` to disable the use of nonces in DPoP proofs. Set this\n * to a secret Uint8Array or hex encoded string to use a predictable seed for\n * all nonces (typically useful when multiple instances are running). Leave\n * undefined to generate a random seed at startup.\n */\n dpopSecret: z.union([z.literal(false), dpopSecretSchema]).optional(),\n dpopRotationInterval: rotationIntervalSchema.optional(),\n})\nexport type DpopManagerOptions = z.input<typeof dpopManagerOptionsSchema>\n\nexport class DpopManager {\n protected readonly dpopNonce?: DpopNonce\n\n constructor(options: DpopManagerOptions = {}) {\n const { dpopSecret, dpopRotationInterval } =\n dpopManagerOptionsSchema.parse(options)\n this.dpopNonce =\n dpopSecret === false\n ? undefined\n : new DpopNonce(dpopSecret, dpopRotationInterval)\n }\n\n nextNonce(): string | undefined {\n return this.dpopNonce?.next()\n }\n\n /**\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\n async checkProof(\n httpMethod: string,\n httpUrl: Readonly<URL>,\n httpHeaders: Record<string, undefined | string | string[]>,\n accessToken?: string,\n ): Promise<null | DpopProof> {\n // Fool proofing against use of empty string\n if (!httpMethod) {\n throw new TypeError('HTTP method is required')\n }\n\n const proof = extractProof(httpHeaders)\n if (!proof) return null\n\n const { protectedHeader, payload } = await jwtVerify(proof, EmbeddedJWK, {\n typ: 'dpop+jwt',\n maxTokenAge: 10, // Will ensure presence & validity of \"iat\" claim\n clockTolerance: DPOP_NONCE_MAX_AGE / 1e3,\n }).catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to verify DPoP proof')\n })\n\n // @NOTE For legacy & backwards compatibility reason, we cannot use\n // `jwtPayloadSchema` here as it will reject DPoP proofs containing a query\n // or fragment component in the \"htu\" claim.\n\n // const { ath, htm, htu, jti, nonce } = await jwtPayloadSchema\n // .parseAsync(payload)\n // .catch((err) => {\n // throw buildInvalidDpopProofError('Invalid DPoP proof', err)\n // })\n\n // @TODO Uncomment previous lines (and remove redundant checks bellow) once\n // we decide to drop legacy support.\n const { ath, htm, htu, jti, nonce } = payload\n\n if (nonce !== undefined && typeof nonce !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"nonce\" type')\n }\n\n if (!jti || typeof jti !== 'string') {\n throw new InvalidDpopProofError('DPoP \"jti\" missing')\n }\n\n // Note rfc9110#section-9.1 states that the method name is case-sensitive\n if (!htm || htm !== httpMethod) {\n throw new InvalidDpopProofError('DPoP \"htm\" mismatch')\n }\n\n if (!htu || typeof htu !== 'string') {\n throw new InvalidDpopProofError('Invalid DPoP \"htu\" type')\n }\n\n // > To reduce the likelihood of false negatives, servers SHOULD employ\n // > syntax-based normalization (Section 6.2.2 of [RFC3986]) and\n // > scheme-based normalization (Section 6.2.3 of [RFC3986]) before\n // > comparing the htu claim.\n //\n // RFC9449 section 4.3. Checking DPoP Proofs - https://datatracker.ietf.org/doc/html/rfc9449#section-4.3\n if (!htu || parseHtu(htu) !== normalizeHtuUrl(httpUrl)) {\n throw new InvalidDpopProofError('DPoP \"htu\" mismatch')\n }\n\n if (!nonce && this.dpopNonce) {\n throw new UseDpopNonceError()\n }\n\n if (nonce && !this.dpopNonce?.check(nonce)) {\n throw new UseDpopNonceError('DPoP \"nonce\" mismatch')\n }\n\n if (accessToken) {\n const accessTokenHash = createHash('sha256').update(accessToken).digest()\n if (ath !== accessTokenHash.toString('base64url')) {\n throw new InvalidDpopProofError('DPoP \"ath\" mismatch')\n }\n } else if (ath !== undefined) {\n throw new InvalidDpopProofError('DPoP \"ath\" claim not allowed')\n }\n\n // @NOTE we can assert there is a jwk because the jwtVerify used the\n // EmbeddedJWK key getter mechanism.\n const jwk = protectedHeader.jwk!\n const jkt = await calculateJwkThumbprint(jwk, 'sha256').catch((err) => {\n throw wrapInvalidDpopProofError(err, 'Failed to calculate jkt')\n })\n\n // @NOTE We freeze the proof to prevent accidental modification (esp. from\n // hooks).\n return Object.freeze({ jti, jkt, htm, htu })\n }\n}\n\nfunction extractProof(\n httpHeaders: Record<string, undefined | string | string[]>,\n): string | null {\n const dpopHeader = httpHeaders['dpop']\n switch (typeof dpopHeader) {\n case 'string':\n if (dpopHeader) return dpopHeader\n throw new InvalidDpopProofError('DPoP header cannot be empty')\n case 'object':\n // @NOTE the \"0\" case should never happen a node.js HTTP server will only\n // return an array if the header is set multiple times.\n if (dpopHeader.length === 1 && dpopHeader[0]) return dpopHeader[0]!\n throw new InvalidDpopProofError('DPoP header must contain a single proof')\n default:\n return null\n }\n}\n\n/**\n * Constructs the HTTP URI (htu) claim as defined in RFC9449.\n *\n * The htu claim is the normalized URL of the HTTP request, excluding the query\n * string and fragment. This function ensures that the URL is normalized by\n * removing the search and hash components, as well as by using an URL object to\n * simplify the pathname (e.g. removing dot segments).\n *\n * @returns The normalized URL as a string.\n * @see {@link https://datatracker.ietf.org/doc/html/rfc9449#section-4.3}\n */\nfunction normalizeHtuUrl(url: Readonly<URL>): string {\n // NodeJS's `URL` normalizes the pathname, so we can just use that.\n return url.origin + url.pathname\n}\n\nfunction parseHtu(htu: string): string {\n const url = ifURL(htu)\n if (!url) {\n throw new InvalidDpopProofError('DPoP \"htu\" is not a valid URL')\n }\n\n // @NOTE the checks bellow can be removed once once jwtPayloadSchema is used\n // to validate the DPoP proof payload as it already performs these checks\n // (though the htuSchema).\n\n if (url.password || url.username) {\n throw new InvalidDpopProofError('DPoP \"htu\" must not contain credentials')\n }\n\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new InvalidDpopProofError('DPoP \"htu\" must be http or https')\n }\n\n // @NOTE For legacy & backwards compatibility reason, we allow a query and\n // fragment in the DPoP proof's htu. This is not a standard behavior as the\n // htu is not supposed to contain query or fragment.\n\n // NodeJS's `URL` normalizes the pathname.\n return normalizeHtuUrl(url)\n}\n\nfunction wrapInvalidDpopProofError(\n err: unknown,\n title: string,\n): InvalidDpopProofError {\n const msg =\n err instanceof JOSEError || err instanceof ValidationError\n ? `${title}: ${err.message}`\n : title\n return new InvalidDpopProofError(msg, err)\n}\n"]}
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  export declare const rotationIntervalSchema: z.ZodNumber;
3
- export declare const secretBytesSchema: z.ZodEffects<z.ZodType<Uint8Array<ArrayBuffer>, z.ZodTypeDef, Uint8Array<ArrayBuffer>>, Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
3
+ export declare const secretBytesSchema: z.ZodEffects<z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>, Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>;
4
4
  export declare const secretHexSchema: z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>;
5
- export declare const dpopSecretSchema: z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<ArrayBuffer>, z.ZodTypeDef, Uint8Array<ArrayBuffer>>, Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>, z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>]>;
5
+ export declare const dpopSecretSchema: z.ZodUnion<[z.ZodEffects<z.ZodType<Uint8Array<ArrayBufferLike>, z.ZodTypeDef, Uint8Array<ArrayBufferLike>>, Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>, z.ZodEffects<z.ZodString, Uint8Array<ArrayBufferLike>, string>]>;
6
6
  export type DpopSecret = z.input<typeof dpopSecretSchema>;
7
7
  export declare class DpopNonce {
8
8
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"dpop-nonce.d.ts","sourceRoot":"","sources":["../../src/dpop/dpop-nonce.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,eAAO,MAAM,sBAAsB,aAIN,CAAA;AAI7B,eAAO,MAAM,iBAAiB,2IAI1B,CAAA;AAEJ,eAAO,MAAM,eAAe,gEAO8B,CAAA;AAE1D,eAAO,MAAM,gBAAgB,yNAAgD,CAAA;AAC7E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAEzD,qBAAa,SAAS;;gBAWlB,MAAM,GAAE,UAA4C,EACpD,gBAAgB,SAAwB;IAW1C;;OAEG;IACH,SAAS,KAAK,cAAc,WAE3B;IAED,SAAS,CAAC,MAAM;IA4BhB,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM;IAO1B,IAAI;IAKJ,KAAK,CAAC,KAAK,EAAE,MAAM;CAG3B"}
1
+ {"version":3,"file":"dpop-nonce.d.ts","sourceRoot":"","sources":["../../src/dpop/dpop-nonce.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAMvB,eAAO,MAAM,sBAAsB,aAIN,CAAA;AAI7B,eAAO,MAAM,iBAAiB,2JAI1B,CAAA;AAEJ,eAAO,MAAM,eAAe,gEAO8B,CAAA;AAE1D,eAAO,MAAM,gBAAgB,yOAAgD,CAAA;AAC7E,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AAEzD,qBAAa,SAAS;;gBAWlB,MAAM,GAAE,UAA4C,EACpD,gBAAgB,SAAwB;IAW1C;;OAEG;IACH,SAAS,KAAK,cAAc,WAE3B;IAED,SAAS,CAAC,MAAM;IA4BhB,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM;IAO1B,IAAI;IAKJ,KAAK,CAAC,KAAK,EAAE,MAAM;CAG3B"}