@atproto/bsky 0.0.124 → 0.0.126

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 (117) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/api/app/bsky/notification/listNotifications.d.ts +7 -0
  3. package/dist/api/app/bsky/notification/listNotifications.d.ts.map +1 -1
  4. package/dist/api/app/bsky/notification/listNotifications.js +21 -5
  5. package/dist/api/app/bsky/notification/listNotifications.js.map +1 -1
  6. package/dist/config.d.ts +6 -0
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +24 -15
  9. package/dist/config.js.map +1 -1
  10. package/dist/context.d.ts +6 -1
  11. package/dist/context.d.ts.map +1 -1
  12. package/dist/context.js +6 -0
  13. package/dist/context.js.map +1 -1
  14. package/dist/data-plane/client/hosts.d.ts +37 -0
  15. package/dist/data-plane/client/hosts.d.ts.map +1 -0
  16. package/dist/data-plane/client/hosts.js +106 -0
  17. package/dist/data-plane/client/hosts.js.map +1 -0
  18. package/dist/data-plane/client/index.d.ts +13 -0
  19. package/dist/data-plane/client/index.d.ts.map +1 -0
  20. package/dist/data-plane/client/index.js +133 -0
  21. package/dist/data-plane/client/index.js.map +1 -0
  22. package/dist/data-plane/{client.d.ts → client/util.d.ts} +3 -10
  23. package/dist/data-plane/client/util.d.ts.map +1 -0
  24. package/dist/data-plane/client/util.js +85 -0
  25. package/dist/data-plane/client/util.js.map +1 -0
  26. package/dist/data-plane/server/db/pagination.d.ts +69 -9
  27. package/dist/data-plane/server/db/pagination.d.ts.map +1 -1
  28. package/dist/data-plane/server/db/pagination.js +114 -14
  29. package/dist/data-plane/server/db/pagination.js.map +1 -1
  30. package/dist/data-plane/server/routes/notifs.d.ts.map +1 -1
  31. package/dist/data-plane/server/routes/notifs.js +3 -5
  32. package/dist/data-plane/server/routes/notifs.js.map +1 -1
  33. package/dist/data-plane/server/subscription.d.ts.map +1 -1
  34. package/dist/data-plane/server/subscription.js +6 -0
  35. package/dist/data-plane/server/subscription.js.map +1 -1
  36. package/dist/etcd.d.ts +25 -0
  37. package/dist/etcd.d.ts.map +1 -0
  38. package/dist/etcd.js +109 -0
  39. package/dist/etcd.js.map +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +14 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/lexicon/index.d.ts +6 -0
  44. package/dist/lexicon/index.d.ts.map +1 -1
  45. package/dist/lexicon/index.js +12 -0
  46. package/dist/lexicon/index.js.map +1 -1
  47. package/dist/lexicon/lexicons.d.ts +304 -156
  48. package/dist/lexicon/lexicons.d.ts.map +1 -1
  49. package/dist/lexicon/lexicons.js +168 -80
  50. package/dist/lexicon/lexicons.js.map +1 -1
  51. package/dist/lexicon/types/app/bsky/embed/video.d.ts +1 -0
  52. package/dist/lexicon/types/app/bsky/embed/video.d.ts.map +1 -1
  53. package/dist/lexicon/types/app/bsky/embed/video.js.map +1 -1
  54. package/dist/lexicon/types/com/atproto/identity/defs.d.ts +17 -0
  55. package/dist/lexicon/types/com/atproto/identity/defs.d.ts.map +1 -0
  56. package/dist/lexicon/types/com/atproto/identity/defs.js +16 -0
  57. package/dist/lexicon/types/com/atproto/identity/defs.js.map +1 -0
  58. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts +39 -0
  59. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.d.ts.map +1 -0
  60. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js +7 -0
  61. package/dist/lexicon/types/com/atproto/identity/refreshIdentity.js.map +1 -0
  62. package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts +40 -0
  63. package/dist/lexicon/types/com/atproto/identity/resolveDid.d.ts.map +1 -0
  64. package/dist/lexicon/types/com/atproto/identity/resolveDid.js +7 -0
  65. package/dist/lexicon/types/com/atproto/identity/resolveDid.js.map +1 -0
  66. package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts +1 -0
  67. package/dist/lexicon/types/com/atproto/identity/resolveHandle.d.ts.map +1 -1
  68. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts +36 -0
  69. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.d.ts.map +1 -0
  70. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js +7 -0
  71. package/dist/lexicon/types/com/atproto/identity/resolveIdentity.js.map +1 -0
  72. package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts +0 -4
  73. package/dist/lexicon/types/com/atproto/repo/listRecords.d.ts.map +1 -1
  74. package/dist/lexicon/types/com/atproto/repo/listRecords.js.map +1 -1
  75. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts +0 -2
  76. package/dist/lexicon/types/com/atproto/sync/getRecord.d.ts.map +1 -1
  77. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts +1 -30
  78. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.d.ts.map +1 -1
  79. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js +0 -27
  80. package/dist/lexicon/types/com/atproto/sync/subscribeRepos.js.map +1 -1
  81. package/dist/logger.d.ts +1 -0
  82. package/dist/logger.d.ts.map +1 -1
  83. package/dist/logger.js +2 -1
  84. package/dist/logger.js.map +1 -1
  85. package/package.json +16 -15
  86. package/src/api/app/bsky/notification/listNotifications.ts +28 -6
  87. package/src/config.ts +45 -15
  88. package/src/context.ts +12 -1
  89. package/src/data-plane/client/hosts.ts +103 -0
  90. package/src/data-plane/client/index.ts +123 -0
  91. package/src/data-plane/client/util.ts +66 -0
  92. package/src/data-plane/server/db/pagination.ts +158 -35
  93. package/src/data-plane/server/routes/notifs.ts +4 -9
  94. package/src/data-plane/server/subscription.ts +7 -2
  95. package/src/etcd.ts +90 -0
  96. package/src/index.ts +26 -2
  97. package/src/lexicon/index.ts +36 -0
  98. package/src/lexicon/lexicons.ts +183 -83
  99. package/src/lexicon/types/app/bsky/embed/video.ts +1 -0
  100. package/src/lexicon/types/com/atproto/identity/defs.ts +30 -0
  101. package/src/lexicon/types/com/atproto/identity/refreshIdentity.ts +52 -0
  102. package/src/lexicon/types/com/atproto/identity/resolveDid.ts +52 -0
  103. package/src/lexicon/types/com/atproto/identity/resolveHandle.ts +1 -0
  104. package/src/lexicon/types/com/atproto/identity/resolveIdentity.ts +48 -0
  105. package/src/lexicon/types/com/atproto/repo/listRecords.ts +0 -4
  106. package/src/lexicon/types/com/atproto/sync/getRecord.ts +0 -2
  107. package/src/lexicon/types/com/atproto/sync/subscribeRepos.ts +0 -59
  108. package/src/logger.ts +2 -0
  109. package/tests/etcd.test.ts +301 -0
  110. package/tests/views/__snapshots__/notifications.test.ts.snap +3 -3
  111. package/tests/views/notifications.test.ts +190 -10
  112. package/tsconfig.build.tsbuildinfo +1 -1
  113. package/tsconfig.tests.tsbuildinfo +1 -1
  114. package/dist/data-plane/client.d.ts.map +0 -1
  115. package/dist/data-plane/client.js +0 -156
  116. package/dist/data-plane/client.js.map +0 -1
  117. package/src/data-plane/client.ts +0 -154
