@atproto/bsky 0.0.170 → 0.0.171

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 (132) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.d.ts +4 -0
  3. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.d.ts.map +1 -0
  4. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.js +36 -0
  5. package/dist/api/app/bsky/unspecced/getAgeAssuranceState.js.map +1 -0
  6. package/dist/api/app/bsky/unspecced/initAgeAssurance.d.ts +4 -0
  7. package/dist/api/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -0
  8. package/dist/api/app/bsky/unspecced/initAgeAssurance.js +59 -0
  9. package/dist/api/app/bsky/unspecced/initAgeAssurance.js.map +1 -0
  10. package/dist/api/external.d.ts +4 -0
  11. package/dist/api/external.d.ts.map +1 -0
  12. package/dist/api/external.js +47 -0
  13. package/dist/api/external.js.map +1 -0
  14. package/dist/api/index.d.ts +1 -0
  15. package/dist/api/index.d.ts.map +1 -1
  16. package/dist/api/index.js +6 -1
  17. package/dist/api/index.js.map +1 -1
  18. package/dist/api/kws/api.d.ts +4 -0
  19. package/dist/api/kws/api.d.ts.map +1 -0
  20. package/dist/api/kws/api.js +60 -0
  21. package/dist/api/kws/api.js.map +1 -0
  22. package/dist/api/kws/index.d.ts +4 -0
  23. package/dist/api/kws/index.d.ts.map +1 -0
  24. package/dist/api/kws/index.js +21 -0
  25. package/dist/api/kws/index.js.map +1 -0
  26. package/dist/api/kws/types.d.ts +100 -0
  27. package/dist/api/kws/types.d.ts.map +1 -0
  28. package/dist/api/kws/types.js +29 -0
  29. package/dist/api/kws/types.js.map +1 -0
  30. package/dist/api/kws/util.d.ts +21 -0
  31. package/dist/api/kws/util.d.ts.map +1 -0
  32. package/dist/api/kws/util.js +78 -0
  33. package/dist/api/kws/util.js.map +1 -0
  34. package/dist/api/kws/webhook.d.ts +5 -0
  35. package/dist/api/kws/webhook.d.ts.map +1 -0
  36. package/dist/api/kws/webhook.js +80 -0
  37. package/dist/api/kws/webhook.js.map +1 -0
  38. package/dist/config.d.ts +12 -0
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +40 -0
  41. package/dist/config.js.map +1 -1
  42. package/dist/context.d.ts +3 -0
  43. package/dist/context.d.ts.map +1 -1
  44. package/dist/context.js +3 -0
  45. package/dist/context.js.map +1 -1
  46. package/dist/data-plane/bsync/index.d.ts.map +1 -1
  47. package/dist/data-plane/bsync/index.js +52 -33
  48. package/dist/data-plane/bsync/index.js.map +1 -1
  49. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.d.ts +4 -0
  50. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.d.ts.map +1 -0
  51. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.js +22 -0
  52. package/dist/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.js.map +1 -0
  53. package/dist/data-plane/server/db/migrations/index.d.ts +1 -0
  54. package/dist/data-plane/server/db/migrations/index.d.ts.map +1 -1
  55. package/dist/data-plane/server/db/migrations/index.js +2 -0
  56. package/dist/data-plane/server/db/migrations/index.js.map +1 -1
  57. package/dist/data-plane/server/db/tables/actor.d.ts +2 -0
  58. package/dist/data-plane/server/db/tables/actor.d.ts.map +1 -1
  59. package/dist/data-plane/server/db/tables/actor.js.map +1 -1
  60. package/dist/data-plane/server/routes/profile.d.ts.map +1 -1
  61. package/dist/data-plane/server/routes/profile.js +14 -1
  62. package/dist/data-plane/server/routes/profile.js.map +1 -1
  63. package/dist/feature-gates.d.ts +2 -1
  64. package/dist/feature-gates.d.ts.map +1 -1
  65. package/dist/feature-gates.js +1 -0
  66. package/dist/feature-gates.js.map +1 -1
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +5 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/kws.d.ts +16 -0
  71. package/dist/kws.d.ts.map +1 -0
  72. package/dist/kws.js +86 -0
  73. package/dist/kws.js.map +1 -0
  74. package/dist/lexicon/index.d.ts +6 -2
  75. package/dist/lexicon/index.d.ts.map +1 -1
  76. package/dist/lexicon/index.js +12 -4
  77. package/dist/lexicon/index.js.map +1 -1
  78. package/dist/lexicon/lexicons.d.ts +308 -82
  79. package/dist/lexicon/lexicons.d.ts.map +1 -1
  80. package/dist/lexicon/lexicons.js +157 -42
  81. package/dist/lexicon/lexicons.js.map +1 -1
  82. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts +32 -0
  83. package/dist/lexicon/types/app/bsky/unspecced/defs.d.ts.map +1 -1
  84. package/dist/lexicon/types/app/bsky/unspecced/defs.js +18 -0
  85. package/dist/lexicon/types/app/bsky/unspecced/defs.js.map +1 -1
  86. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.d.ts +18 -0
  87. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.d.ts.map +1 -0
  88. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.js +7 -0
  89. package/dist/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.js.map +1 -0
  90. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts +28 -0
  91. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.d.ts.map +1 -0
  92. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.js +7 -0
  93. package/dist/lexicon/types/app/bsky/unspecced/initAgeAssurance.js.map +1 -0
  94. package/dist/proto/bsky_pb.d.ts +29 -0
  95. package/dist/proto/bsky_pb.d.ts.map +1 -1
  96. package/dist/proto/bsky_pb.js +97 -4
  97. package/dist/proto/bsky_pb.js.map +1 -1
  98. package/dist/stash.d.ts +1 -0
  99. package/dist/stash.d.ts.map +1 -1
  100. package/dist/stash.js +1 -0
  101. package/dist/stash.js.map +1 -1
  102. package/package.json +7 -4
  103. package/proto/bsky.proto +7 -0
  104. package/src/api/app/bsky/unspecced/getAgeAssuranceState.ts +46 -0
  105. package/src/api/app/bsky/unspecced/initAgeAssurance.ts +71 -0
  106. package/src/api/external.ts +13 -0
  107. package/src/api/index.ts +6 -0
  108. package/src/api/kws/api.ts +92 -0
  109. package/src/api/kws/index.ts +23 -0
  110. package/src/api/kws/types.ts +67 -0
  111. package/src/api/kws/util.ts +111 -0
  112. package/src/api/kws/webhook.ts +107 -0
  113. package/src/config.ts +59 -0
  114. package/src/context.ts +6 -0
  115. package/src/data-plane/bsync/index.ts +69 -33
  116. package/src/data-plane/server/db/migrations/20250627T025331240Z-add-actor-age-assurance-columns.ts +22 -0
  117. package/src/data-plane/server/db/migrations/index.ts +1 -0
  118. package/src/data-plane/server/db/tables/actor.ts +2 -0
  119. package/src/data-plane/server/routes/profile.ts +16 -1
  120. package/src/feature-gates.ts +1 -0
  121. package/src/index.ts +7 -1
  122. package/src/kws.ts +108 -0
  123. package/src/lexicon/index.ts +37 -11
  124. package/src/lexicon/lexicons.ts +166 -43
  125. package/src/lexicon/types/app/bsky/unspecced/defs.ts +50 -0
  126. package/src/lexicon/types/app/bsky/unspecced/getAgeAssuranceState.ts +34 -0
  127. package/src/lexicon/types/app/bsky/unspecced/initAgeAssurance.ts +47 -0
  128. package/src/proto/bsky_pb.ts +79 -0
  129. package/src/stash.ts +3 -0
  130. package/tests/views/age-assurance.test.ts +425 -0
  131. package/tsconfig.build.tsbuildinfo +1 -1
  132. package/tsconfig.tests.tsbuildinfo +1 -1
