@atproto/bsky 0.0.198 → 0.0.200

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 (279) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/api/age-assurance/const.d.ts +11 -0
  3. package/dist/api/age-assurance/const.d.ts.map +1 -0
  4. package/dist/api/age-assurance/const.js +142 -0
  5. package/dist/api/age-assurance/const.js.map +1 -0
  6. package/dist/api/age-assurance/index.d.ts +4 -0
  7. package/dist/api/age-assurance/index.d.ts.map +1 -0
  8. package/dist/api/age-assurance/index.js +24 -0
  9. package/dist/api/age-assurance/index.js.map +1 -0
  10. package/dist/api/age-assurance/kws/age-verified.d.ts +109 -0
  11. package/dist/api/age-assurance/kws/age-verified.d.ts.map +1 -0
  12. package/dist/api/age-assurance/kws/age-verified.js +63 -0
  13. package/dist/api/age-assurance/kws/age-verified.js.map +1 -0
  14. package/dist/api/age-assurance/kws/const.d.ts +13 -0
  15. package/dist/api/age-assurance/kws/const.d.ts.map +1 -0
  16. package/dist/api/age-assurance/kws/const.js +36 -0
  17. package/dist/api/age-assurance/kws/const.js.map +1 -0
  18. package/dist/api/age-assurance/kws/external-payload.d.ts +75 -0
  19. package/dist/api/age-assurance/kws/external-payload.d.ts.map +1 -0
  20. package/dist/api/age-assurance/kws/external-payload.js +124 -0
  21. package/dist/api/age-assurance/kws/external-payload.js.map +1 -0
  22. package/dist/api/age-assurance/kws/external-payload.test.d.ts +2 -0
  23. package/dist/api/age-assurance/kws/external-payload.test.d.ts.map +1 -0
  24. package/dist/api/age-assurance/kws/external-payload.test.js +65 -0
  25. package/dist/api/age-assurance/kws/external-payload.test.js.map +1 -0
  26. package/dist/api/age-assurance/redirects/kws-age-verified.d.ts +4 -0
  27. package/dist/api/age-assurance/redirects/kws-age-verified.d.ts.map +1 -0
  28. package/dist/api/age-assurance/redirects/kws-age-verified.js +76 -0
  29. package/dist/api/age-assurance/redirects/kws-age-verified.js.map +1 -0
  30. package/dist/api/age-assurance/stash.d.ts +4 -0
  31. package/dist/api/age-assurance/stash.d.ts.map +1 -0
  32. package/dist/api/age-assurance/stash.js +19 -0
  33. package/dist/api/age-assurance/stash.js.map +1 -0
  34. package/dist/api/age-assurance/types.d.ts +10 -0
  35. package/dist/api/age-assurance/types.d.ts.map +1 -0
  36. package/dist/api/age-assurance/types.js +3 -0
  37. package/dist/api/age-assurance/types.js.map +1 -0
  38. package/dist/api/age-assurance/util.d.ts +15 -0
  39. package/dist/api/age-assurance/util.d.ts.map +1 -0
  40. package/dist/api/age-assurance/util.js +54 -0
  41. package/dist/api/age-assurance/util.js.map +1 -0
  42. package/dist/api/age-assurance/webhooks/kws-age-verified.d.ts +4 -0
  43. package/dist/api/age-assurance/webhooks/kws-age-verified.d.ts.map +1 -0
  44. package/dist/api/age-assurance/webhooks/kws-age-verified.js +63 -0
  45. package/dist/api/age-assurance/webhooks/kws-age-verified.js.map +1 -0
  46. package/dist/api/app/bsky/ageassurance/begin.d.ts +4 -0
  47. package/dist/api/app/bsky/ageassurance/begin.d.ts.map +1 -0
  48. package/dist/api/app/bsky/ageassurance/begin.js +131 -0
  49. package/dist/api/app/bsky/ageassurance/begin.js.map +1 -0
  50. package/dist/api/app/bsky/ageassurance/getConfig.d.ts +4 -0
  51. package/dist/api/app/bsky/ageassurance/getConfig.d.ts.map +1 -0
  52. package/dist/api/app/bsky/ageassurance/getConfig.js +16 -0
  53. package/dist/api/app/bsky/ageassurance/getConfig.js.map +1 -0
  54. package/dist/api/app/bsky/ageassurance/getState.d.ts +4 -0
  55. package/dist/api/app/bsky/ageassurance/getState.d.ts.map +1 -0
  56. package/dist/api/app/bsky/ageassurance/getState.js +42 -0
  57. package/dist/api/app/bsky/ageassurance/getState.js.map +1 -0
  58. package/dist/api/app/bsky/contact/dismissMatch.d.ts +4 -0
  59. package/dist/api/app/bsky/contact/dismissMatch.d.ts.map +1 -0
  60. package/dist/api/app/bsky/contact/dismissMatch.js +23 -0
  61. package/dist/api/app/bsky/contact/dismissMatch.js.map +1 -0
  62. package/dist/api/app/bsky/contact/getMatches.d.ts +4 -0
  63. package/dist/api/app/bsky/contact/getMatches.d.ts.map +1 -0
  64. package/dist/api/app/bsky/contact/getMatches.js +59 -0
  65. package/dist/api/app/bsky/contact/getMatches.js.map +1 -0
  66. package/dist/api/app/bsky/contact/getSyncStatus.d.ts +4 -0
  67. package/dist/api/app/bsky/contact/getSyncStatus.d.ts.map +1 -0
  68. package/dist/api/app/bsky/contact/getSyncStatus.js +32 -0
  69. package/dist/api/app/bsky/contact/getSyncStatus.js.map +1 -0
  70. package/dist/api/app/bsky/contact/importContacts.d.ts +4 -0
  71. package/dist/api/app/bsky/contact/importContacts.d.ts.map +1 -0
  72. package/dist/api/app/bsky/contact/importContacts.js +62 -0
  73. package/dist/api/app/bsky/contact/importContacts.js.map +1 -0
  74. package/dist/api/app/bsky/contact/removeData.d.ts +4 -0
  75. package/dist/api/app/bsky/contact/removeData.d.ts.map +1 -0
  76. package/dist/api/app/bsky/contact/removeData.js +22 -0
  77. package/dist/api/app/bsky/contact/removeData.js.map +1 -0
  78. package/dist/api/app/bsky/contact/startPhoneVerification.d.ts +4 -0
  79. package/dist/api/app/bsky/contact/startPhoneVerification.d.ts.map +1 -0
  80. package/dist/api/app/bsky/contact/startPhoneVerification.js +23 -0
  81. package/dist/api/app/bsky/contact/startPhoneVerification.js.map +1 -0
  82. package/dist/api/app/bsky/contact/util.d.ts +6 -0
  83. package/dist/api/app/bsky/contact/util.d.ts.map +1 -0
  84. package/dist/api/app/bsky/contact/util.js +10 -0
  85. package/dist/api/app/bsky/contact/util.js.map +1 -0
  86. package/dist/api/app/bsky/contact/verifyPhone.d.ts +4 -0
  87. package/dist/api/app/bsky/contact/verifyPhone.d.ts.map +1 -0
  88. package/dist/api/app/bsky/contact/verifyPhone.js +26 -0
  89. package/dist/api/app/bsky/contact/verifyPhone.js.map +1 -0
  90. package/dist/api/app/bsky/graph/getRelationships.d.ts.map +1 -1
  91. package/dist/api/app/bsky/graph/getRelationships.js +4 -0
  92. package/dist/api/app/bsky/graph/getRelationships.js.map +1 -1
  93. package/dist/api/external.d.ts.map +1 -1
  94. package/dist/api/external.js +2 -0
  95. package/dist/api/external.js.map +1 -1
  96. package/dist/api/index.d.ts.map +1 -1
  97. package/dist/api/index.js +22 -2
  98. package/dist/api/index.js.map +1 -1
  99. package/dist/api/kws/api.d.ts.map +1 -1
  100. package/dist/api/kws/api.js +44 -26
  101. package/dist/api/kws/api.js.map +1 -1
  102. package/dist/api/kws/index.d.ts.map +1 -1
  103. package/dist/api/kws/index.js +3 -1
  104. package/dist/api/kws/index.js.map +1 -1
  105. package/dist/api/kws/webhook.d.ts +3 -1
  106. package/dist/api/kws/webhook.d.ts.map +1 -1
  107. package/dist/api/kws/webhook.js +48 -20
  108. package/dist/api/kws/webhook.js.map +1 -1
  109. package/dist/config.d.ts +22 -0
  110. package/dist/config.d.ts.map +1 -1
  111. package/dist/config.js +31 -2
  112. package/dist/config.js.map +1 -1
  113. package/dist/context.d.ts +3 -0
  114. package/dist/context.d.ts.map +1 -1
  115. package/dist/context.js +3 -0
  116. package/dist/context.js.map +1 -1
  117. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  118. package/dist/data-plane/bsync/index.js +22 -0
  119. package/dist/data-plane/bsync/index.js.map +1 -1
  120. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.d.ts +4 -0
  121. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.d.ts.map +1 -0
  122. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.js +30 -0
  123. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.js.map +1 -0
  124. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  125. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  126. package/dist/data-plane/server/db/migrations/index.js +2 -1
  127. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  128. package/dist/data-plane/server/db/pagination.d.ts +3 -3
  129. package/dist/data-plane/server/db/tables/actor.d.ts +3 -0
  130. package/dist/data-plane/server/db/tables/actor.d.ts.map +1 -1
  131. package/dist/data-plane/server/db/tables/actor.js.map +1 -1
  132. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  133. package/dist/data-plane/server/routes/profile.js +13 -1
  134. package/dist/data-plane/server/routes/profile.js.map +1 -1
  135. package/dist/hydration/actor.js +1 -1
  136. package/dist/hydration/actor.js.map +1 -1
  137. package/dist/hydration/hydrator.js +1 -1
  138. package/dist/hydration/hydrator.js.map +1 -1
  139. package/dist/index.d.ts.map +1 -1
  140. package/dist/index.js +12 -0
  141. package/dist/index.js.map +1 -1
  142. package/dist/kws.d.ts +35 -0
  143. package/dist/kws.d.ts.map +1 -1
  144. package/dist/kws.js +54 -0
  145. package/dist/kws.js.map +1 -1
  146. package/dist/lexicon/index.d.ts +19 -0
  147. package/dist/lexicon/index.d.ts.map +1 -1
  148. package/dist/lexicon/index.js +48 -1
  149. package/dist/lexicon/index.js.map +1 -1
  150. package/dist/lexicon/lexicons.d.ts +664 -0
  151. package/dist/lexicon/lexicons.d.ts.map +1 -1
  152. package/dist/lexicon/lexicons.js +354 -0
  153. package/dist/lexicon/lexicons.js.map +1 -1
  154. package/dist/lexicon/types/app/bsky/contact/defs.d.ts +24 -0
  155. package/dist/lexicon/types/app/bsky/contact/defs.d.ts.map +1 -0
  156. package/dist/lexicon/types/app/bsky/contact/defs.js +25 -0
  157. package/dist/lexicon/types/app/bsky/contact/defs.js.map +1 -0
  158. package/dist/lexicon/types/app/bsky/contact/dismissMatch.d.ts +25 -0
  159. package/dist/lexicon/types/app/bsky/contact/dismissMatch.d.ts.map +1 -0
  160. package/dist/lexicon/types/app/bsky/contact/dismissMatch.js +7 -0
  161. package/dist/lexicon/types/app/bsky/contact/dismissMatch.js.map +1 -0
  162. package/dist/lexicon/types/app/bsky/contact/getMatches.d.ts +25 -0
  163. package/dist/lexicon/types/app/bsky/contact/getMatches.d.ts.map +1 -0
  164. package/dist/lexicon/types/app/bsky/contact/getMatches.js +7 -0
  165. package/dist/lexicon/types/app/bsky/contact/getMatches.js.map +1 -0
  166. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.d.ts +21 -0
  167. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.d.ts.map +1 -0
  168. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.js +7 -0
  169. package/dist/lexicon/types/app/bsky/contact/getSyncStatus.js.map +1 -0
  170. package/dist/lexicon/types/app/bsky/contact/importContacts.d.ts +30 -0
  171. package/dist/lexicon/types/app/bsky/contact/importContacts.d.ts.map +1 -0
  172. package/dist/lexicon/types/app/bsky/contact/importContacts.js +7 -0
  173. package/dist/lexicon/types/app/bsky/contact/importContacts.js.map +1 -0
  174. package/dist/lexicon/types/app/bsky/contact/removeData.d.ts +23 -0
  175. package/dist/lexicon/types/app/bsky/contact/removeData.d.ts.map +1 -0
  176. package/dist/lexicon/types/app/bsky/contact/removeData.js +7 -0
  177. package/dist/lexicon/types/app/bsky/contact/removeData.js.map +1 -0
  178. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.d.ts +25 -0
  179. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.d.ts.map +1 -0
  180. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.js +7 -0
  181. package/dist/lexicon/types/app/bsky/contact/startPhoneVerification.js.map +1 -0
  182. package/dist/lexicon/types/app/bsky/contact/verifyPhone.d.ts +29 -0
  183. package/dist/lexicon/types/app/bsky/contact/verifyPhone.d.ts.map +1 -0
  184. package/dist/lexicon/types/app/bsky/contact/verifyPhone.js +7 -0
  185. package/dist/lexicon/types/app/bsky/contact/verifyPhone.js.map +1 -0
  186. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +8 -0
  187. package/dist/lexicon/types/app/bsky/graph/defs.d.ts.map +1 -1
  188. package/dist/lexicon/types/app/bsky/graph/defs.js.map +1 -1
  189. package/dist/logger.d.ts +1 -0
  190. package/dist/logger.d.ts.map +1 -1
  191. package/dist/logger.js +2 -1
  192. package/dist/logger.js.map +1 -1
  193. package/dist/proto/bsky_pb.d.ts +4 -0
  194. package/dist/proto/bsky_pb.d.ts.map +1 -1
  195. package/dist/proto/bsky_pb.js +10 -0
  196. package/dist/proto/bsky_pb.js.map +1 -1
  197. package/dist/proto/rolodex_connect.d.ts +83 -0
  198. package/dist/proto/rolodex_connect.d.ts.map +1 -0
  199. package/dist/proto/rolodex_connect.js +90 -0
  200. package/dist/proto/rolodex_connect.js.map +1 -0
  201. package/dist/proto/rolodex_pb.d.ts +363 -0
  202. package/dist/proto/rolodex_pb.d.ts.map +1 -0
  203. package/dist/proto/rolodex_pb.js +1032 -0
  204. package/dist/proto/rolodex_pb.js.map +1 -0
  205. package/dist/rolodex.d.ts +9 -0
  206. package/dist/rolodex.d.ts.map +1 -0
  207. package/dist/rolodex.js +25 -0
  208. package/dist/rolodex.js.map +1 -0
  209. package/dist/stash.d.ts +1 -0
  210. package/dist/stash.d.ts.map +1 -1
  211. package/dist/stash.js +1 -0
  212. package/dist/stash.js.map +1 -1
  213. package/dist/util/uris.d.ts +2 -2
  214. package/dist/util/uris.d.ts.map +1 -1
  215. package/package.json +16 -15
  216. package/proto/bsky.proto +1 -0
  217. package/proto/rolodex.proto +116 -0
  218. package/src/api/age-assurance/const.ts +142 -0
  219. package/src/api/age-assurance/index.ts +34 -0
  220. package/src/api/age-assurance/kws/age-verified.ts +75 -0
  221. package/src/api/age-assurance/kws/const.ts +33 -0
  222. package/src/api/age-assurance/kws/external-payload.test.ts +72 -0
  223. package/src/api/age-assurance/kws/external-payload.ts +149 -0
  224. package/src/api/age-assurance/redirects/kws-age-verified.ts +107 -0
  225. package/src/api/age-assurance/stash.ts +22 -0
  226. package/src/api/age-assurance/types.ts +10 -0
  227. package/src/api/age-assurance/util.ts +66 -0
  228. package/src/api/age-assurance/webhooks/kws-age-verified.ts +75 -0
  229. package/src/api/app/bsky/ageassurance/begin.ts +167 -0
  230. package/src/api/app/bsky/ageassurance/getConfig.ts +15 -0
  231. package/src/api/app/bsky/ageassurance/getState.ts +53 -0
  232. package/src/api/app/bsky/contact/dismissMatch.ts +24 -0
  233. package/src/api/app/bsky/contact/getMatches.ts +111 -0
  234. package/src/api/app/bsky/contact/getSyncStatus.ts +35 -0
  235. package/src/api/app/bsky/contact/importContacts.ts +118 -0
  236. package/src/api/app/bsky/contact/removeData.ts +23 -0
  237. package/src/api/app/bsky/contact/startPhoneVerification.ts +24 -0
  238. package/src/api/app/bsky/contact/util.ts +13 -0
  239. package/src/api/app/bsky/contact/verifyPhone.ts +27 -0
  240. package/src/api/app/bsky/graph/getRelationships.ts +4 -0
  241. package/src/api/external.ts +2 -0
  242. package/src/api/index.ts +20 -0
  243. package/src/api/kws/api.ts +55 -34
  244. package/src/api/kws/index.ts +7 -1
  245. package/src/api/kws/webhook.ts +57 -34
  246. package/src/config.ts +53 -2
  247. package/src/context.ts +6 -0
  248. package/src/data-plane/bsync/index.ts +31 -0
  249. package/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts +28 -0
  250. package/src/data-plane/server/db/migrations/index.ts +1 -0
  251. package/src/data-plane/server/db/tables/actor.ts +3 -0
  252. package/src/data-plane/server/routes/profile.ts +12 -1
  253. package/src/hydration/actor.ts +1 -1
  254. package/src/hydration/hydrator.ts +1 -1
  255. package/src/index.ts +13 -0
  256. package/src/kws.ts +81 -0
  257. package/src/lexicon/index.ts +101 -0
  258. package/src/lexicon/lexicons.ts +375 -0
  259. package/src/lexicon/types/app/bsky/contact/defs.ts +52 -0
  260. package/src/lexicon/types/app/bsky/contact/dismissMatch.ts +43 -0
  261. package/src/lexicon/types/app/bsky/contact/getMatches.ts +43 -0
  262. package/src/lexicon/types/app/bsky/contact/getSyncStatus.ts +39 -0
  263. package/src/lexicon/types/app/bsky/contact/importContacts.ts +49 -0
  264. package/src/lexicon/types/app/bsky/contact/removeData.ts +40 -0
  265. package/src/lexicon/types/app/bsky/contact/startPhoneVerification.ts +43 -0
  266. package/src/lexicon/types/app/bsky/contact/verifyPhone.ts +48 -0
  267. package/src/lexicon/types/app/bsky/graph/defs.ts +8 -0
  268. package/src/logger.ts +2 -0
  269. package/src/proto/bsky_pb.ts +6 -0
  270. package/src/proto/rolodex_connect.ts +89 -0
  271. package/src/proto/rolodex_pb.ts +746 -0
  272. package/src/rolodex.ts +42 -0
  273. package/src/stash.ts +3 -0
  274. package/tests/views/__snapshots__/profile.test.ts.snap +103 -0
  275. package/tests/views/age-assurance-v2.test.ts +745 -0
  276. package/tests/views/age-assurance.test.ts +2 -0
  277. package/tests/views/profile.test.ts +39 -0
  278. package/tsconfig.build.tsbuildinfo +1 -1
  279. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -1,42 +1,41 @@
