@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,75 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Schema for KWS the `status` object on `age-verified` payloads.
5
+ */
6
+ export const KWSAgeVerifiedStatusSchema = z.object({
7
+ verified: z.boolean(),
8
+ verifiedMinimumAge: z.number(),
9
+ transactionId: z.string().optional(),
10
+ })
11
+
12
+ /**
13
+ * The KWS `status` object on `age-verified` payloads.
14
+ */
15
+ export type KWSAgeVerifiedStatus = z.infer<typeof KWSAgeVerifiedStatusSchema>
16
+
17
+ export function serializeKWSAgeVerifiedStatus(
18
+ status: KWSAgeVerifiedStatus,
19
+ ): string {
20
+ return JSON.stringify(KWSAgeVerifiedStatusSchema.parse(status))
21
+ }
22
+
23
+ /**
24
+ * Parse KWS `age-verified` status object.
25
+ */
26
+ export const parseKWSAgeVerifiedStatus = (
27
+ raw: string,
28
+ ): KWSAgeVerifiedStatus => {
29
+ try {
30
+ const value = JSON.parse(raw)
31
+ return KWSAgeVerifiedStatusSchema.parse(value)
32
+ } catch (err) {
33
+ throw new Error(`Invalid KWS age-verified status: ${raw}`, {
34
+ cause: err,
35
+ })
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Schema for KWS `age-verified` webhooks.
41
+ *
42
+ * Note: we don't use `.strict()` here so that we avoid breaking if KWS adds
43
+ * fields, and some fields below are not strictly typed since we're not using
44
+ * them.
45
+ */
46
+ export const KWSAgeVerifiedWebhookSchema = z.object({
47
+ name: z.string(),
48
+ time: z.string(), // ISO8601 timestamp, but don't validate here
49
+ orgId: z.string().uuid().optional(),
50
+ productId: z.string().uuid().optional(),
51
+ payload: z.object({
52
+ email: z.string(), // no need to validate here
53
+ externalPayload: z.string(),
54
+ status: KWSAgeVerifiedStatusSchema,
55
+ }),
56
+ })
57
+
58
+ /**
59
+ * The raw KWS `age-verified` webhook body
60
+ */
61
+ export type KWSWebhookAgeVerified = z.infer<typeof KWSAgeVerifiedWebhookSchema>
62
+
63
+ /**
64
+ * Parse KWS `age-verified` webhook body and its external payload.
65
+ */
66
+ export const parseKWSAgeVerifiedWebhook = (
67
+ raw: string,
68
+ ): KWSWebhookAgeVerified => {
69
+ try {
70
+ const value: unknown = JSON.parse(raw)
71
+ return KWSAgeVerifiedWebhookSchema.parse(value)
72
+ } catch (err) {
73
+ throw new Error(`Invalid webhook body: ${raw}`, { cause: err })
74
+ }
75
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Supported languages for KWS Adult Verification. This list comes from KWS's
3
+ * Age Verification Developer Guide PDF doc.
4
+ */
5
+ export const KWS_SUPPORTED_LANGUAGES = new Set([
6
+ 'en',
7
+ 'ar',
8
+ 'zh-Hans',
9
+ 'nl',
10
+ 'tl',
11
+ 'fr',
12
+ 'de',
13
+ 'id',
14
+ 'it',
15
+ 'ja',
16
+ 'ko',
17
+ 'pl',
18
+ 'pt-BR',
19
+ 'pt',
20
+ 'ru',
21
+ 'es',
22
+ 'th',
23
+ 'tr',
24
+ 'vi',
25
+ ])
26
+
27
+ /**
28
+ * Regions where our "version 2" using the `age-verified` KWS flow is
29
+ * available. In these regions, we'll use a different KWS flow from the
30
+ * existing `adult-verified` flow, pass along a different external payload, and
31
+ * handle webhooks/redirects differently in the appview.
32
+ */
33
+ export const KWS_V2_COUNTRIES = new Set(['AU'])
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+ import {
3
+ KWSExternalPayloadVersion,
4
+ parseKWSExternalPayloadV1WithV2Compat,
5
+ parseKWSExternalPayloadV2,
6
+ parseKWSExternalPayloadVersion,
7
+ serializeKWSExternalPayloadV1,
8
+ serializeKWSExternalPayloadV2,
9
+ } from './external-payload'
10
+
11
+ describe('parseKWSExternalPayloadVersion', () => {
12
+ it('should return V2 for "2"', () => {
13
+ const result = parseKWSExternalPayloadVersion('2')
14
+ expect(result).toBe('2')
15
+ })
16
+ it('should return V1 for unknown versions', () => {
17
+ const result = parseKWSExternalPayloadVersion('unknown')
18
+ expect(result).toBe('1')
19
+ })
20
+ })
21
+
22
+ describe('parseKWSExternalPayloadV1WithV2Compat', () => {
23
+ it('should parse V1 payload correctly', () => {
24
+ const payload = {
25
+ attemptId: '123',
26
+ actorDid: 'did:plc:123',
27
+ }
28
+ const serialized = serializeKWSExternalPayloadV1(payload)
29
+ const result = parseKWSExternalPayloadV1WithV2Compat(serialized)
30
+ expect(result).toEqual({
31
+ version: KWSExternalPayloadVersion.V1,
32
+ ...payload,
33
+ })
34
+ })
35
+ it('should parse V2 payload correctly', () => {
36
+ const payload = {
37
+ version: KWSExternalPayloadVersion.V2 as const,
38
+ attemptId: '123',
39
+ actorDid: 'did:plc:123',
40
+ countryCode: 'US',
41
+ }
42
+ const serialized = serializeKWSExternalPayloadV2(payload)
43
+ const result = parseKWSExternalPayloadV1WithV2Compat(serialized)
44
+ expect(result).toEqual(payload)
45
+ })
46
+ })
47
+
48
+ describe('serializeKWSExternalPayloadV2 & parseKWSExternalPayloadV2', () => {
49
+ const payload = {
50
+ version: KWSExternalPayloadVersion.V2 as const,
51
+ attemptId: '123',
52
+ actorDid: 'did:plc:123',
53
+ countryCode: 'US',
54
+ regionCode: 'CA',
55
+ }
56
+ it('compresses when serializing', () => {
57
+ const serialized = serializeKWSExternalPayloadV2(payload)
58
+ const comparison = JSON.stringify({
59
+ v: KWSExternalPayloadVersion.V2,
60
+ id: payload.attemptId,
61
+ did: payload.actorDid,
62
+ gc: payload.countryCode,
63
+ gr: payload.regionCode,
64
+ })
65
+ expect(serialized).toEqual(comparison)
66
+ })
67
+ it('decompresses when parsing', () => {
68
+ const serialized = serializeKWSExternalPayloadV2(payload)
69
+ const deserialized = parseKWSExternalPayloadV2(serialized)
70
+ expect(deserialized).toEqual(payload)
71
+ })
72
+ })
@@ -0,0 +1,149 @@
1
+ import { z } from 'zod'
2
+
3
+ export const KWS_EXTERNAL_PAYLOAD_CHAR_LIMIT = 250
4
+
5
+ /**
6
+ * Thrown when the provided external payload exceeds KWS's character limit.
7
+ *
8
+ * This is most commonly caused by DIDs that are too long, such as for
9
+ * `did:web` DIDs. But it's very rare, and the client has special handling for
10
+ * this case.
11
+ */
12
+ export class KWSExternalPayloadTooLargeError extends Error {}
13
+
14
+ export enum KWSExternalPayloadVersion {
15
+ V1 = '1',
16
+ V2 = '2',
17
+ }
18
+
19
+ export function parseKWSExternalPayloadVersion(raw: string) {
20
+ switch (raw) {
21
+ case KWSExternalPayloadVersion.V2:
22
+ return KWSExternalPayloadVersion.V2
23
+ default:
24
+ return KWSExternalPayloadVersion.V1
25
+ }
26
+ }
27
+
28
+ export type KWSExternalPayloadV1 = {
29
+ actorDid: string
30
+ attemptId: string
31
+ }
32
+
33
+ export const KWSExternalPayloadV1Schema = z.object({
34
+ actorDid: z.string(),
35
+ attemptId: z.string(),
36
+ })
37
+
38
+ export function parseKWSExternalPayloadV1(raw: string): KWSExternalPayloadV1 {
39
+ try {
40
+ const value: unknown = JSON.parse(raw)
41
+ return KWSExternalPayloadV1Schema.parse(value)
42
+ } catch (err) {
43
+ throw new Error(`Failed to parse KWSExternalPayloadV1`, {
44
+ cause: err,
45
+ })
46
+ }
47
+ }
48
+
49
+ export function serializeKWSExternalPayloadV1(
50
+ payload: KWSExternalPayloadV1,
51
+ ): string {
52
+ try {
53
+ return JSON.stringify(KWSExternalPayloadV1Schema.parse(payload))
54
+ } catch (err) {
55
+ throw new Error('Failed to serialize KWSExternalPayloadV1', { cause: err })
56
+ }
57
+ }
58
+
59
+ /**
60
+ * During our migration from v1 to v2 of the KWS external payload, we'll be
61
+ * sending v2 payloads on the v1 flow (the `adult-verified` email flow). We use
62
+ * this utility to parse either v1 or v2 payloads in that flow.
63
+ *
64
+ * Check for the `version` field on the output of this method to discriminate
65
+ * between the two types and handle them differently.
66
+ */
67
+ export function parseKWSExternalPayloadV1WithV2Compat(
68
+ raw: string,
69
+ ):
70
+ | (KWSExternalPayloadV1 & { version: KWSExternalPayloadVersion.V1 })
71
+ | KWSExternalPayloadV2 {
72
+ const deserialized = JSON.parse(raw)
73
+ const v2 = deserialized.v === KWSExternalPayloadVersion.V2
74
+
75
+ if (v2) {
76
+ return parseKWSExternalPayloadV2(raw)
77
+ } else {
78
+ return {
79
+ ...parseKWSExternalPayloadV1(raw),
80
+ version: KWSExternalPayloadVersion.V1,
81
+ }
82
+ }
83
+ }
84
+
85
+ /***************************
86
+ * KWS External Payload V2 *
87
+ ***************************/
88
+
89
+ export type KWSExternalPayloadV2 = {
90
+ version: KWSExternalPayloadVersion.V2
91
+ attemptId: string
92
+ actorDid: string
93
+ countryCode: string
94
+ regionCode?: string
95
+ }
96
+
97
+ export const KWSExternalPayloadV2Schema = z.object({
98
+ v: z.string(),
99
+ id: z.string(),
100
+ did: z.string(),
101
+ gc: z.string().length(2),
102
+ gr: z.string().optional(),
103
+ })
104
+
105
+ export function serializeKWSExternalPayloadV2(
106
+ payload: KWSExternalPayloadV2,
107
+ ): string {
108
+ let compressed: z.infer<typeof KWSExternalPayloadV2Schema>
109
+ try {
110
+ compressed = KWSExternalPayloadV2Schema.parse({
111
+ v: KWSExternalPayloadVersion.V2, // version
112
+ id: payload.attemptId,
113
+ did: payload.actorDid,
114
+ gc: payload.countryCode, // geolocation country
115
+ gr: payload.regionCode, // geolocation region
116
+ })
117
+ } catch (err) {
118
+ throw new Error('Failed to serialize KWSExternalPayloadV2', { cause: err })
119
+ }
120
+
121
+ const serialized = JSON.stringify(compressed)
122
+
123
+ if (serialized.length > KWS_EXTERNAL_PAYLOAD_CHAR_LIMIT) {
124
+ throw new KWSExternalPayloadTooLargeError(
125
+ `Serialized external payload size ${serialized.length} exceeds limit of ${KWS_EXTERNAL_PAYLOAD_CHAR_LIMIT}`,
126
+ )
127
+ }
128
+
129
+ return serialized
130
+ }
131
+
132
+ export function parseKWSExternalPayloadV2(raw: string): KWSExternalPayloadV2 {
133
+ try {
134
+ const deserialized = JSON.parse(raw)
135
+ const parsed = KWSExternalPayloadV2Schema.parse(deserialized)
136
+
137
+ return {
138
+ version: KWSExternalPayloadVersion.V2,
139
+ attemptId: parsed.id,
140
+ actorDid: parsed.did,
141
+ countryCode: parsed.gc,
142
+ regionCode: parsed.gr,
143
+ }
144
+ } catch (err) {
145
+ throw new Error(`Failed to parse KWSExternalPayloadV2`, {
146
+ cause: err,
147
+ })
148
+ }
149
+ }
@@ -0,0 +1,107 @@
1
+ import express, { RequestHandler } from 'express'
2
+ import { ageAssuranceLogger as logger } from '../../../logger'
3
+ import { getClientUa, validateSignature } from '../../kws/util'
4
+ import { AGE_ASSURANCE_CONFIG } from '../const'
5
+ import { parseKWSAgeVerifiedStatus } from '../kws/age-verified'
6
+ import {
7
+ type KWSExternalPayloadV2,
8
+ parseKWSExternalPayloadV2,
9
+ } from '../kws/external-payload'
10
+ import { createEvent } from '../stash'
11
+ import { AppContextWithAA } from '../types'
12
+ import { computeAgeAssuranceAccessOrThrow } from '../util'
13
+
14
+ function parseQueryParams(
15
+ ctx: AppContextWithAA,
16
+ req: express.Request,
17
+ ): {
18
+ status: string
19
+ externalPayload: string
20
+ } {
21
+ try {
22
+ const status = String(req.query.status)
23
+ const externalPayload = String(req.query.externalPayload)
24
+ const signature = String(req.query.signature)
25
+
26
+ validateSignature(
27
+ ctx.cfg.kws.ageVerifiedRedirectSecret,
28
+ `${status}:${externalPayload}`,
29
+ signature,
30
+ )
31
+
32
+ return {
33
+ status,
34
+ externalPayload,
35
+ }
36
+ } catch (err) {
37
+ throw new Error('Invalid KWS API request', { cause: err })
38
+ }
39
+ }
40
+
41
+ export const handler =
42
+ (ctx: AppContextWithAA): RequestHandler =>
43
+ async (req: express.Request, res: express.Response) => {
44
+ let externalPayload: KWSExternalPayloadV2 | undefined
45
+
46
+ try {
47
+ const query = parseQueryParams(ctx, req)
48
+ const { verified, verifiedMinimumAge } = parseKWSAgeVerifiedStatus(
49
+ query.status,
50
+ )
51
+ externalPayload = parseKWSExternalPayloadV2(query.externalPayload)
52
+ const { actorDid, attemptId, countryCode, regionCode } = externalPayload
53
+
54
+ /*
55
+ * KWS does not send unverified webhooks for age verification, so we
56
+ * expect all webhooks to be verified. This is just a sanity check.
57
+ */
58
+ if (!verified) {
59
+ const message =
60
+ 'Expected KWS verification redirect to have verified status'
61
+ logger.error({}, message)
62
+ throw new Error(message)
63
+ }
64
+
65
+ const { access } = computeAgeAssuranceAccessOrThrow(
66
+ AGE_ASSURANCE_CONFIG,
67
+ {
68
+ countryCode,
69
+ regionCode,
70
+ verifiedMinimumAge,
71
+ },
72
+ )
73
+
74
+ await createEvent(ctx, actorDid, {
75
+ attemptId,
76
+ // Assumes `app.set('trust proxy', ...)` configured with `true` or specific values.
77
+ completeIp: req.ip,
78
+ completeUa: getClientUa(req),
79
+ countryCode,
80
+ regionCode,
81
+ status: 'assured',
82
+ access,
83
+ })
84
+
85
+ const q = new URLSearchParams({ actorDid, result: 'success' })
86
+
87
+ return res
88
+ .status(302)
89
+ .setHeader('Location', `${ctx.cfg.kws.redirectUrl}?${q}`)
90
+ .end()
91
+ } catch (err) {
92
+ logger.error(
93
+ { err, ...externalPayload },
94
+ 'Failed to handle KWS verification redirect',
95
+ )
96
+
97
+ const q = new URLSearchParams({
98
+ ...(externalPayload ? { actorDid: externalPayload.actorDid } : {}),
99
+ result: 'unknown',
100
+ })
101
+
102
+ return res
103
+ .status(302)
104
+ .setHeader('Location', `${ctx.cfg.kws.redirectUrl}?${q}`)
105
+ .end()
106
+ }
107
+ }
@@ -0,0 +1,22 @@
1
+ import { TID } from '@atproto/common'
2
+ import { AppContext } from '../../context'
3
+ import { Event as AgeAssuranceEvent } from '../../lexicon/types/app/bsky/ageassurance/defs'
4
+ import { Namespaces } from '../../stash'
5
+
6
+ export async function createEvent(
7
+ ctx: AppContext,
8
+ actorDid: string,
9
+ event: Omit<AgeAssuranceEvent, 'createdAt'>,
10
+ ) {
11
+ const payload: AgeAssuranceEvent = {
12
+ createdAt: new Date().toISOString(),
13
+ ...event,
14
+ }
15
+ await ctx.stashClient.create({
16
+ actorDid: actorDid,
17
+ namespace: Namespaces.AppBskyAgeassuranceDefsEvent,
18
+ key: TID.nextStr(),
19
+ payload,
20
+ })
21
+ return payload
22
+ }
@@ -0,0 +1,10 @@
1
+ import { KwsConfig, ServerConfig } from '../../config'
2
+ import { AppContext } from '../../context'
3
+ import { KwsClient } from '../../kws'
4
+
5
+ export type AppContextWithAA = AppContext & {
6
+ kwsClient: KwsClient
7
+ cfg: ServerConfig & {
8
+ kws: KwsConfig
9
+ }
10
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ type AppBskyAgeassuranceDefs,
3
+ computeAgeAssuranceRegionAccess,
4
+ getAgeAssuranceRegionConfig,
5
+ } from '@atproto/api'
6
+
7
+ /**
8
+ * Compute age assurance access based on verified minimum age. Thrown errors
9
+ * are internal errors, so handle them accordingly.
10
+ */
11
+ export function computeAgeAssuranceAccessOrThrow(
12
+ config: AppBskyAgeassuranceDefs.Config,
13
+ {
14
+ countryCode,
15
+ regionCode,
16
+ verifiedMinimumAge,
17
+ }: {
18
+ countryCode: string
19
+ regionCode?: string
20
+ verifiedMinimumAge: number
21
+ },
22
+ ) {
23
+ const region = getAgeAssuranceRegionConfig(config, {
24
+ countryCode,
25
+ regionCode,
26
+ })
27
+
28
+ if (region) {
29
+ const result = computeAgeAssuranceRegionAccess(region, {
30
+ assuredAge: verifiedMinimumAge,
31
+ /*
32
+ * We don't care about this here, this is a client-only rule. If we have
33
+ * verified data, we can use that, and the account creation date is
34
+ * irrelevant.
35
+ */
36
+ accountCreatedAt: undefined,
37
+ })
38
+
39
+ if (result) {
40
+ return result
41
+ } else {
42
+ /*
43
+ * If we don't get a result, it's because none of the rules matched,
44
+ * which is a configuration error: there should always be a default
45
+ * rule.
46
+ */
47
+ throw new Error('Cound not compute age assurance region access')
48
+ }
49
+ } else {
50
+ /**
51
+ * If we had geolocation data, but we don't have a region config for this
52
+ * geolocation, then it means a user outside of our configured regions
53
+ * has completed age verification. In this case, we can't determine their
54
+ * access level, so we throw an error.
55
+ *
56
+ * This case is also guarded in `app.bsky.ageassurance.begin`.
57
+ */
58
+ throw new Error('Could not get config for region')
59
+ }
60
+ }
61
+
62
+ export function createLocationString(countryCode: string, regionCode?: string) {
63
+ return regionCode
64
+ ? `${countryCode.toUpperCase()}-${regionCode.toUpperCase()}`
65
+ : countryCode.toUpperCase()
66
+ }
@@ -0,0 +1,75 @@
1
+ import express, { RequestHandler } from 'express'
2
+ import { ageAssuranceLogger as logger } from '../../../logger'
3
+ import { AGE_ASSURANCE_CONFIG } from '../const'
4
+ import {
5
+ type KWSWebhookAgeVerified,
6
+ parseKWSAgeVerifiedWebhook,
7
+ } from '../kws/age-verified'
8
+ import { parseKWSExternalPayloadV2 } from '../kws/external-payload'
9
+ import { createEvent } from '../stash'
10
+ import { type AppContextWithAA } from '../types'
11
+ import { computeAgeAssuranceAccessOrThrow } from '../util'
12
+
13
+ export const handler =
14
+ (ctx: AppContextWithAA): RequestHandler =>
15
+ async (req: express.Request, res: express.Response) => {
16
+ let body: KWSWebhookAgeVerified
17
+ try {
18
+ body = parseKWSAgeVerifiedWebhook(req.body)
19
+ } catch (err) {
20
+ const message = 'Failed to parse KWS webhook body'
21
+ logger.error({ err }, message)
22
+ return res.status(400).json({ error: message })
23
+ }
24
+
25
+ const { status, externalPayload } = body.payload
26
+ const { verified, verifiedMinimumAge } = status
27
+ const { actorDid, countryCode, regionCode, attemptId } =
28
+ parseKWSExternalPayloadV2(externalPayload)
29
+
30
+ /*
31
+ * KWS does not send unverified webhooks for age verification, so we
32
+ * expect all webhooks to be verified. This is just a sanity check.
33
+ */
34
+ if (!verified) {
35
+ const message = 'Expected KWS webhook to have verified status'
36
+ logger.error({}, message)
37
+ return res.status(400).json({ error: message })
38
+ }
39
+
40
+ let result: ReturnType<typeof computeAgeAssuranceAccessOrThrow> | undefined
41
+ try {
42
+ result = computeAgeAssuranceAccessOrThrow(AGE_ASSURANCE_CONFIG, {
43
+ countryCode,
44
+ regionCode,
45
+ verifiedMinimumAge,
46
+ })
47
+ } catch (err) {
48
+ // internal errors
49
+ logger.error(
50
+ { err, attemptId, actorDid, countryCode, regionCode },
51
+ 'Failed to compute age assurance access',
52
+ )
53
+ }
54
+
55
+ try {
56
+ if (result) {
57
+ await createEvent(ctx, actorDid, {
58
+ attemptId,
59
+ countryCode,
60
+ regionCode,
61
+ status: 'assured',
62
+ access: result.access,
63
+ })
64
+ }
65
+
66
+ return res.status(200).end()
67
+ } catch (err) {
68
+ const message = 'Failed to handle KWS webhook'
69
+ logger.error(
70
+ { err, attemptId, actorDid, countryCode, regionCode },
71
+ message,
72
+ )
73
+ return res.status(500).json({ error: message })
74
+ }
75
+ }