package/src/config.ts CHANGED
@@ -6,6 +6,17 @@ type LiveNowConfig = {
6
6
  domains: string[]
7
7
  }[]
8
8
 
9
+ export interface KwsConfig {
10
+ apiKey: string
11
+ apiOrigin: string
12
+ authOrigin: string
13
+ clientId: string
14
+ redirectUrl: string
15
+ userAgent: string
16
+ verificationSecret: string
17
+ webhookSecret: string
18
+ }
19
+
9
20
  export interface ServerConfigValues {
10
21
  // service
11
22
  version?: string
@@ -72,6 +83,7 @@ export interface ServerConfigValues {
72
83
  proxyMaxResponseSize?: number
73
84
  proxyMaxRetries?: number
74
85
  proxyPreferCompressed?: boolean
86
+ kws?: KwsConfig
75
87
  }
76
88
 
77
89
  export class ServerConfig {
@@ -222,6 +234,48 @@ export class ServerConfig {
222
234
  const proxyPreferCompressed =
223
235
  process.env.BSKY_PROXY_PREFER_COMPRESSED === 'true'
224
236
 
237
+ let kws: KwsConfig | undefined
238
+ const kwsApiKey = process.env.BSKY_KWS_API_KEY
239
+ const kwsApiOrigin = process.env.BSKY_KWS_API_ORIGIN
240
+ const kwsAuthOrigin = process.env.BSKY_KWS_AUTH_ORIGIN
241
+ const kwsClientId = process.env.BSKY_KWS_CLIENT_ID
242
+ const kwsRedirectUrl = process.env.BSKY_KWS_REDIRECT_URL
243
+ const kwsUserAgent = process.env.BSKY_KWS_USER_AGENT
244
+ const kwsVerificationSecret = process.env.BSKY_KWS_VERIFICATION_SECRET
245
+ const kwsWebhookSecret = process.env.BSKY_KWS_WEBHOOK_SIGNING_KEY
246
+ if (
247
+ kwsApiKey ||
248
+ kwsApiOrigin ||
249
+ kwsAuthOrigin ||
250
+ kwsClientId ||
251
+ kwsRedirectUrl ||
252
+ kwsUserAgent ||
253
+ kwsVerificationSecret ||
254
+ kwsWebhookSecret
255
+ ) {
256
+ assert(
257
+ kwsApiOrigin &&
258
+ kwsAuthOrigin &&
259
+ kwsClientId &&
260
+ kwsRedirectUrl &&
261
+ kwsUserAgent &&
262
+ kwsVerificationSecret &&
263
+ kwsWebhookSecret &&
264
+ kwsApiKey,
265
+ 'all KWS environment variables must be set if any are set',
266
+ )
267
+ kws = {
268
+ apiKey: kwsApiKey,
269
+ apiOrigin: kwsApiOrigin,
270
+ authOrigin: kwsAuthOrigin,
271
+ clientId: kwsClientId,
272
+ redirectUrl: kwsRedirectUrl,
273
+ userAgent: kwsUserAgent,
274
+ verificationSecret: kwsVerificationSecret,
275
+ webhookSecret: kwsWebhookSecret,
276
+ }
277
+ }
278
+
225
279
  return new ServerConfig({
226
280
  version,
227
281
  debugMode,
@@ -279,6 +333,7 @@ export class ServerConfig {
279
333
  proxyMaxResponseSize,
280
334
  proxyMaxRetries,
281
335
  proxyPreferCompressed,
336
+ kws,
282
337
  ...stripUndefineds(overrides ?? {}),
283
338
  })
284
339
  }
@@ -513,6 +568,10 @@ export class ServerConfig {
513
568
  get proxyPreferCompressed(): boolean {
514
569
  return this.cfg.proxyPreferCompressed ?? true
515
570
  }
571
+
572
+ get kws() {
573
+ return this.cfg.kws
574
+ }
516
575
  }
517
576
 
518
577
  function stripUndefineds(
package/src/context.ts CHANGED
@@ -12,6 +12,7 @@ import { CourierClient } from './courier'
12
12
  import { DataPlaneClient, HostList } from './data-plane/client'
13
13
  import { FeatureGates } from './feature-gates'
14
14
  import { Hydrator } from './hydration/hydrator'
15
+ import { KwsClient } from './kws'
15
16
  import { httpLogger as log } from './logger'
16
17
  import { StashClient } from './stash'
17
18
  import {
@@ -41,6 +42,7 @@ export class AppContext {
41
42
  authVerifier: AuthVerifier
42
43
  featureGates: FeatureGates
43
44
  blobDispatcher: Dispatcher
45
+ kwsClient: KwsClient | undefined
44
46
  },
45
47
  ) {}
46
48
 
@@ -116,6 +118,10 @@ export class AppContext {
116
118
  return this.opts.blobDispatcher
117
119
  }
118
120
 
121
+ get kwsClient(): KwsClient | undefined {
122
+ return this.opts.kwsClient
123
+ }
124
+
119
125
  reqLabelers(req: express.Request): ParsedLabelers {
120
126
  const val = req.header('atproto-accept-labelers')
121
127
  let parsed: ParsedLabelers | null
@@ -9,6 +9,7 @@ import { jsonStringToLex } from '@atproto/lexicon'
9
9
  import { AtUri } from '@atproto/syntax'
10
10
  import { ids } from '../../lexicon/lexicons'
11
11
  import { SubjectActivitySubscription } from '../../lexicon/types/app/bsky/notification/defs'
12
+ import { AgeAssuranceEvent } from '../../lexicon/types/app/bsky/unspecced/defs'
12
13
  import { httpLogger } from '../../logger'
13
14
  import { Service } from '../../proto/bsync_connect'
14
15
  import {
@@ -159,10 +160,10 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
159
160
 
160
161
  const now = new Date().toISOString()
161
162
 
162
- // index all items into private_data
163
+ // Index all items into private_data.
163
164
  await handleGenericOperation(db, req, now)
164
165
 
165
- // maintain bespoke indexes for certain namespaces
166
+ // Maintain bespoke indexes for certain namespaces.
166
167
  if (
167
168
  namespace ===
168
169
  Namespaces.AppBskyNotificationDefsSubjectActivitySubscription
@@ -174,6 +175,16 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
174
175
  'mock bsync put operation failed',
175
176
  ),
176
177
  )
178
+ } else if (
179
+ namespace === Namespaces.AppBskyUnspeccedDefsAgeAssuranceEvent
180
+ ) {
181
+ await handleAgeAssuranceEventOperation(db, req, now).catch(
182
+ (err: unknown) =>
183
+ httpLogger.warn(
184
+ { err, namespace },
185
+ 'mock bsync put operation failed',
186
+ ),
187
+ )
177
188
  }
178
189
 
179
190
  return {
@@ -197,6 +208,43 @@ const createRoutes = (db: Database) => (router: ConnectRouter) =>
197
208
  },
198
209
  })
199
210
 
211
+ // upsert into or remove from private_data
212
+ const handleGenericOperation = async (
213
+ db: Database,
214
+ req: PutOperationRequest,
215
+ now: string,
216
+ ) => {
217
+ const { actorDid, namespace, key, method, payload } = req
218
+ if (method === Method.CREATE || method === Method.UPDATE) {
219
+ await db.db
220
+ .insertInto('private_data')
221
+ .values({
222
+ actorDid,
223
+ namespace,
224
+ key,
225
+ payload: Buffer.from(payload).toString('utf8'),
226
+ indexedAt: now,
227
+ updatedAt: now,
228
+ })
229
+ .onConflict((oc) =>
230
+ oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({
231
+ payload: excluded(db.db, 'payload'),
232
+ updatedAt: excluded(db.db, 'updatedAt'),
233
+ }),
234
+ )
235
+ .execute()
236
+ } else if (method === Method.DELETE) {
237
+ await db.db
238
+ .deleteFrom('private_data')
239
+ .where('actorDid', '=', actorDid)
240
+ .where('namespace', '=', namespace)
241
+ .where('key', '=', key)
242
+ .execute()
243
+ } else {
244
+ assert.fail(`unexpected method ${method}`)
245
+ }
246
+ }
247
+
200
248
  const handleSubjectActivitySubscriptionOperation = async (
201
249
  db: Database,
202
250
  req: PutOperationRequest,
@@ -246,39 +294,27 @@ const handleSubjectActivitySubscriptionOperation = async (
246
294
  .execute()
247
295
  }
248
296
 
249
- // upsert into or remove from private_data
250
- const handleGenericOperation = async (
297
+ const handleAgeAssuranceEventOperation = async (
251
298
  db: Database,
252
299
  req: PutOperationRequest,
253
- now: string,
300
+ _now: string,
254
301
  ) => {
255
- const { actorDid, namespace, key, method, payload } = req
256
- if (method === Method.CREATE || method === Method.UPDATE) {
257
- await db.db
258
- .insertInto('private_data')
259
- .values({
260
- actorDid,
261
- namespace,
262
- key,
263
- payload: Buffer.from(payload).toString('utf8'),
264
- indexedAt: now,
265
- updatedAt: now,
266
- })
267
- .onConflict((oc) =>
268
- oc.columns(['actorDid', 'namespace', 'key']).doUpdateSet({
269
- payload: excluded(db.db, 'payload'),
270
- updatedAt: excluded(db.db, 'updatedAt'),
271
- }),
272
- )
273
- .execute()
274
- } else if (method === Method.DELETE) {
275
- await db.db
276
- .deleteFrom('private_data')
277
- .where('actorDid', '=', actorDid)
278
- .where('namespace', '=', namespace)
279
- .where('key', '=', key)
280
- .execute()
281
- } else {
282
- assert.fail(`unexpected method ${method}`)
302
+ const { actorDid, method, payload } = req
303
+ if (method !== Method.CREATE) return
304
+
305
+ const parsed = jsonStringToLex(
306
+ Buffer.from(payload).toString('utf8'),
307
+ ) as AgeAssuranceEvent
308
+ const { status, createdAt } = parsed
309
+
310
+ const update = {
311
+ ageAssuranceStatus: status,
312
+ ageAssuranceLastInitiatedAt: status === 'pending' ? createdAt : undefined,
283
313
  }
314
+
315
+ return db.db
316
+ .updateTable('actor')
317
+ .set(update)
318
+ .where('did', '=', actorDid)
319
+ .execute()
284
320
  }
@@ -0,0 +1,22 @@
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('ageAssuranceStatus', 'text')
7
+ .execute()
8
+
9
+ await db.schema
10
+ .alterTable('actor')
11
+ .addColumn('ageAssuranceLastInitiatedAt', 'varchar')
12
+ .execute()
13
+ }
14
+
15
+ export async function down(db: Kysely<unknown>): Promise<void> {
16
+ await db.schema.alterTable('actor').dropColumn('ageAssuranceStatus').execute()
17
+
18
+ await db.schema
19
+ .alterTable('actor')
20
+ .dropColumn('ageAssuranceLastInitiatedAt')
21
+ .execute()
22
+ }
@@ -52,3 +52,4 @@ export * as _20250526T023712742Z from './20250526T023712742Z-like-repost-via'
52
52
  export * as _20250528T221913281Z from './20250528T221913281Z-add-record-tags'
53
53
  export * as _20250602T190357447Z from './20250602T190357447Z-add-private-data'
54
54
  export * as _20250611T140649895Z from './20250611T140649895Z-add-activity-subscription'
55
+ export * as _20250627T025331240Z from './20250627T025331240Z-add-actor-age-assurance-columns'
@@ -7,6 +7,8 @@ export interface Actor {
7
7
  takedownRef: string | null
8
8
  upstreamStatus: string | null
9
9
  trustedVerifier: Generated<boolean>
10
+ ageAssuranceStatus: string | null
11
+ ageAssuranceLastInitiatedAt: string | null
10
12
  }
11
13
 
12
14
  export const tableName = 'actor'
@@ -22,7 +22,7 @@ type VerifiedBy = {
22
22
 
23
23
  export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
24
24
  async getActors(req) {
25
- const { dids } = req
25
+ const { dids, returnAgeAssuranceForDids } = req
26
26
  if (dids.length === 0) {
27
27
  return { actors: [] }
28
28
  }
@@ -105,6 +105,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
105
105
  }
106
106
  return acc
107
107
  }, {} as VerifiedBy)
108
+ const ageAssuranceForDids = new Set(returnAgeAssuranceForDids)
108
109
 
109
110
  const activitySubscription = () => {
110
111
  const record = parseRecordBytes<AppBskyNotificationDeclaration.Record>(
@@ -128,6 +129,19 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
128
129
  }
129
130
  }
130
131
 
132
+ const ageAssuranceStatus = () => {
133
+ if (!ageAssuranceForDids.has(did)) {
134
+ return undefined
135
+ }
136
+
137
+ return {
138
+ status: row?.ageAssuranceStatus ?? 'unknown',
139
+ lastInitiatedAt: row?.ageAssuranceLastInitiatedAt
140
+ ? Timestamp.fromDate(new Date(row?.ageAssuranceLastInitiatedAt))
141
+ : undefined,
142
+ }
143
+ }
144
+
131
145
  return {
132
146
  exists: !!row,
133
147
  handle: row?.handle ?? undefined,
@@ -149,6 +163,7 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
149
163
  tags: [],
150
164
  profileTags: [],
151
165
  allowActivitySubscriptionsFrom: activitySubscription(),
166
+ ageAssuranceStatus: ageAssuranceStatus(),
152
167
  }
153
168
  })
154
169
  return { actors }
@@ -13,6 +13,7 @@ export enum GateID {
13
13
  * appease TS
14
14
  */
15
15
  _ = '',
16
+ AgeAssurance = 'age_assurance',
16
17
  }
17
18
 
18
19
  /**
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ import { AtpAgent } from '@atproto/api'
10
10
  import { DAY, SECOND } from '@atproto/common'
11
11
  import { Keypair } from '@atproto/crypto'
12
12
  import { IdResolver } from '@atproto/identity'
13
- import API, { blobResolver, health, wellKnown } from './api'
13
+ import API, { blobResolver, external, health, wellKnown } from './api'
14
14
  import { createBlobDispatcher } from './api/blob-dispatcher'
15
15
  import { AuthVerifier, createPublicKeyObject } from './auth-verifier'
16
16
  import { authWithApiKey as bsyncAuth, createBsyncClient } from './bsync'
@@ -27,6 +27,7 @@ import { FeatureGates } from './feature-gates'
27
27
  import { Hydrator } from './hydration/hydrator'
28
28
  import * as imageServer from './image/server'
29
29
  import { ImageUriBuilder } from './image/uri'
30
+ import { createKwsClient } from './kws'
30
31
  import { createServer } from './lexicon'
31
32
  import { loggerMiddleware } from './logger'
32
33
  import { createStashClient } from './stash'
@@ -58,6 +59,7 @@ export class BskyAppView {
58
59
  }): BskyAppView {
59
60
  const { config, signingKey } = opts
60
61
  const app = express()
62
+ app.set('trust proxy', true)
61
63
  app.use(cors({ maxAge: DAY / SECOND }))
62
64
  app.use(loggerMiddleware)
63
65
  app.use(compression())
@@ -150,6 +152,8 @@ export class BskyAppView {
150
152
  })
151
153
  : undefined
152
154
 
155
+ const kwsClient = config.kws ? createKwsClient(config.kws) : undefined
156
+
153
157
  const entrywayJwtPublicKey = config.entrywayJwtPublicKeyHex
154
158
  ? createPublicKeyObject(config.entrywayJwtPublicKeyHex)
155
159
  : undefined
@@ -186,6 +190,7 @@ export class BskyAppView {
186
190
  authVerifier,
187
191
  featureGates,
188
192
  blobDispatcher,
193
+ kwsClient,
189
194
  })
190
195
 
191
196
  let server = createServer({
@@ -205,6 +210,7 @@ export class BskyAppView {
205
210
  app.use(imageServer.createMiddleware(ctx, { prefix: '/img/' }))
206
211
  app.use(server.xrpc.router)
207
212
  app.use(error.handler)
213
+ app.use('/external', external.createRouter(ctx))
208
214
 
209
215
  return new BskyAppView({ ctx, app })
210
216
  }
package/src/kws.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod'
2
+ import { KwsExternalPayload } from './api/kws/types'
3
+ import { serializeExternalPayload } from './api/kws/util'
4
+ import { buildBasicAuth } from './auth-verifier'
5
+ import { KwsConfig } from './config'
6
+ import { httpLogger as log } from './logger'
7
+
8
+ export const createKwsClient = (cfg: KwsConfig): KwsClient => {
9
+ return new KwsClient(cfg)
10
+ }
11
+
12
+ // Not `.strict()` to avoid breaking if KWS adds fields.
13
+ const authResponseSchema = z.object({
14
+ access_token: z.string(),
15
+ })
16
+
17
+ export class KwsClient {
18
+ constructor(public cfg: KwsConfig) {}
19
+
20
+ private async auth() {
21
+ try {
22
+ const res = await fetch(
23
+ `${this.cfg.authOrigin}/auth/realms/kws/protocol/openid-connect/token`,
24
+ {
25
+ method: 'POST',
26
+ headers: {
27
+ Accept: 'application/json',
28
+ 'Content-Type': 'application/x-www-form-urlencoded',
29
+ Authorization: buildBasicAuth(this.cfg.clientId, this.cfg.apiKey),
30
+ },
31
+ body: new URLSearchParams({
32
+ grant_type: 'client_credentials',
33
+ scope: 'verification',
34
+ }),
35
+ },
36
+ )
37
+ if (!res.ok) {
38
+ const errorText = await res.text()
39
+ throw new Error(
40
+ `Failed to fetch age assurance access token: status: ${res.status}, statusText: ${res.statusText}, errorText: ${errorText}`,
41
+ )
42
+ }
43
+
44
+ const auth = await res.json()
45
+ const authResponse = authResponseSchema.parse(auth)
46
+ return authResponse.access_token
47
+ } catch (err) {
48
+ log.error({ err }, 'Failed to authenticate with KWS')
49
+ throw err
50
+ }
51
+ }
52
+
53
+ private async fetchWithAuth(
54
+ url: string,
55
+ init: RequestInit,
56
+ ): Promise<Response> {
57
+ const accessToken = await this.auth()
58
+
59
+ return fetch(url, {
60
+ ...init,
61
+ headers: {
62
+ ...(init.headers ?? {}),
63
+ Authorization: `Bearer ${accessToken}`,
64
+ },
65
+ })
66
+ }
67
+
68
+ async sendEmail({
69
+ countryCode,
70
+ email,
71
+ externalPayload,
72
+ language,
73
+ }: {
74
+ countryCode: string
75
+ email: string
76
+ externalPayload: KwsExternalPayload
77
+ language: string
78
+ }) {
79
+ const res = await this.fetchWithAuth(
80
+ `${this.cfg.apiOrigin}/v1/verifications/send-email`,
81
+ {
82
+ method: 'POST',
83
+ headers: {
84
+ 'Content-Type': 'application/json',
85
+ 'User-Agent': this.cfg.userAgent,
86
+ },
87
+ body: JSON.stringify({
88
+ email,
89
+ externalPayload: serializeExternalPayload(externalPayload),
90
+ language,
91
+ location: countryCode,
92
+ userContext: 'adult',
93
+ }),
94
+ },
95
+ )
96
+
97
+ if (!res.ok) {
98
+ const errorText = await res.text()
99
+ log.error(
100
+ { status: res.status, statusText: res.statusText, errorText },
101
+ 'Failed to send age assurance email',
102
+ )
103
+ throw new Error('Failed to send age assurance email')
104
+ }
105
+
106
+ return res.json()
107
+ }
108
+ }
@@ -109,8 +109,8 @@ import * as AppBskyFeedGetFeedGenerators from './types/app/bsky/feed/getFeedGene
109
109
  import * as AppBskyFeedGetFeedSkeleton from './types/app/bsky/feed/getFeedSkeleton.js'
110
110
  import * as AppBskyFeedGetLikes from './types/app/bsky/feed/getLikes.js'
111
111
  import * as AppBskyFeedGetListFeed from './types/app/bsky/feed/getListFeed.js'
112
- import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'
113
112
  import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread.js'
113
+ import * as AppBskyFeedGetPosts from './types/app/bsky/feed/getPosts.js'
114
114
  import * as AppBskyFeedGetQuotes from './types/app/bsky/feed/getQuotes.js'
115
115
  import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy.js'
116
116
  import * as AppBskyFeedGetSuggestedFeeds from './types/app/bsky/feed/getSuggestedFeeds.js'
@@ -148,6 +148,7 @@ import * as AppBskyNotificationPutPreferences from './types/app/bsky/notificatio
148
148
  import * as AppBskyNotificationPutPreferencesV2 from './types/app/bsky/notification/putPreferencesV2.js'
149
149
  import * as AppBskyNotificationRegisterPush from './types/app/bsky/notification/registerPush.js'
150
150
  import * as AppBskyNotificationUpdateSeen from './types/app/bsky/notification/updateSeen.js'
151
+ import * as AppBskyUnspeccedGetAgeAssuranceState from './types/app/bsky/unspecced/getAgeAssuranceState.js'
151
152
  import * as AppBskyUnspeccedGetConfig from './types/app/bsky/unspecced/getConfig.js'
152
153
  import * as AppBskyUnspeccedGetPopularFeedGenerators from './types/app/bsky/unspecced/getPopularFeedGenerators.js'
153
154
  import * as AppBskyUnspeccedGetPostThreadOtherV2 from './types/app/bsky/unspecced/getPostThreadOtherV2.js'
@@ -163,6 +164,7 @@ import * as AppBskyUnspeccedGetTaggedSuggestions from './types/app/bsky/unspecce
163
164
  import * as AppBskyUnspeccedGetTrendingTopics from './types/app/bsky/unspecced/getTrendingTopics.js'
164
165
  import * as AppBskyUnspeccedGetTrends from './types/app/bsky/unspecced/getTrends.js'
165
166
  import * as AppBskyUnspeccedGetTrendsSkeleton from './types/app/bsky/unspecced/getTrendsSkeleton.js'
167
+ import * as AppBskyUnspeccedInitAgeAssurance from './types/app/bsky/unspecced/initAgeAssurance.js'
166
168
  import * as AppBskyUnspeccedSearchActorsSkeleton from './types/app/bsky/unspecced/searchActorsSkeleton.js'
167
169
  import * as AppBskyUnspeccedSearchPostsSkeleton from './types/app/bsky/unspecced/searchPostsSkeleton.js'
168
170
  import * as AppBskyUnspeccedSearchStarterPacksSkeleton from './types/app/bsky/unspecced/searchStarterPacksSkeleton.js'
@@ -1597,27 +1599,27 @@ export class AppBskyFeedNS {
1597
1599
  return this._server.xrpc.method(nsid, cfg)
1598
1600
  }
1599
1601
 
1600
- getPosts<A extends Auth = void>(
1602
+ getPostThread<A extends Auth = void>(
1601
1603
  cfg: MethodConfigOrHandler<
1602
1604
  A,
1603
- AppBskyFeedGetPosts.QueryParams,
1604
- AppBskyFeedGetPosts.HandlerInput,
1605
- AppBskyFeedGetPosts.HandlerOutput
1605
+ AppBskyFeedGetPostThread.QueryParams,
1606
+ AppBskyFeedGetPostThread.HandlerInput,
1607
+ AppBskyFeedGetPostThread.HandlerOutput
1606
1608
  >,
1607
1609
  ) {
1608
- const nsid = 'app.bsky.feed.getPosts' // @ts-ignore
1610
+ const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore
1609
1611
  return this._server.xrpc.method(nsid, cfg)
1610
1612
  }
1611
1613
 
1612
- getPostThread<A extends Auth = void>(
1614
+ getPosts<A extends Auth = void>(
1613
1615
  cfg: MethodConfigOrHandler<
1614
1616
  A,
1615
- AppBskyFeedGetPostThread.QueryParams,
1616
- AppBskyFeedGetPostThread.HandlerInput,
1617
- AppBskyFeedGetPostThread.HandlerOutput
1617
+ AppBskyFeedGetPosts.QueryParams,
1618
+ AppBskyFeedGetPosts.HandlerInput,
1619
+ AppBskyFeedGetPosts.HandlerOutput
1618
1620
  >,
1619
1621
  ) {
1620
- const nsid = 'app.bsky.feed.getPostThread' // @ts-ignore
1622
+ const nsid = 'app.bsky.feed.getPosts' // @ts-ignore
1621
1623
  return this._server.xrpc.method(nsid, cfg)
1622
1624
  }
1623
1625
 
@@ -2105,6 +2107,18 @@ export class AppBskyUnspeccedNS {
2105
2107
  this._server = server
2106
2108
  }
2107
2109
 
2110
+ getAgeAssuranceState<A extends Auth = void>(
2111
+ cfg: MethodConfigOrHandler<
2112
+ A,
2113
+ AppBskyUnspeccedGetAgeAssuranceState.QueryParams,
2114
+ AppBskyUnspeccedGetAgeAssuranceState.HandlerInput,
2115
+ AppBskyUnspeccedGetAgeAssuranceState.HandlerOutput
2116
+ >,
2117
+ ) {
2118
+ const nsid = 'app.bsky.unspecced.getAgeAssuranceState' // @ts-ignore
2119
+ return this._server.xrpc.method(nsid, cfg)
2120
+ }
2121
+
2108
2122
  getConfig<A extends Auth = void>(
2109
2123
  cfg: MethodConfigOrHandler<
2110
2124
  A,
@@ -2285,6 +2299,18 @@ export class AppBskyUnspeccedNS {
2285
2299
  return this._server.xrpc.method(nsid, cfg)
2286
2300
  }
2287
2301
 
2302
+ initAgeAssurance<A extends Auth = void>(
2303
+ cfg: MethodConfigOrHandler<
2304
+ A,
2305
+ AppBskyUnspeccedInitAgeAssurance.QueryParams,
2306
+ AppBskyUnspeccedInitAgeAssurance.HandlerInput,
2307
+ AppBskyUnspeccedInitAgeAssurance.HandlerOutput
2308
+ >,
2309
+ ) {
2310
+ const nsid = 'app.bsky.unspecced.initAgeAssurance' // @ts-ignore
2311
+ return this._server.xrpc.method(nsid, cfg)
2312
+ }
2313
+
2288
2314
  searchActorsSkeleton<A extends Auth = void>(
2289
2315
  cfg: MethodConfigOrHandler<
2290
2316
  A,