@atproto/bsky 0.0.216 → 0.0.218

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 (92) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
  3. package/dist/api/app/bsky/feed/searchPosts.js +6 -4
  4. package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
  5. package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
  6. package/dist/api/app/bsky/unspecced/getPostThreadV2.js +1 -1
  7. package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
  8. package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.d.ts.map +1 -1
  9. package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js +9 -2
  10. package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js.map +1 -1
  11. package/dist/api/app/bsky/unspecced/getSuggestedUsers.d.ts.map +1 -1
  12. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js +9 -2
  13. package/dist/api/app/bsky/unspecced/getSuggestedUsers.js.map +1 -1
  14. package/dist/auth-verifier.d.ts +1 -1
  15. package/dist/config.d.ts +2 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +5 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/context.d.ts +3 -3
  20. package/dist/context.d.ts.map +1 -1
  21. package/dist/context.js +2 -2
  22. package/dist/context.js.map +1 -1
  23. package/dist/data-plane/server/db/pagination.d.ts +3 -3
  24. package/dist/feature-gates/gates.d.ts +5 -0
  25. package/dist/feature-gates/gates.d.ts.map +1 -0
  26. package/dist/feature-gates/gates.js +6 -0
  27. package/dist/feature-gates/gates.js.map +1 -0
  28. package/dist/feature-gates/index.d.ts +24 -0
  29. package/dist/feature-gates/index.d.ts.map +1 -0
  30. package/dist/feature-gates/index.js +135 -0
  31. package/dist/feature-gates/index.js.map +1 -0
  32. package/dist/feature-gates/metrics.d.ts +32 -0
  33. package/dist/feature-gates/metrics.d.ts.map +1 -0
  34. package/dist/feature-gates/metrics.js +100 -0
  35. package/dist/feature-gates/metrics.js.map +1 -0
  36. package/dist/feature-gates/metrics.test.d.ts +2 -0
  37. package/dist/feature-gates/metrics.test.d.ts.map +1 -0
  38. package/dist/feature-gates/metrics.test.js +152 -0
  39. package/dist/feature-gates/metrics.test.js.map +1 -0
  40. package/dist/feature-gates/types.d.ts +49 -0
  41. package/dist/feature-gates/types.d.ts.map +1 -0
  42. package/dist/feature-gates/types.js +3 -0
  43. package/dist/feature-gates/types.js.map +1 -0
  44. package/dist/feature-gates/utils.d.ts +21 -0
  45. package/dist/feature-gates/utils.d.ts.map +1 -0
  46. package/dist/feature-gates/utils.js +85 -0
  47. package/dist/feature-gates/utils.js.map +1 -0
  48. package/dist/hydration/hydrator.d.ts +8 -3
  49. package/dist/hydration/hydrator.d.ts.map +1 -1
  50. package/dist/hydration/hydrator.js +9 -5
  51. package/dist/hydration/hydrator.js.map +1 -1
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +7 -6
  54. package/dist/index.js.map +1 -1
  55. package/dist/lexicon/lexicons.d.ts +8 -0
  56. package/dist/lexicon/lexicons.d.ts.map +1 -1
  57. package/dist/lexicon/lexicons.js +4 -0
  58. package/dist/lexicon/lexicons.js.map +1 -1
  59. package/dist/lexicon/types/app/bsky/feed/sendInteractions.d.ts +1 -0
  60. package/dist/lexicon/types/app/bsky/feed/sendInteractions.d.ts.map +1 -1
  61. package/dist/lexicon/types/app/bsky/feed/sendInteractions.js.map +1 -1
  62. package/dist/views/index.d.ts.map +1 -1
  63. package/dist/views/index.js +7 -4
  64. package/dist/views/index.js.map +1 -1
  65. package/package.json +12 -12
  66. package/src/api/app/bsky/feed/searchPosts.ts +10 -8
  67. package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +0 -1
  68. package/src/api/app/bsky/unspecced/getPostThreadV2.ts +3 -3
  69. package/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts +13 -6
  70. package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +13 -6
  71. package/src/config.ts +8 -0
  72. package/src/context.ts +4 -4
  73. package/src/feature-gates/README.md +47 -0
  74. package/src/feature-gates/gates.ts +9 -0
  75. package/src/feature-gates/index.ts +146 -0
  76. package/src/feature-gates/metrics.test.ts +196 -0
  77. package/src/feature-gates/metrics.ts +107 -0
  78. package/src/feature-gates/types.ts +52 -0
  79. package/src/feature-gates/utils.ts +90 -0
  80. package/src/hydration/hydrator.ts +12 -6
  81. package/src/index.ts +8 -7
  82. package/src/lexicon/lexicons.ts +4 -0
  83. package/src/lexicon/types/app/bsky/feed/sendInteractions.ts +1 -0
  84. package/src/views/index.ts +9 -8
  85. package/tests/views/thread.test.ts +2 -0
  86. package/tests/views/verification.test.ts +19 -37
  87. package/tsconfig.build.tsbuildinfo +1 -1
  88. package/dist/feature-gates.d.ts +0 -44
  89. package/dist/feature-gates.d.ts.map +0 -1
  90. package/dist/feature-gates.js +0 -133
  91. package/dist/feature-gates.js.map +0 -1
  92. package/src/feature-gates.ts +0 -136