package/src/config.ts CHANGED
@@ -10,7 +10,9 @@ export interface ServerConfigValues {
10
10
  alternateAudienceDids: string[]
11
11
  entrywayJwtPublicKeyHex?: string
12
12
  // external services
13
+ etcdHosts: string[]
13
14
  dataplaneUrls: string[]
15
+ dataplaneUrlsEtcdKeyPrefix?: string
14
16
  dataplaneHttpVersion?: '1.1' | '2'
15
17
  dataplaneIgnoreBadTls?: boolean
16
18
  bsyncUrl: string
@@ -47,6 +49,8 @@ export interface ServerConfigValues {
47
49
  bigThreadUris: Set<string>
48
50
  bigThreadDepth?: number
49
51
  maxThreadDepth?: number
52
+ // notifications
53
+ notificationsDelayMs?: number
50
54
  // client config
51
55
  clientCheckEmailConfirmed?: boolean
52
56
  topicsEnabled?: boolean
@@ -74,15 +78,15 @@ export class ServerConfig {
74
78
  const envPort = parseInt(process.env.BSKY_PORT || '', 10)
75
79
  const port = isNaN(envPort) ? 2584 : envPort
76
80
  const didPlcUrl = process.env.BSKY_DID_PLC_URL || 'http://localhost:2582'
77
- const alternateAudienceDids = process.env.BSKY_ALT_AUDIENCE_DIDS
78
- ? process.env.BSKY_ALT_AUDIENCE_DIDS.split(',')
79
- : []
81
+ const alternateAudienceDids = envList(process.env.BSKY_ALT_AUDIENCE_DIDS)
80
82
  const entrywayJwtPublicKeyHex =
81
83
  process.env.BSKY_ENTRYWAY_JWT_PUBLIC_KEY_HEX || undefined
82
- const handleResolveNameservers = process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS
83
- ? process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS.split(',')
84
- : []
84
+ const handleResolveNameservers = envList(
85
+ process.env.BSKY_HANDLE_RESOLVE_NAMESERVERS,
86
+ )
85
87
  const cdnUrl = process.env.BSKY_CDN_URL || process.env.BSKY_IMG_URI_ENDPOINT
88
+ const etcdHosts =
89
+ overrides?.etcdHosts ?? envList(process.env.BSKY_ETCD_HOSTS)
86
90
  // e.g. https://video.invalid/watch/%s/%s/playlist.m3u8
87
91
  const videoPlaylistUrlPattern = process.env.BSKY_VIDEO_PLAYLIST_URL_PATTERN
88
92
  // e.g. https://video.invalid/watch/%s/%s/thumbnail.jpg
@@ -97,16 +101,25 @@ export class ServerConfig {
97
101
  const suggestionsApiKey = process.env.BSKY_SUGGESTIONS_API_KEY || undefined
98
102
  const topicsUrl = process.env.BSKY_TOPICS_URL || undefined
99
103
  const topicsApiKey = process.env.BSKY_TOPICS_API_KEY
100
- let dataplaneUrls = overrides?.dataplaneUrls
101
- dataplaneUrls ??= process.env.BSKY_DATAPLANE_URLS
102
- ? process.env.BSKY_DATAPLANE_URLS.split(',')
103
- : []
104
+ const dataplaneUrls =
105
+ overrides?.dataplaneUrls ?? envList(process.env.BSKY_DATAPLANE_URLS)
106
+ const dataplaneUrlsEtcdKeyPrefix =
107
+ process.env.BSKY_DATAPLANE_URLS_ETCD_KEY_PREFIX || undefined
104
108
  const dataplaneHttpVersion = process.env.BSKY_DATAPLANE_HTTP_VERSION || '2'
105
109
  const dataplaneIgnoreBadTls =
106
110
  process.env.BSKY_DATAPLANE_IGNORE_BAD_TLS === 'true'
107
- const labelsFromIssuerDids = process.env.BSKY_LABELS_FROM_ISSUER_DIDS
108
- ? process.env.BSKY_LABELS_FROM_ISSUER_DIDS.split(',')
109
- : []
111
+ assert(
112
+ !dataplaneUrlsEtcdKeyPrefix || etcdHosts.length,
113
+ 'etcd prefix for dataplane urls may only be configured when there are etcd hosts',
114
+ )
115
+ assert(
116
+ dataplaneUrls.length || dataplaneUrlsEtcdKeyPrefix,
117
+ 'dataplane urls are not configured directly nor with etcd',
118
+ )
119
+ assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2')
120
+ const labelsFromIssuerDids = envList(
121
+ process.env.BSKY_LABELS_FROM_ISSUER_DIDS,
122
+ )
110
123
  const bsyncUrl = process.env.BSKY_BSYNC_URL || undefined
111
124
  assert(bsyncUrl)
112
125
  const bsyncApiKey = process.env.BSKY_BSYNC_API_KEY || undefined
@@ -133,8 +146,6 @@ export class ServerConfig {
133
146
  )
134
147
  const modServiceDid = process.env.MOD_SERVICE_DID
135
148
  assert(modServiceDid)
136
- assert(dataplaneUrls.length)
137
- assert(dataplaneHttpVersion === '1.1' || dataplaneHttpVersion === '2')
138
149
  const statsigKey =
139
150
  process.env.NODE_ENV === 'test'
140
151
  ? 'secret-key'
@@ -161,6 +172,10 @@ export class ServerConfig {
161
172
  ? parseInt(process.env.BSKY_MAX_THREAD_DEPTH || '', 10)
162
173
  : undefined
163
174
 
175
+ const notificationsDelayMs = process.env.BSKY_NOTIFICATIONS_DELAY_MS
176
+ ? parseInt(process.env.BSKY_NOTIFICATIONS_DELAY_MS || '', 10)
177
+ : 0
178
+
164
179
  const disableSsrfProtection = process.env.BSKY_DISABLE_SSRF_PROTECTION
165
180
  ? process.env.BSKY_DISABLE_SSRF_PROTECTION === 'true'
166
181
  : debugMode
@@ -185,7 +200,9 @@ export class ServerConfig {
185
200
  serverDid,
186
201
  alternateAudienceDids,
187
202
  entrywayJwtPublicKeyHex,
203
+ etcdHosts,
188
204
  dataplaneUrls,
205
+ dataplaneUrlsEtcdKeyPrefix,
189
206
  dataplaneHttpVersion,
190
207
  dataplaneIgnoreBadTls,
191
208
  searchUrl,
@@ -220,6 +237,7 @@ export class ServerConfig {
220
237
  bigThreadUris,
221
238
  bigThreadDepth,
222
239
  maxThreadDepth,
240
+ notificationsDelayMs,
223
241
  disableSsrfProtection,
224
242
  proxyAllowHTTP2,
225
243
  proxyHeadersTimeout,
@@ -267,6 +285,14 @@ export class ServerConfig {
267
285
  return this.cfg.entrywayJwtPublicKeyHex
268
286
  }
269
287
 
288
+ get etcdHosts() {
289
+ return this.cfg.etcdHosts
290
+ }
291
+
292
+ get dataplaneUrlsEtcdKeyPrefix() {
293
+ return this.cfg.dataplaneUrlsEtcdKeyPrefix
294
+ }
295
+
270
296
  get dataplaneUrls() {
271
297
  return this.cfg.dataplaneUrls
272
298
  }
@@ -407,6 +433,10 @@ export class ServerConfig {
407
433
  return this.cfg.maxThreadDepth
408
434
  }
409
435
 
436
+ get notificationsDelayMs() {
437
+ return this.cfg.notificationsDelayMs ?? 0
438
+ }
439
+
410
440
  get disableSsrfProtection(): boolean {
411
441
  return this.cfg.disableSsrfProtection ?? false
412
442
  }
package/src/context.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as plc from '@did-plc/lib'
2
+ import { Etcd3 } from 'etcd3'
2
3
  import express from 'express'
3
4
  import { Dispatcher } from 'undici'
4
5
  import { AtpAgent } from '@atproto/api'
@@ -8,7 +9,7 @@ import { AuthVerifier } from './auth-verifier'
8
9
  import { BsyncClient } from './bsync'
9
10
  import { ServerConfig } from './config'
10
11
  import { CourierClient } from './courier'
11
- import { DataPlaneClient } from './data-plane/client'
12
+ import { DataPlaneClient, HostList } from './data-plane/client'
12
13
  import { FeatureGates } from './feature-gates'
13
14
  import { Hydrator } from './hydration/hydrator'
14
15
  import { httpLogger as log } from './logger'
@@ -23,7 +24,9 @@ export class AppContext {
23
24
  constructor(
24
25
  private opts: {
25
26
  cfg: ServerConfig
27
+ etcd: Etcd3 | undefined
26
28
  dataplane: DataPlaneClient
29
+ dataplaneHostList: HostList
27
30
  searchAgent: AtpAgent | undefined
28
31
  suggestionsAgent: AtpAgent | undefined
29
32
  topicsAgent: AtpAgent | undefined
@@ -43,10 +46,18 @@ export class AppContext {
43
46
  return this.opts.cfg
44
47
  }
45
48
 
49
+ get etcd() {
50
+ return this.opts.etcd
51
+ }
52
+
46
53
  get dataplane(): DataPlaneClient {
47
54
  return this.opts.dataplane
48
55
  }
49
56
 
57
+ get dataplaneHostList(): HostList {
58
+ return this.opts.dataplaneHostList
59
+ }
60
+
50
61
  get searchAgent(): AtpAgent | undefined {
51
62
  return this.opts.searchAgent
52
63
  }
@@ -0,0 +1,103 @@
1
+ import { Etcd3 } from 'etcd3'
2
+ import { EtcdMap } from '../../etcd'
3
+ import { dataplaneLogger as logger } from '../../logger'
4
+
5
+ /**
6
+ * Interface for a reactive list of hosts, i.e. for use with the dataplane client.
7
+ */
8
+ export interface HostList {
9
+ get: () => Iterable<string>
10
+ onUpdate(handler: HostListHandler): void
11
+ }
12
+
13
+ type HostListHandler = (hosts: Iterable<string>) => void
14
+
15
+ /**
16
+ * Maintains a reactive HostList based on a simple setter.
17
+ */
18
+ export class BasicHostList implements HostList {
19
+ private hosts: Iterable<string>
20
+ private handlers: HostListHandler[] = []
21
+
22
+ constructor(hosts: Iterable<string>) {
23
+ this.hosts = hosts
24
+ }
25
+
26
+ get() {
27
+ return this.hosts
28
+ }
29
+
30
+ set(hosts: Iterable<string>) {
31
+ this.hosts = hosts
32
+ this.update()
33
+ }
34
+
35
+ private update() {
36
+ for (const handler of this.handlers) {
37
+ handler(this.hosts)
38
+ }
39
+ }
40
+
41
+ onUpdate(handler: HostListHandler) {
42
+ this.handlers.push(handler)
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Maintains a reactive HostList based on etcd key values under a given key prefix.
48
+ * When fallback is provided, ensures that this fallback is used whenever no hosts are available.
49
+ */
50
+ export class EtcdHostList implements HostList {
51
+ private kv: EtcdMap
52
+ private inner = new BasicHostList(new Set())
53
+ private fallback: Set<string>
54
+
55
+ constructor(etcd: Etcd3, prefix: string, fallback?: string[]) {
56
+ this.fallback = new Set(fallback)
57
+ this.kv = new EtcdMap(etcd, prefix)
58
+ this.update() // init fallback if necessary
59
+ this.kv.watcher.on('connected', (res) => {
60
+ logger.warn(
61
+ { watcherId: this.kv.watcher.id, header: res.header },
62
+ 'EtcdHostList connected',
63
+ )
64
+ })
65
+ this.kv.watcher.on('disconnected', (err) => {
66
+ logger.warn(
67
+ { watcherId: this.kv.watcher.id, err },
68
+ 'EtcdHostList disconnected',
69
+ )
70
+ })
71
+ this.kv.watcher.on('error', (err) => {
72
+ logger.error({ watcherId: this.kv.watcher.id, err }, 'EtcdHostList error')
73
+ })
74
+ }
75
+
76
+ async connect() {
77
+ await this.kv.connect()
78
+ this.update()
79
+ this.kv.onUpdate(() => this.update())
80
+ }
81
+
82
+ get() {
83
+ return this.inner.get()
84
+ }
85
+
86
+ private update() {
87
+ const hosts = new Set<string>()
88
+ for (const host of this.kv.values()) {
89
+ if (URL.canParse(host)) {
90
+ hosts.add(host)
91
+ }
92
+ }
93
+ if (hosts.size) {
94
+ this.inner.set(hosts)
95
+ } else if (this.fallback.size) {
96
+ this.inner.set(this.fallback)
97
+ }
98
+ }
99
+
100
+ onUpdate(handler: HostListHandler) {
101
+ this.inner.onUpdate(handler)
102
+ }
103
+ }
@@ -0,0 +1,123 @@
1
+ import assert from 'node:assert'
2
+ import { randomInt } from 'node:crypto'
3
+ import {
4
+ Code,
5
+ ConnectError,
6
+ PromiseClient,
7
+ createPromiseClient,
8
+ makeAnyClient,
9
+ } from '@connectrpc/connect'
10
+ import { createGrpcTransport } from '@connectrpc/connect-node'
11
+ import { Service } from '../../proto/bsky_connect'
12
+ import { HostList } from './hosts'
13
+
14
+ export * from './hosts'
15
+ export * from './util'
16
+
17
+ export type DataPlaneClient = PromiseClient<typeof Service>
18
+ type HttpVersion = '1.1' | '2'
19
+ const MAX_RETRIES = 3
20
+
21
+ export const createDataPlaneClient = (
22
+ hostList: HostList,
23
+ opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean },
24
+ ) => {
25
+ const clients = new DataPlaneClients(hostList, opts)
26
+ return makeAnyClient(Service, (method) => {
27
+ return async (...args) => {
28
+ let tries = 0
29
+ let error: unknown
30
+ let remainingClients = clients.get()
31
+ while (tries < MAX_RETRIES) {
32
+ const client = randomElement(remainingClients)
33
+ assert(client, 'no clients available')
34
+ try {
35
+ return await client[method.localName](...args)
36
+ } catch (err) {
37
+ if (
38
+ err instanceof ConnectError &&
39
+ (err.code === Code.Unavailable || err.code === Code.Aborted)
40
+ ) {
41
+ tries++
42
+ error = err
43
+ remainingClients = getRemainingClients(remainingClients, client)
44
+ } else {
45
+ throw err
46
+ }
47
+ }
48
+ }
49
+ assert(error)
50
+ throw error
51
+ }
52
+ }) as DataPlaneClient
53
+ }
54
+
55
+ export { Code }
56
+
57
+ /**
58
+ * Uses a reactive HostList in order to maintain a pool of DataPlaneClients.
59
+ * Each DataPlaneClient is cached per host so that it maintains connections
60
+ * and other internal state when the underlying HostList is updated.
61
+ */
62
+ class DataPlaneClients {
63
+ private clients: DataPlaneClient[] = []
64
+ private clientsByHost = new Map<string, DataPlaneClient>()
65
+
66
+ constructor(
67
+ private hostList: HostList,
68
+ private clientOpts: {
69
+ httpVersion?: HttpVersion
70
+ rejectUnauthorized?: boolean
71
+ },
72
+ ) {
73
+ this.refresh()
74
+ this.hostList.onUpdate(() => this.refresh())
75
+ }
76
+
77
+ get(): readonly DataPlaneClient[] {
78
+ return this.clients
79
+ }
80
+
81
+ private refresh() {
82
+ this.clients = []
83
+ for (const host of this.hostList.get()) {
84
+ let client = this.clientsByHost.get(host)
85
+ if (!client) {
86
+ client = this.createClient(host)
87
+ this.clientsByHost.set(host, client)
88
+ }
89
+ this.clients.push(client)
90
+ }
91
+ }
92
+
93
+ private createClient(host: string) {
94
+ return createBaseClient(host, this.clientOpts)
95
+ }
96
+ }
97
+
98
+ const createBaseClient = (
99
+ baseUrl: string,
100
+ opts: { httpVersion?: HttpVersion; rejectUnauthorized?: boolean },
101
+ ): DataPlaneClient => {
102
+ const { httpVersion = '2', rejectUnauthorized = true } = opts
103
+ const transport = createGrpcTransport({
104
+ baseUrl,
105
+ httpVersion,
106
+ acceptCompression: [],
107
+ nodeOptions: { rejectUnauthorized },
108
+ })
109
+ return createPromiseClient(Service, transport)
110
+ }
111
+
112
+ const getRemainingClients = (
113
+ clients: readonly DataPlaneClient[],
114
+ lastClient: DataPlaneClient,
115
+ ) => {
116
+ if (clients.length < 2) return clients // no clients to choose from
117
+ return clients.filter((c) => c !== lastClient)
118
+ }
119
+
120
+ const randomElement = <T>(arr: readonly T[]): T | undefined => {
121
+ if (arr.length === 0) return
122
+ return arr[randomInt(arr.length)]
123
+ }
@@ -0,0 +1,66 @@
1
+ import { Code, ConnectError } from '@connectrpc/connect'
2
+ import * as ui8 from 'uint8arrays'
3
+ import { getDidKeyFromMultibase } from '@atproto/identity'
4
+
5
+ export const isDataplaneError = (
6
+ err: unknown,
7
+ code?: Code,
8
+ ): err is ConnectError => {
9
+ if (err instanceof ConnectError) {
10
+ return !code || err.code === code
11
+ }
12
+ return false
13
+ }
14
+
15
+ export const unpackIdentityServices = (servicesBytes: Uint8Array) => {
16
+ const servicesStr = ui8.toString(servicesBytes, 'utf8')
17
+ if (!servicesStr) return {}
18
+ return JSON.parse(servicesStr) as UnpackedServices
19
+ }
20
+
21
+ export const unpackIdentityKeys = (keysBytes: Uint8Array) => {
22
+ const keysStr = ui8.toString(keysBytes, 'utf8')
23
+ if (!keysStr) return {}
24
+ return JSON.parse(keysStr) as UnpackedKeys
25
+ }
26
+
27
+ export const getServiceEndpoint = (
28
+ services: UnpackedServices,
29
+ opts: { id: string; type: string },
30
+ ) => {
31
+ const endpoint =
32
+ services[opts.id] &&
33
+ services[opts.id].Type === opts.type &&
34
+ validateUrl(services[opts.id].URL)
35
+ return endpoint || undefined
36
+ }
37
+
38
+ export const getKeyAsDidKey = (keys: UnpackedKeys, opts: { id: string }) => {
39
+ const key =
40
+ keys[opts.id] &&
41
+ getDidKeyFromMultibase({
42
+ type: keys[opts.id].Type,
43
+ publicKeyMultibase: keys[opts.id].PublicKeyMultibase,
44
+ })
45
+ return key || undefined
46
+ }
47
+
48
+ type UnpackedServices = Record<string, { Type: string; URL: string }>
49
+
50
+ type UnpackedKeys = Record<string, { Type: string; PublicKeyMultibase: string }>
51
+
52
+ const validateUrl = (urlStr: string): string | undefined => {
53
+ let url
54
+ try {
55
+ url = new URL(urlStr)
56
+ } catch {
57
+ return undefined
58
+ }
59
+ if (!['http:', 'https:'].includes(url.protocol)) {
60
+ return undefined
61
+ } else if (!url.hostname) {
62
+ return undefined
63
+ } else {
64
+ return urlStr
65
+ }
66
+ }