@atproto/bsky 0.0.198 → 0.0.199

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 (151) hide show
  1. package/CHANGELOG.md +14 -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/external.d.ts.map +1 -1
  59. package/dist/api/external.js +2 -0
  60. package/dist/api/external.js.map +1 -1
  61. package/dist/api/index.d.ts.map +1 -1
  62. package/dist/api/index.js +8 -2
  63. package/dist/api/index.js.map +1 -1
  64. package/dist/api/kws/api.d.ts.map +1 -1
  65. package/dist/api/kws/api.js +44 -26
  66. package/dist/api/kws/api.js.map +1 -1
  67. package/dist/api/kws/index.d.ts.map +1 -1
  68. package/dist/api/kws/index.js +3 -1
  69. package/dist/api/kws/index.js.map +1 -1
  70. package/dist/api/kws/webhook.d.ts +3 -1
  71. package/dist/api/kws/webhook.d.ts.map +1 -1
  72. package/dist/api/kws/webhook.js +48 -20
  73. package/dist/api/kws/webhook.js.map +1 -1
  74. package/dist/config.d.ts +14 -0
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +10 -2
  77. package/dist/config.js.map +1 -1
  78. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  79. package/dist/data-plane/bsync/index.js +22 -0
  80. package/dist/data-plane/bsync/index.js.map +1 -1
  81. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.d.ts +4 -0
  82. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.d.ts.map +1 -0
  83. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.js +30 -0
  84. package/dist/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.js.map +1 -0
  85. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  86. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  87. package/dist/data-plane/server/db/migrations/index.js +2 -1
  88. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  89. package/dist/data-plane/server/db/pagination.d.ts +3 -3
  90. package/dist/data-plane/server/db/tables/actor.d.ts +3 -0
  91. package/dist/data-plane/server/db/tables/actor.d.ts.map +1 -1
  92. package/dist/data-plane/server/db/tables/actor.js.map +1 -1
  93. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  94. package/dist/data-plane/server/routes/profile.js +13 -1
  95. package/dist/data-plane/server/routes/profile.js.map +1 -1
  96. package/dist/hydration/hydrator.js +1 -1
  97. package/dist/hydration/hydrator.js.map +1 -1
  98. package/dist/kws.d.ts +35 -0
  99. package/dist/kws.d.ts.map +1 -1
  100. package/dist/kws.js +54 -0
  101. package/dist/kws.js.map +1 -1
  102. package/dist/logger.d.ts +1 -0
  103. package/dist/logger.d.ts.map +1 -1
  104. package/dist/logger.js +2 -1
  105. package/dist/logger.js.map +1 -1
  106. package/dist/proto/bsky_pb.d.ts +8 -0
  107. package/dist/proto/bsky_pb.d.ts.map +1 -1
  108. package/dist/proto/bsky_pb.js +20 -0
  109. package/dist/proto/bsky_pb.js.map +1 -1
  110. package/dist/stash.d.ts +1 -0
  111. package/dist/stash.d.ts.map +1 -1
  112. package/dist/stash.js +1 -0
  113. package/dist/stash.js.map +1 -1
  114. package/dist/util/uris.d.ts +2 -2
  115. package/dist/util/uris.d.ts.map +1 -1
  116. package/package.json +10 -9
  117. package/proto/bsky.proto +1 -0
  118. package/src/api/age-assurance/const.ts +142 -0
  119. package/src/api/age-assurance/index.ts +34 -0
  120. package/src/api/age-assurance/kws/age-verified.ts +75 -0
  121. package/src/api/age-assurance/kws/const.ts +33 -0
  122. package/src/api/age-assurance/kws/external-payload.test.ts +72 -0
  123. package/src/api/age-assurance/kws/external-payload.ts +149 -0
  124. package/src/api/age-assurance/redirects/kws-age-verified.ts +107 -0
  125. package/src/api/age-assurance/stash.ts +22 -0
  126. package/src/api/age-assurance/types.ts +10 -0
  127. package/src/api/age-assurance/util.ts +66 -0
  128. package/src/api/age-assurance/webhooks/kws-age-verified.ts +75 -0
  129. package/src/api/app/bsky/ageassurance/begin.ts +167 -0
  130. package/src/api/app/bsky/ageassurance/getConfig.ts +15 -0
  131. package/src/api/app/bsky/ageassurance/getState.ts +53 -0
  132. package/src/api/external.ts +2 -0
  133. package/src/api/index.ts +6 -0
  134. package/src/api/kws/api.ts +55 -34
  135. package/src/api/kws/index.ts +7 -1
  136. package/src/api/kws/webhook.ts +57 -34
  137. package/src/config.ts +26 -2
  138. package/src/data-plane/bsync/index.ts +31 -0
  139. package/src/data-plane/server/db/migrations/20251120T004738098Z-update-actor-age-assurance-v2.ts +28 -0
  140. package/src/data-plane/server/db/migrations/index.ts +1 -0
  141. package/src/data-plane/server/db/tables/actor.ts +3 -0
  142. package/src/data-plane/server/routes/profile.ts +12 -1
  143. package/src/hydration/hydrator.ts +1 -1
  144. package/src/kws.ts +81 -0
  145. package/src/logger.ts +2 -0
  146. package/src/proto/bsky_pb.ts +12 -0
  147. package/src/stash.ts +3 -0
  148. package/tests/views/age-assurance-v2.test.ts +745 -0
  149. package/tests/views/age-assurance.test.ts +2 -0
  150. package/tsconfig.build.tsbuildinfo +1 -1
  151. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -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 {
@@ -251,6 +265,10 @@ export class ServerConfig {
251
265
  const kwsUserAgent = process.env.BSKY_KWS_USER_AGENT
252
266
  const kwsVerificationSecret = process.env.BSKY_KWS_VERIFICATION_SECRET
253
267
  const kwsWebhookSecret = process.env.BSKY_KWS_WEBHOOK_SECRET
268
+ const kwsAgeVerifiedWebhookSecret =
269
+ process.env.BSKY_KWS_AGE_VERIFIED_WEBHOOK_SECRET
270
+ const kwsAgeVerifiedRedirectSecret =
271
+ process.env.BSKY_KWS_AGE_VERIFIED_REDIRECT_SECRET
254
272
  if (
255
273
  kwsApiKey ||
256
274
  kwsApiOrigin ||
@@ -259,7 +277,9 @@ export class ServerConfig {
259
277
  kwsRedirectUrl ||
260
278
  kwsUserAgent ||
261
279
  kwsVerificationSecret ||
262
- kwsWebhookSecret
280
+ kwsWebhookSecret ||
281
+ kwsAgeVerifiedWebhookSecret ||
282
+ kwsAgeVerifiedRedirectSecret
263
283
  ) {
264
284
  assert(
265
285
  kwsApiOrigin &&
@@ -269,7 +289,9 @@ export class ServerConfig {
269
289
  kwsUserAgent &&
270
290
  kwsVerificationSecret &&
271
291
  kwsWebhookSecret &&
272
- kwsApiKey,
292
+ kwsApiKey &&
293
+ kwsAgeVerifiedWebhookSecret &&
294
+ kwsAgeVerifiedRedirectSecret,
273
295
  'all KWS environment variables must be set if any are set',
274
296
  )
275
297
  kws = {
@@ -281,6 +303,8 @@ export class ServerConfig {
281
303
  userAgent: kwsUserAgent,
282
304
  verificationSecret: kwsVerificationSecret,
283
305
  webhookSecret: kwsWebhookSecret,
306
+ ageVerifiedWebhookSecret: kwsAgeVerifiedWebhookSecret,
307
+ ageVerifiedRedirectSecret: kwsAgeVerifiedRedirectSecret,
284
308
  }
285
309
  }
286
310
 
@@ -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
 
@@ -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/kws.ts CHANGED
@@ -15,8 +15,30 @@ const authResponseSchema = z.object({
15
15
  })
16
16
 
17
17
  const EXTERNAL_PAYLOAD_CHAR_LIMIT = 200
18
+ /**
19
+ * Thrown when the provided external payload exceeds KWS's character limit.
20
+ * This is most commonly caused by DIDs that are too long, such as for
21
+ * `did:web` DIDs. But it's very rare, and the client has special handling for
22
+ * this case.
23
+ */
18
24
  export class KwsExternalPayloadError extends Error {}
19
25
 
26
+ export type KWSSendEmailRequestCommon = {
27
+ email: string
28
+ location: string
29
+ language: string
30
+ externalPayload: string
31
+ }
32
+
33
+ export type KWSSendEmailRequest =
34
+ | (KWSSendEmailRequestCommon & {
35
+ userContext: 'adult'
36
+ })
37
+ | (KWSSendEmailRequestCommon & {
38
+ userContext: 'age'
39
+ minimumAge: number
40
+ })
41
+
20
42
  export class KwsClient {
21
43
  constructor(public cfg: KwsConfig) {}
22
44
 
@@ -68,6 +90,9 @@ export class KwsClient {
68
90
  })
69
91
  }
70
92
 
93
+ /**
94
+ * @deprecated Use `sendAdultVerifiedFlowEmail` or `sendAgeVerifiedFlowEmail` instead.
95
+ */
71
96
  async sendEmail({
72
97
  countryCode,
73
98
  email,
@@ -113,4 +138,60 @@ export class KwsClient {
113
138
 
114
139
  return res.json()
115
140
  }
141
+
142
+ /**
143
+ * Sends a KWS verification email with the given properties.
144
+ */
145
+ async email(props: KWSSendEmailRequest) {
146
+ const res = await this.fetchWithAuth(
147
+ `${this.cfg.apiOrigin}/v1/verifications/send-email`,
148
+ {
149
+ method: 'POST',
150
+ headers: {
151
+ 'Content-Type': 'application/json',
152
+ 'User-Agent': this.cfg.userAgent,
153
+ },
154
+ body: JSON.stringify(props),
155
+ },
156
+ )
157
+
158
+ if (!res.ok) {
159
+ const errorText = await res.text()
160
+ log.error(
161
+ {
162
+ status: res.status,
163
+ statusText: res.statusText,
164
+ errorText,
165
+ flow: props.userContext,
166
+ },
167
+ 'Failed to send KWS email',
168
+ )
169
+ throw new Error('Failed to send KWS email')
170
+ }
171
+
172
+ return res.json()
173
+ }
174
+
175
+ /**
176
+ * Sends an email to the user initiating an `adult` verification flow, which
177
+ * results in `adult-verified` events/webhooks.
178
+ */
179
+ async sendAdultVerifiedFlowEmail(props: KWSSendEmailRequestCommon) {
180
+ return this.email({
181
+ ...props,
182
+ userContext: 'adult',
183
+ })
184
+ }
185
+
186
+ /**
187
+ * Sends an email to the user initiating an `age` verification flow, which
188
+ * results in `age-verified` events/webhooks.
189
+ */
190
+ async sendAgeVerifiedFlowEmail(props: KWSSendEmailRequestCommon) {
191
+ return this.email({
192
+ ...props,
193
+ userContext: 'age',
194
+ minimumAge: 16, // KWS required value
195
+ })
196
+ }
116
197
  }
package/src/logger.ts CHANGED
@@ -17,6 +17,8 @@ export const featureGatesLogger: ReturnType<typeof subsystemLogger> =
17
17
  subsystemLogger('bsky:featuregates')
18
18
  export const dataplaneLogger: ReturnType<typeof subsystemLogger> =
19
19
  subsystemLogger('bsky:dp')
20
+ export const ageAssuranceLogger: ReturnType<typeof subsystemLogger> =
21
+ subsystemLogger('bsky:aa')
20
22
  export const httpLogger: ReturnType<typeof subsystemLogger> =
21
23
  subsystemLogger('bsky')
22
24
 
@@ -3856,6 +3856,16 @@ export class AgeAssuranceStatus extends Message<AgeAssuranceStatus> {
3856
3856
  */
3857
3857
  overrideApplied = false;
3858
3858
 
3859
+ /**
3860
+ * @generated from field: string access = 4;
3861
+ */
3862
+ access = "";
3863
+
3864
+ /**
3865
+ * @generated from field: string access_reason = 5;
3866
+ */
3867
+ accessReason = "";
3868
+
3859
3869
  constructor(data?: PartialMessage<AgeAssuranceStatus>) {
3860
3870
  super();
3861
3871
  proto3.util.initPartial(data, this);
@@ -3867,6 +3877,8 @@ export class AgeAssuranceStatus extends Message<AgeAssuranceStatus> {
3867
3877
  { no: 1, name: "status", kind: "scalar", T: 9 /* ScalarType.STRING */ },
3868
3878
  { no: 2, name: "last_initiated_at", kind: "message", T: Timestamp },
3869
3879
  { no: 3, name: "override_applied", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
3880
+ { no: 4, name: "access", kind: "scalar", T: 9 /* ScalarType.STRING */ },
3881
+ { no: 5, name: "access_reason", kind: "scalar", T: 9 /* ScalarType.STRING */ },
3870
3882
  ]);
3871
3883
 
3872
3884
  static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): AgeAssuranceStatus {
package/src/stash.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { LexValue, stringifyLex } from '@atproto/lexicon'
4
4
  import { BsyncClient } from './bsync'
5
5
  import { lexicons } from './lexicon/lexicons'
6
+ import { Event as AgeAssuranceEventV2 } from './lexicon/types/app/bsky/ageassurance/defs'
6
7
  import { Bookmark } from './lexicon/types/app/bsky/bookmark/defs'
7
8
  import {
8
9
  Preferences,
@@ -22,6 +23,8 @@ export const Namespaces = {
22
23
  'app.bsky.notification.defs#subjectActivitySubscription' satisfies PickNSID<SubjectActivitySubscription>,
23
24
  AppBskyUnspeccedDefsAgeAssuranceEvent:
24
25
  'app.bsky.unspecced.defs#ageAssuranceEvent' satisfies PickNSID<AgeAssuranceEvent>,
26
+ AppBskyAgeassuranceDefsEvent:
27
+ 'app.bsky.ageassurance.defs#event' satisfies PickNSID<AgeAssuranceEventV2>,
25
28
  }
26
29
 
27
30
  export type Namespace = (typeof Namespaces)[keyof typeof Namespaces]