@@ -0,0 +1,107 @@
1
+ import { featureGatesLogger } from '../logger'
2
+
3
+ type Events = {
4
+ 'experiment:viewed': {
5
+ experimentId: string
6
+ variationId: string
7
+ }
8
+ 'feature:viewed': {
9
+ featureId: string
10
+ featureResultValue: unknown
11
+ /** Only available if feature has experiment rules applied */
12
+ experimentId?: string
13
+ /** Only available if feature has experiment rules applied */
14
+ variationId?: string
15
+ }
16
+ }
17
+
18
+ type Event<M extends Record<string, any>> = {
19
+ time: number
20
+ event: keyof M
21
+ payload: M[keyof M]
22
+ metadata: Record<string, any>
23
+ }
24
+
25
+ export type Config = {
26
+ trackingEndpoint?: string
27
+ }
28
+
29
+ export class MetricsClient<M extends Record<string, any> = Events> {
30
+ maxBatchSize = 100
31
+
32
+ private started: boolean = false
33
+ private queue: Event<M>[] = []
34
+ private flushInterval: NodeJS.Timeout | null = null
35
+ constructor(private config: Config) {}
36
+
37
+ start() {
38
+ if (this.started) return
39
+ this.started = true
40
+ this.flushInterval = setInterval(() => {
41
+ this.flush()
42
+ }, 10_000)
43
+ }
44
+
45
+ stop() {
46
+ if (this.flushInterval) {
47
+ clearInterval(this.flushInterval)
48
+ this.flushInterval = null
49
+ }
50
+ this.flush()
51
+ }
52
+
53
+ track<E extends keyof M>(
54
+ event: E,
55
+ payload: M[E],
56
+ metadata: Record<string, any> = {},
57
+ ) {
58
+ this.start()
59
+
60
+ const e = {
61
+ source: 'appview',
62
+ time: Date.now(),
63
+ event,
64
+ payload,
65
+ metadata,
66
+ }
67
+ this.queue.push(e)
68
+
69
+ if (this.queue.length > this.maxBatchSize) {
70
+ this.flush()
71
+ }
72
+ }
73
+
74
+ flush() {
75
+ if (!this.queue.length) return
76
+ const events = this.queue.splice(0, this.queue.length)
77
+ this.sendBatch(events)
78
+ }
79
+
80
+ private async sendBatch(events: Event<M>[]) {
81
+ if (!this.config.trackingEndpoint) return
82
+
83
+ try {
84
+ const res = await fetch(this.config.trackingEndpoint, {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ body: JSON.stringify({ events }),
90
+ keepalive: true,
91
+ })
92
+
93
+ if (!res.ok) {
94
+ const errorText = await res.text().catch(() => 'Unknown error')
95
+ featureGatesLogger.error(
96
+ { err: new Error(`${res.status} Failed to fetch - ${errorText}`) },
97
+ 'Failed to send metrics',
98
+ )
99
+ } else {
100
+ // Drain response body to allow connection reuse.
101
+ await res.text().catch(() => {})
102
+ }
103
+ } catch (err) {
104
+ featureGatesLogger.error({ err }, 'Failed to send metrics')
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,52 @@
1
+ import type express from 'express'
2
+ import { FeatureGate } from './gates'
3
+
4
+ /**
5
+ * The user context passed to the feature gates client for evaluation and
6
+ * tracking purposes.
7
+ */
8
+ export type RawUserContext = {
9
+ /**
10
+ * The user's DID
11
+ */
12
+ viewer: string | null
13
+ /**
14
+ * The express request object, used to extract analytics headers for the user context
15
+ */
16
+ req: express.Request
17
+ }
18
+
19
+ /**
20
+ * Extracted values from the `RawUserContext`. These values should match the
21
+ * `attributes` we've configured for GrowthBook in our GB dashboard. We also
22
+ * send these same values as properties in our analytics events, so we want to
23
+ * make sure they are consistent.
24
+ */
25
+ export type ParsedUserContext = {
26
+ did?: string | null
27
+ deviceId: string
28
+ sessionId: string
29
+ }
30
+
31
+ /**
32
+ * This loosely matches the metadata we send from the client for analytics
33
+ * events. We want to make sure we have the same properties in both places so
34
+ * that we can correlate feature gate evaluations with analytics events.
35
+ *
36
+ * @see https://github.com/bluesky-social/social-app/blob/76109a58dc7aafccdfbd07a81cbd9925e065d1c0/src/analytics/metadata.ts
37
+ */
38
+ export type TrackingMetadata = {
39
+ base: {
40
+ deviceId: string
41
+ sessionId: string
42
+ }
43
+ session: {
44
+ did: string | undefined
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Pre-evaluated feature gates map, the result of
50
+ * `ctx.FeatureGatesClient.checkGates()`
51
+ */
52
+ export type CheckedFeatureGatesMap = Map<FeatureGate, boolean>
@@ -0,0 +1,90 @@
1
+ import crypto from 'node:crypto'
2
+ import { type UserContext as GrowthBookUserContext } from '@growthbook/growthbook'
3
+ import { ParsedUserContext, RawUserContext, TrackingMetadata } from './types'
4
+
5
+ /**
6
+ * These need to match what the client sends
7
+ */
8
+ const ANALYTICS_HEADER_DEVICE_ID = 'X-Bsky-Device-Id'
9
+ const ANALYTICS_HEADER_SESSION_ID = 'X-Bsky-Session-Id'
10
+
11
+ /**
12
+ * Parse the `RawUserContext` into a `ParsedUserContext` that is used as
13
+ * GrowthBook `attributes` as well as the metadata payload for our analytics
14
+ * events. This ensures that the same user properties are used for both feature
15
+ * gate targeting and analytics.
16
+ */
17
+ export function parseRawUserContext(
18
+ userContext: RawUserContext,
19
+ ): ParsedUserContext {
20
+ const did = userContext.viewer
21
+
22
+ // prioritize passthrough header
23
+ let deviceId = userContext.req.header(ANALYTICS_HEADER_DEVICE_ID)
24
+ if (!deviceId) {
25
+ if (did) {
26
+ /*
27
+ * If we don't have a device header, fall back to the DID. Our event
28
+ * proxy ensures ordering based on this deviceId (also called a stableId
29
+ * in the proxy), so if we have a DID, we want to use it to ensure client
30
+ * and server events are properly ordered.
31
+ */
32
+ deviceId = did
33
+ } else {
34
+ /*
35
+ * Without any better option for identifying the user, we generate a
36
+ * random deviceId.
37
+ */
38
+ deviceId = `anon-${crypto.randomUUID()}`
39
+ }
40
+ }
41
+
42
+ // prioritize passthrough header
43
+ let sessionId = userContext.req.header(ANALYTICS_HEADER_SESSION_ID)
44
+ if (!sessionId) {
45
+ /*
46
+ * Without any better option for identifying the user, we generate a
47
+ * random deviceId.
48
+ */
49
+ sessionId = `anon-${crypto.randomUUID()}`
50
+ }
51
+
52
+ return {
53
+ did,
54
+ deviceId,
55
+ sessionId,
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Extract the `ParsedUserContext` from the GrowthBook `UserContext`, which we
61
+ * passed into `isOn` as `attributes`.
62
+ */
63
+ export function extractParsedUserContextFromGrowthBookUserContext(
64
+ userContext: GrowthBookUserContext,
65
+ ): ParsedUserContext {
66
+ return {
67
+ did: userContext.attributes?.did,
68
+ deviceId: userContext.attributes?.deviceId,
69
+ sessionId: userContext.attributes?.sessionId,
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Convert the `ParsedUserContext` into the `TrackingMetadata` format that we
75
+ * use for our analytics events. This ensures that we have the same user
76
+ * properties as we do for events from our client app.
77
+ */
78
+ export function parsedUserContextToTrackingMetadata(
79
+ parsedUserContext: ParsedUserContext,
80
+ ): TrackingMetadata {
81
+ return {
82
+ base: {
83
+ deviceId: parsedUserContext.deviceId,
84
+ sessionId: parsedUserContext.sessionId,
85
+ },
86
+ session: {
87
+ did: parsedUserContext.did ?? undefined,
88
+ },
89
+ }
90
+ }
@@ -2,7 +2,7 @@ import assert from 'node:assert'
2
2
  import { mapDefined } from '@atproto/common'
3
3
  import { AtUri } from '@atproto/syntax'
4
4
  import { DataPlaneClient } from '../data-plane/client'
5
- import { type CheckedFeatureGatesMap, FeatureGateID } from '../feature-gates'
5
+ import { type CheckedFeatureGatesMap } from '../feature-gates/types'
6
6
  import { ids } from '../lexicon/lexicons'
7
7
  import { Record as ProfileRecord } from '../lexicon/types/app/bsky/actor/profile'
8
8
  import { isMain as isEmbedRecord } from '../lexicon/types/app/bsky/embed/record'
@@ -83,7 +83,13 @@ export class HydrateCtx {
83
83
  overrideIncludeTakedownsForActor = this.vals.overrideIncludeTakedownsForActor
84
84
  include3pBlocks = this.vals.include3pBlocks
85
85
  includeDebugField = this.vals.includeDebugField
86
- featureGates: CheckedFeatureGatesMap = this.vals.featureGates || new Map()
86
+ /**
87
+ * Cache of evaluated feature gates to be used in a given request lifecycle.
88
+ * The actual evaluations happen at the top of the route handler and the
89
+ * results are stored in this map.
90
+ */
91
+ featureGatesMap: CheckedFeatureGatesMap =
92
+ this.vals.featureGatesMap || new Map()
87
93
  constructor(private vals: HydrateCtxVals) {}
88
94
  // Convenience with use with dataplane.getActors cache control
89
95
  get skipCacheForViewer() {
@@ -102,7 +108,7 @@ export type HydrateCtxVals = {
102
108
  overrideIncludeTakedownsForActor?: boolean
103
109
  include3pBlocks?: boolean
104
110
  includeDebugField?: boolean
105
- featureGates?: CheckedFeatureGatesMap
111
+ featureGatesMap?: CheckedFeatureGatesMap
106
112
  }
107
113
 
108
114
  export type HydrationState = {
@@ -746,8 +752,8 @@ export class Hydrator {
746
752
  ctx: HydrateCtx,
747
753
  ): Promise<HydrationState> {
748
754
  const postsState = await this.hydratePosts(refs, ctx, undefined, {
749
- processDynamicTagsForView: ctx.featureGates.get(
750
- FeatureGateID.ThreadsReplyRankingExplorationEnable,
755
+ processDynamicTagsForView: ctx.featureGatesMap.get(
756
+ 'threads:reply_ranking_exploration:enable',
751
757
  )
752
758
  ? 'thread'
753
759
  : undefined,
@@ -1331,7 +1337,7 @@ export class Hydrator {
1331
1337
  includeTakedowns: vals.includeTakedowns,
1332
1338
  include3pBlocks: vals.include3pBlocks,
1333
1339
  includeDebugField,
1334
- featureGates: vals.featureGates,
1340
+ featureGatesMap: vals.featureGatesMap,
1335
1341
  })
1336
1342
  }
1337
1343
 
package/src/index.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  createDataPlaneClient,
24
24
  } from './data-plane/client'
25
25
  import * as error from './error'
26
- import { FeatureGates } from './feature-gates'
26
+ import { FeatureGatesClient } 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'
@@ -181,9 +181,10 @@ export class BskyAppView {
181
181
  entrywayJwtPublicKey,
182
182
  })
183
183
 
184
- const featureGates = new FeatureGates({
185
- apiHost: config.growthBookApiHost,
186
- clientKey: config.growthBookClientKey,
184
+ const featureGatesClient = new FeatureGatesClient({
185
+ growthBookApiHost: config.growthBookApiHost,
186
+ growthBookClientKey: config.growthBookClientKey,
187
+ eventProxyTrackingEndpoint: config.eventProxyTrackingEndpoint,
187
188
  })
188
189
 
189
190
  const blobDispatcher = createBlobDispatcher(config)
@@ -205,7 +206,7 @@ export class BskyAppView {
205
206
  courierClient,
206
207
  rolodexClient,
207
208
  authVerifier,
208
- featureGates,
209
+ featureGatesClient,
209
210
  blobDispatcher,
210
211
  kwsClient,
211
212
  })
@@ -241,7 +242,7 @@ export class BskyAppView {
241
242
  if (this.ctx.dataplaneHostList instanceof EtcdHostList) {
242
243
  await this.ctx.dataplaneHostList.connect()
243
244
  }
244
- await this.ctx.featureGates.start()
245
+ this.ctx.featureGatesClient.start() // lazy, no await
245
246
  const server = this.app.listen(this.ctx.cfg.port)
246
247
  this.server = server
247
248
  server.keepAliveTimeout = 90000
@@ -253,7 +254,7 @@ export class BskyAppView {
253
254
  }
254
255
 
255
256
  async destroy(): Promise<void> {
256
- this.ctx.featureGates.destroy()
257
+ this.ctx.featureGatesClient.destroy()
257
258
  await this.terminator?.terminate()
258
259
  await this.ctx.etcd?.close()
259
260
  }
@@ -4947,6 +4947,10 @@ export const schemaDict = {
4947
4947
  type: 'object',
4948
4948
  required: ['interactions'],
4949
4949
  properties: {
4950
+ feed: {
4951
+ type: 'string',
4952
+ format: 'at-uri',
4953
+ },
4950
4954
  interactions: {
4951
4955
  type: 'array',
4952
4956
  items: {
@@ -18,6 +18,7 @@ const id = 'app.bsky.feed.sendInteractions'
18
18
  export type QueryParams = {}
19
19
 
20
20
  export interface InputSchema {
21
+ feed?: string
21
22
  interactions: AppBskyFeedDefs.Interaction[]
22
23
  }
23
24
 
@@ -1,6 +1,5 @@
1
1
  import { HOUR, MINUTE, mapDefined } from '@atproto/common'
2
2
  import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
3
- import { FeatureGateID } from '../feature-gates'
4
3
  import { Actor, ProfileViewerState } from '../hydration/actor'
5
4
  import { FeedItem, Like, Post, Repost } from '../hydration/feed'
6
5
  import { Follow, Verification } from '../hydration/graph'
@@ -504,6 +503,10 @@ export class Views {
504
503
  const actor = state.actors?.get(did)
505
504
  if (!actor) return
506
505
 
506
+ // Currently, the handle comes as "handle.invalid" from the production dataplane.
507
+ // But the contract allows for empty handle, so we cover both cases.
508
+ if (!actor.handle || actor.handle === INVALID_HANDLE) return
509
+
507
510
  const isImpersonation = state.labels?.get(did)?.isImpersonation
508
511
 
509
512
  const verifications: VerificationView[] = actor.verifications.map(
@@ -1416,8 +1419,8 @@ export class Views {
1416
1419
  threadTagsHide: this.threadTagsHide,
1417
1420
  visibilityTagRankPrefix: this.visibilityTagRankPrefix,
1418
1421
  },
1419
- state.ctx?.featureGates.get(
1420
- FeatureGateID.ThreadsReplyRankingExplorationEnable,
1422
+ state.ctx?.featureGatesMap.get(
1423
+ 'threads:reply_ranking_exploration:enable',
1421
1424
  ),
1422
1425
  )
1423
1426
 
@@ -1789,8 +1792,8 @@ export class Views {
1789
1792
  threadTagsHide: this.threadTagsHide,
1790
1793
  visibilityTagRankPrefix: this.visibilityTagRankPrefix,
1791
1794
  },
1792
- state.ctx?.featureGates.get(
1793
- FeatureGateID.ThreadsReplyRankingExplorationEnable,
1795
+ state.ctx?.featureGatesMap.get(
1796
+ 'threads:reply_ranking_exploration:enable',
1794
1797
  ),
1795
1798
  )
1796
1799
  }
@@ -1987,9 +1990,7 @@ export class Views {
1987
1990
 
1988
1991
  let hiddenByTag = false
1989
1992
  if (
1990
- state.ctx?.featureGates.get(
1991
- FeatureGateID.ThreadsReplyRankingExplorationEnable,
1992
- )
1993
+ state.ctx?.featureGatesMap.get('threads:reply_ranking_exploration:enable')
1993
1994
  ) {
1994
1995
  hiddenByTag = authorDid !== opDid && post.tags.has(this.visibilityTagHide)
1995
1996
  } else {
@@ -568,6 +568,8 @@ describe('appview thread views', () => {
568
568
  sc.getHeaders(bob),
569
569
  )
570
570
 
571
+ await network.processAll()
572
+
571
573
  const threadBeforeListTakedown = await agent.app.bsky.feed.getPostThread(
572
574
  { depth: 1, uri: reply.ref.uriStr },
573
575
  {
@@ -17,7 +17,7 @@ describe('verification views', () => {
17
17
  let network: TestNetwork
18
18
  let agent: AtpAgent
19
19
  let labelerDid: string
20
- let sc: SeedClient
20
+ let sc: SeedClient<TestNetwork>
21
21
 
22
22
  // account dids, for convenience
23
23
  let alice: string
@@ -31,6 +31,8 @@ describe('verification views', () => {
31
31
  let verifier1: string
32
32
  let verifier2: string
33
33
  let verifier3: string
34
+ let handleinvalid: string
35
+ let handleempty: string
34
36
 
35
37
  beforeAll(async () => {
36
38
  network = await TestNetwork.create({
@@ -38,24 +40,11 @@ describe('verification views', () => {
38
40
  })
39
41
  agent = network.bsky.getClient()
40
42
  sc = network.getSeedClient()
41
- await verificationsSeed(sc)
42
-
43
- labelerDid = network.bsky.ctx.cfg.modServiceDid
44
- await createLabel({
45
- src: labelerDid,
46
- uri: sc.dids.impersonator,
47
- cid: '',
48
- val: 'impersonation',
49
- })
50
- await createLabel({
51
- src: labelerDid,
52
- uri: sc.dids.verifier3,
53
- cid: '',
54
- val: 'impersonation',
55
- })
56
43
 
44
+ await verificationsSeed(sc)
57
45
  await network.processAll()
58
46
 
47
+ labelerDid = network.bsky.ctx.cfg.modServiceDid
59
48
  alice = sc.dids.alice
60
49
  bob = sc.dids.bob
61
50
  carol = sc.dids.carol
@@ -67,6 +56,8 @@ describe('verification views', () => {
67
56
  verifier1 = sc.dids.verifier1
68
57
  verifier2 = sc.dids.verifier2
69
58
  verifier3 = sc.dids.verifier3
59
+ handleinvalid = sc.dids.handleinvalid
60
+ handleempty = sc.dids.handleempty
70
61
 
71
62
  await network.bsky.db.db
72
63
  .updateTable('actor')
@@ -241,6 +232,18 @@ describe('verification views', () => {
241
232
  `at://${verifier1}/app.bsky.graph.verification/`,
242
233
  ],
243
234
  },
235
+ {
236
+ description:
237
+ 'returns undefined for user with invalid handle even if they have verifications',
238
+ getDid: () => handleinvalid,
239
+ getExpected: () => undefined,
240
+ },
241
+ {
242
+ description:
243
+ 'returns undefined for user with empty handle even if they have verifications',
244
+ getDid: () => handleempty,
245
+ getExpected: () => undefined,
246
+ },
244
247
  ]
245
248
 
246
249
  it.each(testCases)(
@@ -277,25 +280,4 @@ describe('verification views', () => {
277
280
  )
278
281
  return res.data
279
282
  }
280
-
281
- const createLabel = async (opts: {
282
- src?: string
283
- uri: string
284
- cid: string
285
- val: string
286
- exp?: string
287
- }) => {
288
- await network.bsky.db.db
289
- .insertInto('label')
290
- .values({
291
- uri: opts.uri,
292
- cid: opts.cid,
293
- val: opts.val,
294
- cts: new Date().toISOString(),
295
- exp: opts.exp ?? null,
296
- neg: false,
297
- src: opts.src ?? labelerDid,
298
- })
299
- .execute()
300
- }
301
283
  })