1
1
  import express, { RequestHandler } from 'express'
2
2
  import { httpLogger as log } from '../../logger'
3
+ import { AGE_ASSURANCE_CONFIG } from '../age-assurance/const'
3
4
  import {
4
- AppContextWithKwsClient,
5
- KwsVerificationIntermediateQuery,
6
- KwsVerificationQuery,
7
- verificationIntermediateQuerySchema,
8
- } from './types'
5
+ KWSExternalPayloadVersion,
6
+ parseKWSExternalPayloadV1WithV2Compat,
7
+ } from '../age-assurance/kws/external-payload'
8
+ import { createEvent } from '../age-assurance/stash'
9
+ import { computeAgeAssuranceAccessOrThrow } from '../age-assurance/util'
10
+ import { AppContextWithKwsClient } from './types'
9
11
  import {
10
12
  createStashEvent,
11
13
  getClientUa,
12
- parseExternalPayload,
13
14
  parseStatus,
14
15
  validateSignature,
15
16
  } from './util'
16
17
 
17
- const validateRequest = (
18
+ function parseQueryParams(
18
19
  ctx: AppContextWithKwsClient,
19
20
  req: express.Request,
20
- ): KwsVerificationQuery => {
21
+ ): {
22
+ status: string
23
+ externalPayload: string
24
+ } {
21
25
  try {
22
- const intermediate: KwsVerificationIntermediateQuery =
23
- verificationIntermediateQuerySchema.parse({
24
- externalPayload: req.query.externalPayload,
25
- signature: req.query.signature,
26
- status: req.query.status,
27
- })
26
+ const status = String(req.query.status)
27
+ const externalPayload = String(req.query.externalPayload)
28
+ const signature = String(req.query.signature)
28
29
 
29
- const data = `${intermediate.status}:${intermediate.externalPayload}`
30
30
  validateSignature(
31
31
  ctx.cfg.kws.verificationSecret,
32
- data,
33
- intermediate.signature,
32
+ `${status}:${externalPayload}`,
33
+ signature,
34
34
  )
35
35
 
36
36
  return {
37
- ...intermediate,
38
- externalPayload: parseExternalPayload(intermediate.externalPayload),
39
- status: parseStatus(intermediate.status),
37
+ status,
38
+ externalPayload,
40
39
  }
41
40
  } catch (err) {
42
41
  throw new Error('Invalid KWS API request', { cause: err })
@@ -48,29 +47,51 @@ export const verificationHandler =
48
47
  async (req: express.Request, res: express.Response) => {
49
48
  let actorDid: string | undefined
50
49
  try {
51
- const query = validateRequest(ctx, req)
52
- const {
53
- externalPayload,
54
- status: { verified },
55
- } = query
50
+ const query = parseQueryParams(ctx, req)
51
+ const { verified } = parseStatus(query.status)
56
52
  if (!verified) {
57
53
  throw new Error(
58
54
  'Unexpected KWS verification response call with unverified status',
59
55
  )
60
56
  }
61
57
 
62
- const { actorDid: externalPayloadActorDid, attemptId } = externalPayload
63
- actorDid = externalPayloadActorDid
64
58
  // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
65
59
  const completeIp = req.ip
66
60
  const completeUa = getClientUa(req)
67
- await createStashEvent(ctx, {
68
- actorDid,
69
- attemptId,
70
- completeIp,
71
- completeUa,
72
- status: 'assured',
73
- })
61
+ const externalPayload = parseKWSExternalPayloadV1WithV2Compat(
62
+ query.externalPayload,
63
+ )
64
+ actorDid = externalPayload.actorDid
65
+
66
+ if (externalPayload.version === KWSExternalPayloadVersion.V2) {
67
+ const { countryCode, regionCode, attemptId } = externalPayload
68
+ const { access } = computeAgeAssuranceAccessOrThrow(
69
+ AGE_ASSURANCE_CONFIG,
70
+ {
71
+ countryCode: countryCode,
72
+ regionCode: regionCode,
73
+ verifiedMinimumAge: 18, // `adult-verified` is 18+ only
74
+ },
75
+ )
76
+ await createEvent(ctx, actorDid, {
77
+ attemptId,
78
+ status: 'assured',
79
+ access,
80
+ countryCode,
81
+ regionCode,
82
+ completeIp,
83
+ completeUa,
84
+ })
85
+ } else {
86
+ await createStashEvent(ctx, {
87
+ actorDid,
88
+ attemptId: externalPayload.attemptId,
89
+ status: 'assured',
90
+ completeIp,
91
+ completeUa,
92
+ })
93
+ }
94
+
74
95
  return res
75
96
  .status(302)
76
97
  .setHeader(
@@ -9,7 +9,13 @@ export const createRouter = (ctx: AppContext): Router => {
9
9
 
10
10
  const router = Router()
11
11
  router.use(raw({ type: 'application/json' }))
12
- router.post('/age-assurance-webhook', webhookAuth(ctx), webhookHandler(ctx))
12
+ router.post(
13
+ '/age-assurance-webhook',
14
+ webhookAuth({
15
+ secret: ctx.cfg.kws.webhookSecret,
16
+ }),
17
+ webhookHandler(ctx),
18
+ )
13
19
  router.get('/age-assurance-verification', verificationHandler(ctx))
14
20
  return router
15
21
  }
@@ -1,19 +1,21 @@
1
1
  import express, { RequestHandler } from 'express'
2
2
  import { httpLogger as log } from '../../logger'
3
+ import { AGE_ASSURANCE_CONFIG } from '../age-assurance/const'
4
+ import {
5
+ KWSExternalPayloadVersion,
6
+ parseKWSExternalPayloadV1WithV2Compat,
7
+ } from '../age-assurance/kws/external-payload'
8
+ import { createEvent } from '../age-assurance/stash'
9
+ import { computeAgeAssuranceAccessOrThrow } from '../age-assurance/util'
3
10
  import {
4
11
  AppContextWithKwsClient,
5
12
  KwsWebhookBody,
6
13
  webhookBodyIntermediateSchema,
7
14
  } from './types'
8
- import {
9
- createStashEvent,
10
- kwsWwwAuthenticate,
11
- parseExternalPayload,
12
- validateSignature,
13
- } from './util'
15
+ import { createStashEvent, kwsWwwAuthenticate, validateSignature } from './util'
14
16
 
15
17
  export const webhookAuth =
16
- (ctx: AppContextWithKwsClient): RequestHandler =>
18
+ ({ secret }: { secret: string }): RequestHandler =>
17
19
  (req: express.Request, res: express.Response, next: express.NextFunction) => {
18
20
  const body: Buffer = req.body
19
21
  const sigHeader = req.headers['x-kws-signature']
@@ -34,7 +36,7 @@ export const webhookAuth =
34
36
  }
35
37
 
36
38
  const data = `${timestamp}.${body}`
37
- validateSignature(ctx.cfg.kws.webhookSecret, data, signature)
39
+ validateSignature(secret, data, signature)
38
40
  next()
39
41
  } catch (err) {
40
42
  log.error({ err }, 'Invalid KWS webhook signature')
@@ -51,21 +53,10 @@ type AgeAssuranceWebhookIntermediateBody = {
51
53
  }
52
54
  }
53
55
 
54
- const parseBody = (serialized: string): KwsWebhookBody => {
56
+ const parseBody = (serialized: string): AgeAssuranceWebhookIntermediateBody => {
55
57
  try {
56
58
  const value: unknown = JSON.parse(serialized)
57
- const intermediate: AgeAssuranceWebhookIntermediateBody =
58
- webhookBodyIntermediateSchema.parse(value)
59
-
60
- return {
61
- ...intermediate,
62
- payload: {
63
- ...intermediate.payload,
64
- externalPayload: parseExternalPayload(
65
- intermediate.payload.externalPayload,
66
- ),
67
- },
68
- }
59
+ return webhookBodyIntermediateSchema.parse(value)
69
60
  } catch (err) {
70
61
  throw new Error(`Invalid webhook body: ${serialized}`, { cause: err })
71
62
  }
@@ -74,7 +65,7 @@ const parseBody = (serialized: string): KwsWebhookBody => {
74
65
  export const webhookHandler =
75
66
  (ctx: AppContextWithKwsClient): RequestHandler =>
76
67
  async (req: express.Request, res: express.Response) => {
77
- let body: KwsWebhookBody
68
+ let body: AgeAssuranceWebhookIntermediateBody
78
69
  try {
79
70
  body = parseBody(req.body)
80
71
  } catch (err) {
@@ -82,23 +73,55 @@ export const webhookHandler =
82
73
  return res.status(400).json(err)
83
74
  }
84
75
 
85
- const {
86
- payload: {
87
- status: { verified },
88
- externalPayload,
89
- },
90
- } = body
91
- const { actorDid, attemptId } = externalPayload
76
+ const { verified } = body.payload.status
92
77
  if (!verified) {
93
78
  throw new Error('Unexpected KWS webhook call with unverified status')
94
79
  }
95
80
 
81
+ const externalPayload = parseKWSExternalPayloadV1WithV2Compat(
82
+ body.payload.externalPayload,
83
+ )
84
+ const isV2 = externalPayload.version === KWSExternalPayloadVersion.V2
85
+
86
+ let result: ReturnType<typeof computeAgeAssuranceAccessOrThrow> | undefined
87
+ if (isV2) {
88
+ const { attemptId, actorDid, countryCode, regionCode } = externalPayload
89
+ try {
90
+ result = computeAgeAssuranceAccessOrThrow(AGE_ASSURANCE_CONFIG, {
91
+ countryCode: countryCode,
92
+ regionCode: regionCode,
93
+ verifiedMinimumAge: 18, // `adult-verified` is 18+ only
94
+ })
95
+ } catch (err) {
96
+ // internal errors
97
+ log.error(
98
+ { err, attemptId, actorDid, countryCode, regionCode },
99
+ 'Failed to compute age assurance access',
100
+ )
101
+ }
102
+ }
103
+
96
104
  try {
97
- await createStashEvent(ctx, {
98
- actorDid,
99
- attemptId,
100
- status: 'assured',
101
- })
105
+ if (isV2) {
106
+ if (result) {
107
+ const { attemptId, actorDid, countryCode, regionCode } =
108
+ externalPayload
109
+ await createEvent(ctx, actorDid, {
110
+ attemptId,
111
+ status: 'assured',
112
+ access: result.access,
113
+ countryCode,
114
+ regionCode,
115
+ })
116
+ } // else do nothing
117
+ } else {
118
+ const { attemptId, actorDid } = externalPayload
119
+ await createStashEvent(ctx, {
120
+ attemptId: attemptId,
121
+ actorDid: actorDid,
122
+ status: 'assured',
123
+ })
124
+ }
102
125
  return res.status(200).end()
103
126
  } catch (err) {
104
127
  log.error({ err }, 'Failed to handle KWS webhook')
package/src/config.ts CHANGED
@@ -13,8 +13,22 @@ export interface KwsConfig {
13
13
  clientId: string
14
14
  redirectUrl: string
15
15
  userAgent: string
16
+ /**
17
+ * V1 secret used to validate `adult-verifieid` redirects
18
+ */
16
19
  verificationSecret: string
20
+ /**
21
+ * V1 secret used to validate `adult-verified` webhooks
22
+ */
17
23
  webhookSecret: string
24
+ /**
25
+ * V2 secret used to validate `age-verified` webhooks
26
+ */
27
+ ageVerifiedWebhookSecret: string
28
+ /**
29
+ * V2 secret used to validate `age-verified` redirects
30
+ */
31
+ ageVerifiedRedirectSecret: string
18
32
  }
19
33
 
20
34
  export interface ServerConfigValues {
@@ -41,6 +55,10 @@ export interface ServerConfigValues {
41
55
  courierApiKey?: string
42
56
  courierHttpVersion?: '1.1' | '2'
43
57
  courierIgnoreBadTls?: boolean
58
+ rolodexUrl?: string
59
+ rolodexApiKey?: string
60
+ rolodexHttpVersion?: '1.1' | '2'
61
+ rolodexIgnoreBadTls?: boolean
44
62
  searchUrl?: string
45
63
  searchTagsHide: Set<string>
46
64
  suggestionsUrl?: string
@@ -172,6 +190,12 @@ export class ServerConfig {
172
190
  const courierIgnoreBadTls =
173
191
  process.env.BSKY_COURIER_IGNORE_BAD_TLS === 'true'
174
192
  assert(courierHttpVersion === '1.1' || courierHttpVersion === '2')
193
+ const rolodexUrl = process.env.BSKY_ROLODEX_URL || undefined
194
+ const rolodexApiKey = process.env.BSKY_ROLODEX_API_KEY || undefined
195
+ const rolodexHttpVersion = process.env.BSKY_ROLODEX_HTTP_VERSION || '2'
196
+ const rolodexIgnoreBadTls =
197
+ process.env.BSKY_ROLODEX_IGNORE_BAD_TLS === 'true'
198
+ assert(rolodexHttpVersion === '1.1' || rolodexHttpVersion === '2')
175
199
  const blobRateLimitBypassKey =
176
200
  process.env.BSKY_BLOB_RATE_LIMIT_BYPASS_KEY || undefined
177
201
  // single domain would be e.g. "mypds.com", subdomains are supported with a leading dot e.g. ".mypds.com"
@@ -251,6 +275,10 @@ export class ServerConfig {
251
275
  const kwsUserAgent = process.env.BSKY_KWS_USER_AGENT
252
276
  const kwsVerificationSecret = process.env.BSKY_KWS_VERIFICATION_SECRET
253
277
  const kwsWebhookSecret = process.env.BSKY_KWS_WEBHOOK_SECRET
278
+ const kwsAgeVerifiedWebhookSecret =
279
+ process.env.BSKY_KWS_AGE_VERIFIED_WEBHOOK_SECRET
280
+ const kwsAgeVerifiedRedirectSecret =
281
+ process.env.BSKY_KWS_AGE_VERIFIED_REDIRECT_SECRET
254
282
  if (
255
283
  kwsApiKey ||
256
284
  kwsApiOrigin ||
@@ -259,7 +287,9 @@ export class ServerConfig {
259
287
  kwsRedirectUrl ||
260
288
  kwsUserAgent ||
261
289
  kwsVerificationSecret ||
262
- kwsWebhookSecret
290
+ kwsWebhookSecret ||
291
+ kwsAgeVerifiedWebhookSecret ||
292
+ kwsAgeVerifiedRedirectSecret
263
293
  ) {
264
294
  assert(
265
295
  kwsApiOrigin &&
@@ -269,7 +299,9 @@ export class ServerConfig {
269
299
  kwsUserAgent &&
270
300
  kwsVerificationSecret &&
271
301
  kwsWebhookSecret &&
272
- kwsApiKey,
302
+ kwsApiKey &&
303
+ kwsAgeVerifiedWebhookSecret &&
304
+ kwsAgeVerifiedRedirectSecret,
273
305
  'all KWS environment variables must be set if any are set',
274
306
  )
275
307
  kws = {
@@ -281,6 +313,8 @@ export class ServerConfig {
281
313
  userAgent: kwsUserAgent,
282
314
  verificationSecret: kwsVerificationSecret,
283
315
  webhookSecret: kwsWebhookSecret,
316
+ ageVerifiedWebhookSecret: kwsAgeVerifiedWebhookSecret,
317
+ ageVerifiedRedirectSecret: kwsAgeVerifiedRedirectSecret,
284
318
  }
285
319
  }
286
320
 
@@ -323,6 +357,10 @@ export class ServerConfig {
323
357
  courierApiKey,
324
358
  courierHttpVersion,
325
359
  courierIgnoreBadTls,
360
+ rolodexUrl,
361
+ rolodexApiKey,
362
+ rolodexHttpVersion,
363
+ rolodexIgnoreBadTls,
326
364
  blobRateLimitBypassKey,
327
365
  blobRateLimitBypassHostname,
328
366
  adminPasswords,
@@ -446,6 +484,19 @@ export class ServerConfig {
446
484
  return this.cfg.courierIgnoreBadTls
447
485
  }
448
486
 
487
+ get rolodexUrl() {
488
+ return this.cfg.rolodexUrl
489
+ }
490
+ get rolodexApiKey() {
491
+ return this.cfg.rolodexApiKey
492
+ }
493
+ get rolodexHttpVersion() {
494
+ return this.cfg.rolodexHttpVersion
495
+ }
496
+ get rolodexIgnoreBadTls() {
497
+ return this.cfg.rolodexIgnoreBadTls
498
+ }
499
+
449
500
  get searchUrl() {
450
501
  return this.cfg.searchUrl
451
502
  }
package/src/context.ts CHANGED
@@ -14,6 +14,7 @@ import { FeatureGates } from './feature-gates'
14
14
  import { Hydrator } from './hydration/hydrator'
15
15
  import { KwsClient } from './kws'
16
16
  import { httpLogger as log } from './logger'
17
+ import { RolodexClient } from './rolodex'
17
18
  import { StashClient } from './stash'
18
19
  import {
19
20
  ParsedLabelers,
@@ -39,6 +40,7 @@ export class AppContext {
39
40
  bsyncClient: BsyncClient
40
41
  stashClient: StashClient
41
42
  courierClient: CourierClient | undefined
43
+ rolodexClient: RolodexClient | undefined
42
44
  authVerifier: AuthVerifier
43
45
  featureGates: FeatureGates
44
46
  blobDispatcher: Dispatcher
@@ -106,6 +108,10 @@ export class AppContext {
106
108
  return this.opts.courierClient
107
109
  }
108
110
 
111
+ get rolodexClient(): RolodexClient | undefined {
112
+ return this.opts.rolodexClient
113
+ }
114
+
109
115
  get authVerifier(): AuthVerifier {
110
116
  return this.opts.authVerifier
111
117
  }
@@ -8,6 +8,7 @@ import { TID } from '@atproto/common'
8
8
  import { jsonStringToLex } from '@atproto/lexicon'
9
9
  import { AtUri } from '@atproto/syntax'
10
10
  import { ids } from '../../lexicon/lexicons'
11
+ import { Event as AgeAssuranceV2Event } from '../../lexicon/types/app/bsky/ageassurance/defs'
11
12
  import { Bookmark } from '../../lexicon/types/app/bsky/bookmark/defs'
12
13
  import { SubjectActivitySubscription } from '../../lexicon/types/app/bsky/notification/defs'
13
14
  import { AgeAssuranceEvent } from '../../lexicon/types/app/bsky/unspecced/defs'
@@ -175,6 +176,8 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
175
176
  namespace === Namespaces.AppBskyUnspeccedDefsAgeAssuranceEvent
176
177
  ) {
177
178
  await handleAgeAssuranceEventOperation(db, req, now)
179
+ } else if (namespace === Namespaces.AppBskyAgeassuranceDefsEvent) {
180
+ await handleAgeAssuranceV2EventOperation(db, req, now)
178
181
  } else if (namespace === Namespaces.AppBskyBookmarkDefsBookmark) {
179
182
  await handleBookmarkOperation(db, req, now)
180
183
  }
@@ -314,6 +317,34 @@ const handleAgeAssuranceEventOperation = async (
314
317
  .execute()
315
318
  }
316
319
 
320
+ const handleAgeAssuranceV2EventOperation = async (
321
+ db: Database,
322
+ req: PutOperationRequest,
323
+ _now: string,
324
+ ) => {
325
+ const { actorDid, method, payload } = req
326
+ if (method !== Method.CREATE) return
327
+
328
+ const parsed = jsonStringToLex(
329
+ Buffer.from(payload).toString('utf8'),
330
+ ) as AgeAssuranceV2Event
331
+ const { status, createdAt, access, countryCode, regionCode } = parsed
332
+
333
+ const update = {
334
+ ageAssuranceStatus: status,
335
+ ageAssuranceLastInitiatedAt: status === 'pending' ? createdAt : undefined,
336
+ ageAssuranceAccess: access,
337
+ ageAssuranceCountryCode: countryCode,
338
+ ageAssuranceRegionCode: regionCode,
339
+ }
340
+
341
+ return db.db
342
+ .updateTable('actor')
343
+ .set(update)
344
+ .where('did', '=', actorDid)
345
+ .execute()
346
+ }
347
+
317
348
  const handleBookmarkOperation = async (
318
349
  db: Database,
319
350
  req: PutOperationRequest,
@@ -0,0 +1,28 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .alterTable('actor')
6
+ .addColumn('ageAssuranceAccess', 'text')
7
+ .execute()
8
+ await db.schema
9
+ .alterTable('actor')
10
+ .addColumn('ageAssuranceCountryCode', 'text')
11
+ .execute()
12
+ await db.schema
13
+ .alterTable('actor')
14
+ .addColumn('ageAssuranceRegionCode', 'text')
15
+ .execute()
16
+ }
17
+
18
+ export async function down(db: Kysely<unknown>): Promise<void> {
19
+ await db.schema.alterTable('actor').dropColumn('ageAssuranceAccess').execute()
20
+ await db.schema
21
+ .alterTable('actor')
22
+ .dropColumn('ageAssuranceCountryCode')
23
+ .execute()
24
+ await db.schema
25
+ .alterTable('actor')
26
+ .dropColumn('ageAssuranceRegionCode')
27
+ .execute()
28
+ }
@@ -55,3 +55,4 @@ export * as _20250611T140649895Z from './20250611T140649895Z-add-activity-subscr
55
55
  export * as _20250627T025331240Z from './20250627T025331240Z-add-actor-age-assurance-columns'
56
56
  export * as _20250812T183735692Z from './20250812T183735692Z-add-bookmarks'
57
57
  export * as _20250813T174955711Z from './20250813T174955711Z-add-post-agg-bookmarks'
58
+ export * as _20251120T004738098Z from './20251120T004738098Z-update-actor-age-assurance-v2'
@@ -9,6 +9,9 @@ export interface Actor {
9
9
  trustedVerifier: Generated<boolean>
10
10
  ageAssuranceStatus: string | null
11
11
  ageAssuranceLastInitiatedAt: string | null
12
+ ageAssuranceAccess: string | null
13
+ ageAssuranceCountryCode: string | null
14
+ ageAssuranceRegionCode: string | null
12
15
  }
13
16
 
14
17
  export const tableName = 'actor'
@@ -134,11 +134,22 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
134
134
  return undefined
135
135
  }
136
136
 
137
+ const status = row?.ageAssuranceStatus ?? 'unknown'
138
+ let access = row?.ageAssuranceAccess
139
+ if (status === 'assured') {
140
+ access = 'full'
141
+ } else if (status === 'blocked') {
142
+ access = 'none'
143
+ } else {
144
+ access = 'unknown'
145
+ }
146
+
137
147
  return {
138
- status: row?.ageAssuranceStatus ?? 'unknown',
139
148
  lastInitiatedAt: row?.ageAssuranceLastInitiatedAt
140
149
  ? Timestamp.fromDate(new Date(row?.ageAssuranceLastInitiatedAt))
141
150
  : undefined,
151
+ status,
152
+ access,
142
153
  }
143
154
  }
144
155
 
@@ -179,7 +179,7 @@ export class ActorHydrator {
179
179
  return acc.set(did, null)
180
180
  }
181
181
 
182
- const profile = actor.profile
182
+ const profile = actor.profile?.record
183
183
  ? parseRecord<ProfileRecord>(actor.profile, includeTakedowns)
184
184
  : undefined
185
185
 
@@ -1327,7 +1327,7 @@ export class Hydrator {
1327
1327
  const uri = new AtUri(uriStr)
1328
1328
  const [did] = await this.actor.getDids([uri.host])
1329
1329
  if (!did) return uriStr
1330
- uri.host = did
1330
+ uri.hostname = did
1331
1331
  return uri.toString()
1332
1332
  }
1333
1333
  }
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ import { ImageUriBuilder } from './image/uri'
30
30
  import { createKwsClient } from './kws'
31
31
  import { createServer } from './lexicon'
32
32
  import { loggerMiddleware } from './logger'
33
+ import { authWithApiKey as rolodexAuth, createRolodexClient } from './rolodex'
33
34
  import { createStashClient } from './stash'
34
35
  import { Views } from './views'
35
36
  import { VideoUriBuilder } from './views/util'
@@ -156,6 +157,17 @@ export class BskyAppView {
156
157
  })
157
158
  : undefined
158
159
 
160
+ const rolodexClient = config.rolodexUrl
161
+ ? createRolodexClient({
162
+ baseUrl: config.rolodexUrl,
163
+ httpVersion: config.rolodexHttpVersion ?? '2',
164
+ nodeOptions: { rejectUnauthorized: !config.rolodexIgnoreBadTls },
165
+ interceptors: config.rolodexApiKey
166
+ ? [rolodexAuth(config.rolodexApiKey)]
167
+ : [],
168
+ })
169
+ : undefined
170
+
159
171
  const kwsClient = config.kws ? createKwsClient(config.kws) : undefined
160
172
 
161
173
  const entrywayJwtPublicKey = config.entrywayJwtPublicKeyHex
@@ -191,6 +203,7 @@ export class BskyAppView {
191
203
  bsyncClient,
192
204
  stashClient,
193
205
  courierClient,
206
+ rolodexClient,
194
207
  authVerifier,
195
208
  featureGates,
196
209
  blobDispatcher,