@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
@@ -0,0 +1,745 @@
1
+ import crypto from 'node:crypto'
2
+ import { once } from 'node:events'
3
+ import { Server, createServer } from 'node:http'
4
+ import { AddressInfo } from 'node:net'
5
+ import express, { Application } from 'express'
6
+ import {
7
+ AppBskyAgeassuranceDefs,
8
+ AtpAgent,
9
+ ageAssuranceRuleIDs as ruleIds,
10
+ } from '@atproto/api'
11
+ import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
12
+ import {
13
+ type KWSWebhookAgeVerified,
14
+ serializeKWSAgeVerifiedStatus,
15
+ } from '../../src/api/age-assurance/kws/age-verified'
16
+ import {
17
+ KWSExternalPayloadVersion,
18
+ serializeKWSExternalPayloadV1,
19
+ serializeKWSExternalPayloadV2,
20
+ } from '../../src/api/age-assurance/kws/external-payload'
21
+ import { KwsWebhookBody } from '../../src/api/kws/types'
22
+ import { ids } from '../../src/lexicon/lexicons'
23
+ import * as AppBskyAgeassuranceBegin from '../../src/lexicon/types/app/bsky/ageassurance/begin'
24
+ import * as AppBskyAgeassuranceGetState from '../../src/lexicon/types/app/bsky/ageassurance/getState'
25
+
26
+ type Database = TestNetwork['bsky']['db']
27
+
28
+ const BSKY_REDIRECT_URL = 'http://bsky'
29
+
30
+ jest.mock('../../dist/api/age-assurance/const.js', () => {
31
+ const AGE_ASSURANCE_CONFIG: AppBskyAgeassuranceDefs.Config = {
32
+ regions: [
33
+ {
34
+ countryCode: 'AA',
35
+ regionCode: undefined,
36
+ rules: [
37
+ {
38
+ $type: ruleIds.IfAssuredOverAge,
39
+ age: 18,
40
+ access: 'full',
41
+ },
42
+ {
43
+ $type: ruleIds.Default,
44
+ access: 'safe',
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ countryCode: 'BB',
50
+ regionCode: undefined,
51
+ rules: [
52
+ {
53
+ $type: ruleIds.IfAssuredOverAge,
54
+ age: 18,
55
+ access: 'full',
56
+ },
57
+ {
58
+ $type: ruleIds.Default,
59
+ access: 'safe',
60
+ },
61
+ ],
62
+ },
63
+ ],
64
+ }
65
+ return {
66
+ AGE_ASSURANCE_CONFIG,
67
+ }
68
+ })
69
+
70
+ jest.mock('../../dist/api/age-assurance/kws/const.js', () => {
71
+ const actual = jest.requireActual('../../dist/api/age-assurance/kws/const.js')
72
+ const KWS_V2_COUNTRIES = new Set(['AA'])
73
+ return {
74
+ ...actual,
75
+ KWS_V2_COUNTRIES,
76
+ }
77
+ })
78
+
79
+ describe('age assurance v2 views', () => {
80
+ let network: TestNetwork
81
+ let db: Database
82
+ let agent: AtpAgent
83
+ let sc: SeedClient
84
+ let kws: MockKwsServer
85
+
86
+ const kwsOauthMock = jest.fn()
87
+ const kwsSendAgeVerifiedFlowEmailMock = jest.fn()
88
+ const kwsSendAdultVerifiedFlowEmailMock = jest.fn()
89
+ const actor = {
90
+ did: '',
91
+ email: '',
92
+ }
93
+
94
+ beforeAll(async () => {
95
+ kws = new MockKwsServer({
96
+ oauthMock: kwsOauthMock,
97
+ sendAgeVerifiedFlowEmailMock: kwsSendAgeVerifiedFlowEmailMock,
98
+ sendAdultVerifiedFlowEmailMock: kwsSendAdultVerifiedFlowEmailMock,
99
+ })
100
+ await kws.listen()
101
+
102
+ network = await TestNetwork.create({
103
+ dbPostgresSchema: 'bsky_views_age_assurance_v_two',
104
+ bsky: {
105
+ statsigEnv: 'test',
106
+ statsigKey: 'secret-key',
107
+ kws: {
108
+ apiKey: 'apiKey',
109
+ apiOrigin: kws.url,
110
+ authOrigin: kws.url,
111
+ clientId: 'clientId',
112
+ redirectUrl: BSKY_REDIRECT_URL,
113
+ userAgent: 'userAgent',
114
+ verificationSecret: kws.verificationSecret,
115
+ webhookSecret: kws.webhookSecret,
116
+ ageVerifiedWebhookSecret: kws.ageVerifiedWebhookSecret,
117
+ ageVerifiedRedirectSecret: kws.ageVerifiedRedirectSecret,
118
+ },
119
+ },
120
+ })
121
+
122
+ kws.setBskyBaseUrl(network.bsky.url)
123
+
124
+ db = network.bsky.db
125
+ agent = network.bsky.getClient()
126
+ sc = network.getSeedClient()
127
+
128
+ await basicSeed(sc)
129
+ await network.processAll()
130
+
131
+ actor.did = sc.dids.alice
132
+ actor.email = sc.accounts[actor.did].email
133
+ })
134
+
135
+ beforeEach(async () => {
136
+ // Default mocks for KWS endpoints.
137
+ kwsOauthMock.mockImplementation(
138
+ (_req: express.Request, res: express.Response) =>
139
+ res.json({
140
+ access_token:
141
+ 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.INVALID',
142
+ expires_in: 3600,
143
+ }),
144
+ )
145
+ kwsSendAgeVerifiedFlowEmailMock.mockImplementation(
146
+ (_req: express.Request, res: express.Response) => {
147
+ res.json({})
148
+ },
149
+ )
150
+ kwsSendAdultVerifiedFlowEmailMock.mockImplementation(
151
+ (_req: express.Request, res: express.Response) => {
152
+ res.json({})
153
+ },
154
+ )
155
+ })
156
+
157
+ afterEach(async () => {
158
+ jest.resetAllMocks()
159
+ await clearPrivateData(db)
160
+ await clearActorAgeAssurance(db)
161
+ })
162
+
163
+ afterAll(async () => {
164
+ await network.close()
165
+ await kws.stop()
166
+ })
167
+
168
+ const getState = async (params: AppBskyAgeassuranceGetState.QueryParams) => {
169
+ const { data } = await agent.app.bsky.ageassurance.getState(params, {
170
+ headers: await network.serviceHeaders(
171
+ actor.did,
172
+ ids.AppBskyAgeassuranceGetState,
173
+ ),
174
+ })
175
+ return data
176
+ }
177
+
178
+ const beginAgeAssurance = async (
179
+ params: Omit<AppBskyAgeassuranceBegin.InputSchema, 'email' | 'language'> & {
180
+ email?: string
181
+ },
182
+ ) => {
183
+ const { data } = await agent.app.bsky.ageassurance.begin(
184
+ {
185
+ ...params,
186
+ email: params.email || sc.accounts[actor.did].email,
187
+ language: 'en',
188
+ },
189
+ {
190
+ headers: await network.serviceHeaders(
191
+ actor.did,
192
+ ids.AppBskyAgeassuranceBegin,
193
+ ),
194
+ },
195
+ )
196
+ return data
197
+ }
198
+
199
+ describe('app.bsky.ageassurance.getState', () => {
200
+ it('initially returns defaults', async () => {
201
+ const { state, metadata } = await getState({
202
+ countryCode: 'US',
203
+ regionCode: undefined,
204
+ })
205
+ expect(metadata.accountCreatedAt).toBeDefined()
206
+ expect(state).toEqual({
207
+ lastInitatedAt: undefined,
208
+ status: 'unknown',
209
+ access: 'unknown',
210
+ })
211
+ })
212
+ })
213
+
214
+ describe('app.bsky.ageassurance.begin', () => {
215
+ it('fails if region not supported', async () => {
216
+ const call = beginAgeAssurance({
217
+ countryCode: 'XX',
218
+ })
219
+ await expect(call).rejects.toHaveProperty('error', 'RegionNotSupported')
220
+ })
221
+
222
+ it('fails if email is invalid', async () => {
223
+ const call = beginAgeAssurance({
224
+ email: 'invalid-email',
225
+ countryCode: 'XX',
226
+ })
227
+ await expect(call).rejects.toHaveProperty('error', 'InvalidEmail')
228
+ })
229
+
230
+ it('succeeds for V2 country', async () => {
231
+ const res = await beginAgeAssurance({
232
+ countryCode: 'AA',
233
+ })
234
+ await network.processAll()
235
+ const { state } = await getState({
236
+ countryCode: 'AA',
237
+ })
238
+ expect(kwsSendAgeVerifiedFlowEmailMock).toHaveBeenCalledTimes(1)
239
+ expect(res).toEqual(state)
240
+ expect(state.lastInitiatedAt).toBeDefined()
241
+ expect(state.status).toEqual('pending')
242
+ expect(state.access).toEqual('unknown')
243
+ })
244
+
245
+ it('succeeds for V1 country', async () => {
246
+ const res = await beginAgeAssurance({
247
+ countryCode: 'BB',
248
+ })
249
+ await network.processAll()
250
+ const { state } = await getState({
251
+ countryCode: 'BB',
252
+ })
253
+ expect(kwsSendAdultVerifiedFlowEmailMock).toHaveBeenCalledTimes(1)
254
+ expect(res).toEqual(state)
255
+ expect(state.lastInitiatedAt).toBeDefined()
256
+ expect(state.status).toEqual('pending')
257
+ expect(state.access).toEqual('unknown')
258
+ })
259
+ })
260
+
261
+ describe('external handlers', () => {
262
+ describe('V2 redirects', () => {
263
+ it('redirects with result=unknown if we fail to parse the status object', async () => {
264
+ const res = await kws.redirectV2({
265
+ externalPayload: serializeKWSExternalPayloadV2({
266
+ version: KWSExternalPayloadVersion.V2,
267
+ actorDid: actor.did,
268
+ attemptId: crypto.randomUUID(),
269
+ countryCode: 'AA',
270
+ }),
271
+ status: JSON.stringify({
272
+ verified: true,
273
+ verifiedMinimumAge: '18', // will fail parsing
274
+ }),
275
+ })
276
+ expect(res.status).toBe(302)
277
+ expect(res.headers.get('Location')).toBe(
278
+ `${BSKY_REDIRECT_URL}?result=unknown`,
279
+ )
280
+ })
281
+
282
+ it('redirects with result=unknown if status is not verified', async () => {
283
+ const res = await kws.redirectV2({
284
+ externalPayload: serializeKWSExternalPayloadV2({
285
+ version: KWSExternalPayloadVersion.V2,
286
+ actorDid: actor.did,
287
+ attemptId: crypto.randomUUID(),
288
+ countryCode: 'AA',
289
+ }),
290
+ status: serializeKWSAgeVerifiedStatus({
291
+ verified: false,
292
+ verifiedMinimumAge: 18,
293
+ }),
294
+ })
295
+ expect(res.status).toBe(302)
296
+ expect(res.headers.get('Location')).toBe(
297
+ `${BSKY_REDIRECT_URL}?actorDid=${encodeURIComponent(actor.did)}&result=unknown`,
298
+ )
299
+ })
300
+
301
+ // this also covers any other thrown errors
302
+ it('redirects with result=unknown if access check throws', async () => {
303
+ const res = await kws.redirectV2({
304
+ externalPayload: serializeKWSExternalPayloadV2({
305
+ version: KWSExternalPayloadVersion.V2,
306
+ actorDid: actor.did,
307
+ attemptId: crypto.randomUUID(),
308
+ countryCode: 'XX', // should never reach KWS anyway
309
+ }),
310
+ status: serializeKWSAgeVerifiedStatus({
311
+ verified: true,
312
+ verifiedMinimumAge: 18,
313
+ }),
314
+ })
315
+ expect(res.status).toBe(302)
316
+ expect(res.headers.get('Location')).toBe(
317
+ `${BSKY_REDIRECT_URL}?actorDid=${encodeURIComponent(actor.did)}&result=unknown`,
318
+ )
319
+ })
320
+
321
+ it('success', async () => {
322
+ await beginAgeAssurance({
323
+ countryCode: 'AA',
324
+ })
325
+ await network.processAll()
326
+ await kws.redirectV2({
327
+ externalPayload: serializeKWSExternalPayloadV2({
328
+ version: KWSExternalPayloadVersion.V2,
329
+ actorDid: actor.did,
330
+ attemptId: crypto.randomUUID(),
331
+ countryCode: 'AA',
332
+ }),
333
+ status: serializeKWSAgeVerifiedStatus({
334
+ verified: true,
335
+ verifiedMinimumAge: 18,
336
+ }),
337
+ })
338
+ await network.processAll()
339
+ const { state } = await getState({
340
+ countryCode: 'AA',
341
+ })
342
+ expect(state.lastInitiatedAt).toBeDefined()
343
+ expect(state.status).toEqual('assured')
344
+ expect(state.access).toEqual('full')
345
+ })
346
+ })
347
+
348
+ describe('V2 webhooks', () => {
349
+ it('returns 400 if we fail to parse the external payload', async () => {
350
+ const res = await kws.webhookV2({
351
+ name: 'age-verified',
352
+ time: new Date().toISOString(),
353
+ orgId: crypto.randomUUID(),
354
+ productId: crypto.randomUUID(),
355
+ payload: {
356
+ email: actor.email,
357
+ externalPayload: serializeKWSExternalPayloadV2({
358
+ version: KWSExternalPayloadVersion.V2,
359
+ actorDid: actor.did,
360
+ attemptId: crypto.randomUUID(),
361
+ countryCode: 'AA',
362
+ }),
363
+ status: {
364
+ verified: true,
365
+ // @ts-ignore testing invalid payload
366
+ verifiedMinimumAge: '18',
367
+ },
368
+ },
369
+ })
370
+ expect(res.status).toBe(400)
371
+ await expect(res.json()).resolves.toHaveProperty(
372
+ 'error',
373
+ 'Failed to parse KWS webhook body',
374
+ )
375
+ })
376
+
377
+ it('returns 400 if status is not verified', async () => {
378
+ const res = await kws.webhookV2({
379
+ name: 'age-verified',
380
+ time: new Date().toISOString(),
381
+ orgId: crypto.randomUUID(),
382
+ productId: crypto.randomUUID(),
383
+ payload: {
384
+ email: actor.email,
385
+ externalPayload: serializeKWSExternalPayloadV2({
386
+ version: KWSExternalPayloadVersion.V2,
387
+ actorDid: actor.did,
388
+ attemptId: crypto.randomUUID(),
389
+ countryCode: 'AA',
390
+ }),
391
+ status: {
392
+ verified: false,
393
+ verifiedMinimumAge: 18,
394
+ },
395
+ },
396
+ })
397
+ expect(res.status).toBe(400)
398
+ await expect(res.json()).resolves.toHaveProperty(
399
+ 'error',
400
+ 'Expected KWS webhook to have verified status',
401
+ )
402
+ })
403
+
404
+ it('returns 200, but AA state unchanged due to invalid region', async () => {
405
+ const res = await kws.webhookV2({
406
+ name: 'age-verified',
407
+ time: new Date().toISOString(),
408
+ orgId: crypto.randomUUID(),
409
+ productId: crypto.randomUUID(),
410
+ payload: {
411
+ email: actor.email,
412
+ externalPayload: serializeKWSExternalPayloadV2({
413
+ version: KWSExternalPayloadVersion.V2,
414
+ actorDid: actor.did,
415
+ attemptId: crypto.randomUUID(),
416
+ countryCode: 'XX',
417
+ }),
418
+ status: {
419
+ verified: true,
420
+ verifiedMinimumAge: 18,
421
+ },
422
+ },
423
+ })
424
+ await network.processAll()
425
+ expect(res.status).toBe(200)
426
+ const { state } = await getState({
427
+ countryCode: 'XX',
428
+ })
429
+ expect(state.status).toEqual('unknown') // we never began, so it's still unknown
430
+ })
431
+
432
+ it('success', async () => {
433
+ await beginAgeAssurance({
434
+ countryCode: 'AA',
435
+ })
436
+ await network.processAll()
437
+ await kws.webhookV2({
438
+ name: 'age-verified',
439
+ time: new Date().toISOString(),
440
+ orgId: crypto.randomUUID(),
441
+ productId: crypto.randomUUID(),
442
+ payload: {
443
+ email: actor.email,
444
+ externalPayload: serializeKWSExternalPayloadV2({
445
+ version: KWSExternalPayloadVersion.V2,
446
+ actorDid: actor.did,
447
+ attemptId: crypto.randomUUID(),
448
+ countryCode: 'AA',
449
+ }),
450
+ status: {
451
+ verified: true,
452
+ verifiedMinimumAge: 18,
453
+ },
454
+ },
455
+ })
456
+ await network.processAll()
457
+ const { state } = await getState({
458
+ countryCode: 'AA',
459
+ })
460
+ expect(state.lastInitiatedAt).toBeDefined()
461
+ expect(state.status).toEqual('assured')
462
+ expect(state.access).toEqual('full')
463
+ })
464
+ })
465
+
466
+ describe('V1 compat', () => {
467
+ it('works via webhook', async () => {
468
+ await beginAgeAssurance({
469
+ countryCode: 'BB',
470
+ })
471
+ await network.processAll()
472
+ await kws.webhookV1({
473
+ payload: {
474
+ externalPayload: serializeKWSExternalPayloadV2({
475
+ version: KWSExternalPayloadVersion.V2,
476
+ actorDid: actor.did,
477
+ attemptId: crypto.randomUUID(),
478
+ countryCode: 'BB',
479
+ }),
480
+ status: {
481
+ verified: true,
482
+ },
483
+ },
484
+ })
485
+ await network.processAll()
486
+ const { state } = await getState({
487
+ countryCode: 'BB',
488
+ })
489
+ expect(state.lastInitiatedAt).toBeDefined()
490
+ expect(state.status).toEqual('assured')
491
+ expect(state.access).toEqual('full')
492
+ })
493
+
494
+ it('works via redirect', async () => {
495
+ await beginAgeAssurance({
496
+ countryCode: 'BB',
497
+ })
498
+ await network.processAll()
499
+ await kws.redirectV1({
500
+ externalPayload: serializeKWSExternalPayloadV2({
501
+ version: KWSExternalPayloadVersion.V2,
502
+ actorDid: actor.did,
503
+ attemptId: crypto.randomUUID(),
504
+ countryCode: 'BB',
505
+ }),
506
+ status: JSON.stringify({
507
+ verified: true,
508
+ }),
509
+ })
510
+ await network.processAll()
511
+ const { state } = await getState({
512
+ countryCode: 'BB',
513
+ })
514
+ expect(state.lastInitiatedAt).toBeDefined()
515
+ expect(state.status).toEqual('assured')
516
+ expect(state.access).toEqual('full')
517
+ })
518
+ })
519
+ })
520
+
521
+ describe('misc', () => {
522
+ it('cannot re-init from terminal state', async () => {
523
+ await kws.redirectV2({
524
+ externalPayload: serializeKWSExternalPayloadV2({
525
+ version: KWSExternalPayloadVersion.V2,
526
+ actorDid: actor.did,
527
+ attemptId: crypto.randomUUID(),
528
+ countryCode: 'AA',
529
+ }),
530
+ status: serializeKWSAgeVerifiedStatus({
531
+ verified: true,
532
+ verifiedMinimumAge: 18,
533
+ }),
534
+ })
535
+ await network.processAll()
536
+ const call = beginAgeAssurance({
537
+ countryCode: 'AA',
538
+ })
539
+ await expect(call).rejects.toHaveProperty('error', 'InvalidInitiation')
540
+ })
541
+
542
+ /*
543
+ * This tests local dataplane behavior, but the actual prod implementation
544
+ * lives in the dataplane repo, obviously.
545
+ */
546
+ it('dataplane converts v1 to v2 state at read time', async () => {
547
+ await beginAgeAssurance({
548
+ countryCode: 'BB',
549
+ })
550
+ await network.processAll()
551
+ await kws.webhookV1({
552
+ payload: {
553
+ externalPayload: serializeKWSExternalPayloadV1({
554
+ actorDid: actor.did,
555
+ attemptId: crypto.randomUUID(),
556
+ }),
557
+ status: {
558
+ verified: true,
559
+ },
560
+ },
561
+ })
562
+ await network.processAll()
563
+ const { state } = await getState({
564
+ countryCode: 'BB',
565
+ })
566
+ expect(state.lastInitiatedAt).toBeDefined()
567
+ expect(state.status).toEqual('assured')
568
+ expect(state.access).toEqual('full')
569
+ })
570
+ })
571
+ })
572
+
573
+ const clearPrivateData = async (db: Database) => {
574
+ await db.db.deleteFrom('private_data').execute()
575
+ }
576
+
577
+ const clearActorAgeAssurance = async (db: Database) => {
578
+ await db.db
579
+ .updateTable('actor')
580
+ .set({
581
+ ageAssuranceStatus: null,
582
+ ageAssuranceLastInitiatedAt: null,
583
+ ageAssuranceAccess: null,
584
+ ageAssuranceCountryCode: null,
585
+ ageAssuranceRegionCode: null,
586
+ })
587
+ .execute()
588
+ }
589
+
590
+ class MockKwsServer {
591
+ verificationSecret = 'verificationSecret' // unused here
592
+ webhookSecret = 'webhookSecret' // unused here
593
+ ageVerifiedWebhookSecret = 'ageVerifiedWebhookSecret'
594
+ ageVerifiedRedirectSecret = 'ageVerifiedRedirectSecret'
595
+
596
+ private app: Application
597
+ private server: Server
598
+ private bskyUrlBase = ''
599
+
600
+ constructor({
601
+ oauthMock,
602
+ sendAgeVerifiedFlowEmailMock,
603
+ sendAdultVerifiedFlowEmailMock,
604
+ }: {
605
+ oauthMock: jest.Mock
606
+ sendAgeVerifiedFlowEmailMock: jest.Mock
607
+ sendAdultVerifiedFlowEmailMock: jest.Mock
608
+ }) {
609
+ this.app = express()
610
+ .use(express.json())
611
+ .post('/auth/realms/kws/protocol/openid-connect/token', (_, res) =>
612
+ oauthMock(_, res),
613
+ )
614
+ .post('/v1/verifications/send-email', (req, res) => {
615
+ const body = req.body
616
+ if (body.userContext === 'age') {
617
+ return sendAgeVerifiedFlowEmailMock(req, res)
618
+ } else if (body.userContext === 'adult') {
619
+ return sendAdultVerifiedFlowEmailMock(req, res)
620
+ }
621
+ })
622
+
623
+ this.server = createServer(this.app)
624
+ }
625
+
626
+ async listen(port?: number) {
627
+ this.server.listen(port)
628
+ await once(this.server, 'listening')
629
+ }
630
+
631
+ async stop() {
632
+ this.server.close()
633
+ await once(this.server, 'close')
634
+ }
635
+
636
+ setBskyBaseUrl(url: string) {
637
+ this.bskyUrlBase = url
638
+ }
639
+
640
+ redirectV1({
641
+ externalPayload,
642
+ status,
643
+ }: {
644
+ externalPayload: string
645
+ status: string
646
+ }) {
647
+ const sig = crypto
648
+ .createHmac('sha256', this.verificationSecret)
649
+ .update(`${status}:${externalPayload}`)
650
+ .digest('hex')
651
+
652
+ const queryString = new URLSearchParams({
653
+ externalPayload,
654
+ signature: sig,
655
+ status,
656
+ }).toString()
657
+
658
+ return fetch(
659
+ `${this.bskyUrlBase}/external/kws/age-assurance-verification?${queryString}`,
660
+ {
661
+ method: 'GET',
662
+ redirect: 'manual',
663
+ },
664
+ )
665
+ }
666
+
667
+ redirectV2({
668
+ externalPayload,
669
+ status,
670
+ }: {
671
+ externalPayload: string
672
+ status: string
673
+ }) {
674
+ const sig = crypto
675
+ .createHmac('sha256', this.ageVerifiedRedirectSecret)
676
+ .update(`${status}:${externalPayload}`)
677
+ .digest('hex')
678
+
679
+ const queryString = new URLSearchParams({
680
+ externalPayload,
681
+ signature: sig,
682
+ status,
683
+ }).toString()
684
+
685
+ return fetch(
686
+ `${this.bskyUrlBase}/external/age-assurance/redirects/kws-age-verified?${queryString}`,
687
+ {
688
+ method: 'GET',
689
+ redirect: 'manual',
690
+ },
691
+ )
692
+ }
693
+
694
+ webhookV1(
695
+ body: Omit<KwsWebhookBody, 'payload'> & {
696
+ payload: Omit<KwsWebhookBody['payload'], 'externalPayload'> & {
697
+ externalPayload: string
698
+ }
699
+ },
700
+ ): Promise<Response> {
701
+ const bodyBuffer = Buffer.from(JSON.stringify(body))
702
+
703
+ const timestamp = new Date().valueOf()
704
+ const sig = crypto
705
+ .createHmac('sha256', this.webhookSecret)
706
+ .update(`${timestamp}.${bodyBuffer}`)
707
+ .digest('hex')
708
+
709
+ return fetch(`${this.bskyUrlBase}/external/kws/age-assurance-webhook`, {
710
+ method: 'POST',
711
+ body: bodyBuffer,
712
+ headers: {
713
+ 'x-kws-signature': `t=${timestamp},v1=${sig}`,
714
+ 'Content-Type': 'application/json',
715
+ },
716
+ })
717
+ }
718
+
719
+ webhookV2(body: KWSWebhookAgeVerified): Promise<Response> {
720
+ const bodyBuffer = Buffer.from(JSON.stringify(body))
721
+
722
+ const timestamp = new Date().valueOf()
723
+ const sig = crypto
724
+ .createHmac('sha256', this.ageVerifiedWebhookSecret)
725
+ .update(`${timestamp}.${bodyBuffer}`)
726
+ .digest('hex')
727
+
728
+ return fetch(
729
+ `${this.bskyUrlBase}/external/age-assurance/webhooks/kws-age-verified`,
730
+ {
731
+ method: 'POST',
732
+ body: bodyBuffer,
733
+ headers: {
734
+ 'x-kws-signature': `t=${timestamp},v1=${sig}`,
735
+ 'Content-Type': 'application/json',
736
+ },
737
+ },
738
+ )
739
+ }
740
+
741
+ get url() {
742
+ const address = this.server.address() as AddressInfo
743
+ return `http://localhost:${address.port}`
744
+ }
745
+ }