@atproto/ozone 0.1.63 → 0.1.65

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 (52) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/api/moderation/queryEvents.d.ts.map +1 -1
  3. package/dist/api/moderation/queryEvents.js +2 -1
  4. package/dist/api/moderation/queryEvents.js.map +1 -1
  5. package/dist/daemon/blob-diverter.d.ts +26 -11
  6. package/dist/daemon/blob-diverter.d.ts.map +1 -1
  7. package/dist/daemon/blob-diverter.js +109 -52
  8. package/dist/daemon/blob-diverter.js.map +1 -1
  9. package/dist/lexicon/lexicons.d.ts +30 -0
  10. package/dist/lexicon/lexicons.d.ts.map +1 -1
  11. package/dist/lexicon/lexicons.js +15 -0
  12. package/dist/lexicon/lexicons.js.map +1 -1
  13. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +2 -0
  14. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  15. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  16. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +1 -0
  17. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  18. package/dist/mod-service/index.d.ts +1 -0
  19. package/dist/mod-service/index.d.ts.map +1 -1
  20. package/dist/mod-service/index.js +12 -1
  21. package/dist/mod-service/index.js.map +1 -1
  22. package/dist/mod-service/views.d.ts.map +1 -1
  23. package/dist/mod-service/views.js +8 -0
  24. package/dist/mod-service/views.js.map +1 -1
  25. package/dist/setting/constants.d.ts +1 -0
  26. package/dist/setting/constants.d.ts.map +1 -1
  27. package/dist/setting/constants.js +2 -1
  28. package/dist/setting/constants.js.map +1 -1
  29. package/dist/setting/validators.d.ts.map +1 -1
  30. package/dist/setting/validators.js +19 -0
  31. package/dist/setting/validators.js.map +1 -1
  32. package/dist/util.d.ts +2 -3
  33. package/dist/util.d.ts.map +1 -1
  34. package/dist/util.js +9 -20
  35. package/dist/util.js.map +1 -1
  36. package/package.json +10 -11
  37. package/src/api/moderation/queryEvents.ts +2 -0
  38. package/src/daemon/blob-diverter.ts +137 -102
  39. package/src/lexicon/lexicons.ts +17 -0
  40. package/src/lexicon/types/tools/ozone/moderation/defs.ts +2 -0
  41. package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +1 -0
  42. package/src/mod-service/index.ts +14 -0
  43. package/src/mod-service/views.ts +11 -0
  44. package/src/setting/constants.ts +1 -0
  45. package/src/setting/validators.ts +28 -1
  46. package/src/util.ts +8 -20
  47. package/tests/_util.ts +46 -0
  48. package/tests/blob-divert.test.ts +30 -19
  49. package/tests/moderation.test.ts +3 -1
  50. package/tests/server.test.ts +23 -35
  51. package/tests/takedown.test.ts +64 -0
  52. package/tsconfig.tests.tsbuildinfo +1 -1
package/dist/util.js CHANGED
@@ -1,12 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.getSigningKeyId = void 0;
4
- exports.retryHttp = retryHttp;
5
- exports.retryableHttp = retryableHttp;
6
- const axios_1 = require("axios");
7
- const structured_headers_1 = require("structured-headers");
8
- const xrpc_1 = require("@atproto/xrpc");
3
+ exports.formatLabelerHeader = exports.defaultLabelerHeader = exports.parseLabelerHeader = exports.LABELER_HEADER_NAME = exports.retryHttp = exports.RETRYABLE_HTTP_STATUS_CODES = exports.getSigningKeyId = void 0;
9
4
  const common_1 = require("@atproto/common");
5
+ const xrpc_1 = require("@atproto/xrpc");
6
+ const structured_headers_1 = require("structured-headers");
10
7
  const getSigningKeyId = async (db, signingKey) => {
11
8
  const selectRes = await db.db
12
9
  .selectFrom('signing_key')
@@ -24,25 +21,17 @@ const getSigningKeyId = async (db, signingKey) => {
24
21
  return insertRes.id;
25
22
  };
26
23
  exports.getSigningKeyId = getSigningKeyId;
27
- async function retryHttp(fn, opts = {}) {
28
- return (0, common_1.retry)(fn, { retryable: retryableHttp, ...opts });
29
- }
30
- function retryableHttp(err) {
24
+ exports.RETRYABLE_HTTP_STATUS_CODES = new Set([
25
+ 408, 425, 429, 500, 502, 503, 504, 522, 524,
26
+ ]);
27
+ exports.retryHttp = (0, common_1.createRetryable)((err) => {
31
28
  if (err instanceof xrpc_1.XRPCError) {
32
29
  if (err.status === xrpc_1.ResponseType.Unknown)
33
30
  return true;
34
- return retryableHttpStatusCodes.has(err.status);
35
- }
36
- if (err instanceof axios_1.AxiosError) {
37
- if (!err.response)
38
- return true;
39
- return retryableHttpStatusCodes.has(err.response.status);
31
+ return exports.RETRYABLE_HTTP_STATUS_CODES.has(err.status);
40
32
  }
41
33
  return false;
42
- }
43
- const retryableHttpStatusCodes = new Set([
44
- 408, 425, 429, 500, 502, 503, 504, 522, 524,
45
- ]);
34
+ });
46
35
  exports.LABELER_HEADER_NAME = 'atproto-accept-labelers';
47
36
  const parseLabelerHeader = (header, ignoreDid) => {
48
37
  if (!header)
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ozone",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "license": "MIT",
5
5
  "description": "Backend service for moderating the Bluesky network.",
6
6
  "keywords": [
@@ -18,7 +18,6 @@
18
18
  "bin": "dist/bin.js",
19
19
  "dependencies": {
20
20
  "@did-plc/lib": "^0.0.1",
21
- "axios": "^1.6.7",
22
21
  "compression": "^1.7.4",
23
22
  "cors": "^2.8.5",
24
23
  "express": "^4.17.2",
@@ -32,14 +31,15 @@
32
31
  "structured-headers": "^1.0.1",
33
32
  "typed-emitter": "^2.1.0",
34
33
  "uint8arrays": "3.0.0",
35
- "@atproto/api": "^0.13.24",
36
- "@atproto/common": "^0.4.5",
34
+ "undici": "^6.14.1",
35
+ "@atproto/api": "^0.13.26",
36
+ "@atproto/common": "^0.4.6",
37
37
  "@atproto/crypto": "^0.4.2",
38
- "@atproto/identity": "^0.4.3",
39
- "@atproto/lexicon": "^0.4.4",
38
+ "@atproto/identity": "^0.4.4",
39
+ "@atproto/lexicon": "^0.4.5",
40
40
  "@atproto/syntax": "^0.3.1",
41
- "@atproto/xrpc": "^0.6.5",
42
- "@atproto/xrpc-server": "^0.7.4"
41
+ "@atproto/xrpc": "^0.6.6",
42
+ "@atproto/xrpc-server": "^0.7.5"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@did-plc/server": "^0.0.1",
@@ -48,12 +48,11 @@
48
48
  "@types/express-serve-static-core": "^4.17.36",
49
49
  "@types/pg": "^8.6.6",
50
50
  "@types/qs": "^6.9.7",
51
- "axios": "^0.27.2",
52
51
  "jest": "^28.1.2",
53
52
  "ts-node": "^10.8.2",
54
53
  "typescript": "^5.6.3",
55
- "@atproto/lex-cli": "^0.5.4",
56
- "@atproto/pds": "^0.4.80"
54
+ "@atproto/lex-cli": "^0.5.5",
55
+ "@atproto/pds": "^0.4.82"
57
56
  },
58
57
  "scripts": {
59
58
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
@@ -25,6 +25,7 @@ export default function (server: Server, ctx: AppContext) {
25
25
  reportTypes,
26
26
  collections = [],
27
27
  subjectType,
28
+ policies,
28
29
  } = params
29
30
  const db = ctx.db
30
31
  const modService = ctx.modService(db)
@@ -47,6 +48,7 @@ export default function (server: Server, ctx: AppContext) {
47
48
  reportTypes,
48
49
  collections,
49
50
  subjectType,
51
+ policies,
50
52
  })
51
53
  return {
52
54
  encoding: 'application/json',
@@ -1,16 +1,19 @@
1
1
  import {
2
- VerifyCidTransform,
3
- forwardStreamErrors,
2
+ createDecoders,
4
3
  getPdsEndpoint,
4
+ VerifyCidTransform,
5
+ allFulfilled,
5
6
  } from '@atproto/common'
6
7
  import { IdResolver } from '@atproto/identity'
7
- import axios from 'axios'
8
- import { Readable } from 'stream'
8
+ import { ResponseType, XRPCError } from '@atproto/xrpc'
9
9
  import { CID } from 'multiformats/cid'
10
+ import { Readable } from 'node:stream'
11
+ import { finished, pipeline } from 'node:stream/promises'
12
+ import * as undici from 'undici'
10
13
 
14
+ import { BlobDivertConfig } from '../config'
11
15
  import Database from '../db'
12
16
  import { retryHttp } from '../util'
13
- import { BlobDivertConfig } from '../config'
14
17
 
15
18
  export class BlobDiverter {
16
19
  serviceConfig: BlobDivertConfig
@@ -27,126 +30,158 @@ export class BlobDiverter {
27
30
  this.idResolver = services.idResolver
28
31
  }
29
32
 
30
- private async getBlob({
31
- pds,
32
- did,
33
- cid,
34
- }: {
35
- pds: string
36
- did: string
37
- cid: string
38
- }) {
39
- const blobResponse = await axios.get(
40
- `${pds}/xrpc/com.atproto.sync.getBlob`,
41
- {
42
- params: { did, cid },
43
- decompress: true,
44
- responseType: 'stream',
45
- timeout: 5000, // 5sec of inactivity on the connection
46
- },
47
- )
48
- const imageStream: Readable = blobResponse.data
49
- const verifyCid = new VerifyCidTransform(CID.parse(cid))
50
- forwardStreamErrors(imageStream, verifyCid)
51
-
52
- return {
53
- contentType:
54
- blobResponse.headers['content-type'] || 'application/octet-stream',
55
- imageStream: imageStream.pipe(verifyCid),
33
+ /**
34
+ * @throws {XRPCError} so that retryHttp can handle retries
35
+ */
36
+ async getBlob(options: GetBlobOptions): Promise<Blob> {
37
+ const blobUrl = getBlobUrl(options)
38
+
39
+ const blobResponse = await undici
40
+ .request(blobUrl, {
41
+ headersTimeout: 10e3,
42
+ bodyTimeout: 30e3,
43
+ })
44
+ .catch((err) => {
45
+ throw asXrpcClientError(err, `Error fetching blob ${options.cid}`)
46
+ })
47
+
48
+ if (blobResponse.statusCode !== 200) {
49
+ blobResponse.body.destroy()
50
+ throw new XRPCError(
51
+ blobResponse.statusCode,
52
+ undefined,
53
+ `Error downloading blob ${options.cid}`,
54
+ )
56
55
  }
57
- }
58
-
59
- async sendImage({
60
- url,
61
- imageStream,
62
- contentType,
63
- }: {
64
- url: string
65
- imageStream: Readable
66
- contentType: string
67
- }) {
68
- const result = await axios(url, {
69
- method: 'POST',
70
- data: imageStream,
71
- headers: {
72
- Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
73
- 'Content-Type': contentType,
74
- },
75
- })
76
56
 
77
- return result.status === 200
57
+ try {
58
+ const type = blobResponse.headers['content-type']
59
+ const encoding = blobResponse.headers['content-encoding']
60
+
61
+ const verifier = new VerifyCidTransform(CID.parse(options.cid))
62
+
63
+ void pipeline([
64
+ blobResponse.body,
65
+ ...createDecoders(encoding),
66
+ verifier,
67
+ ]).catch((_err) => {})
68
+
69
+ return {
70
+ type: typeof type === 'string' ? type : 'application/octet-stream',
71
+ stream: verifier,
72
+ }
73
+ } catch (err) {
74
+ // Typically un-supported content encoding
75
+ blobResponse.body.destroy()
76
+ throw err
77
+ }
78
78
  }
79
79
 
80
- private async uploadBlob(
81
- {
82
- imageStream,
83
- contentType,
84
- }: { imageStream: Readable; contentType: string },
85
- {
86
- subjectDid,
87
- subjectUri,
88
- }: { subjectDid: string; subjectUri: string | null },
89
- ) {
90
- const url = new URL(
91
- `${this.serviceConfig.url}/xrpc/com.atproto.unspecced.reportBlob`,
92
- )
93
- url.searchParams.set('did', subjectDid)
94
- if (subjectUri) url.searchParams.set('uri', subjectUri)
95
- const result = await this.sendImage({
96
- url: url.toString(),
97
- imageStream,
98
- contentType,
99
- })
80
+ /**
81
+ * @throws {XRPCError} so that retryHttp can handle retries
82
+ */
83
+ async uploadBlob(blob: Blob, report: ReportBlobOptions) {
84
+ const uploadUrl = reportBlobUrl(this.serviceConfig.url, report)
85
+
86
+ const result = await undici
87
+ .request(uploadUrl, {
88
+ method: 'POST',
89
+ body: blob.stream,
90
+ headersTimeout: 30e3,
91
+ bodyTimeout: 10e3,
92
+ headers: {
93
+ Authorization: basicAuth('admin', this.serviceConfig.adminPassword),
94
+ 'content-type': blob.type,
95
+ },
96
+ })
97
+ .catch((err) => {
98
+ throw asXrpcClientError(err, `Error uploading blob ${report.did}`)
99
+ })
100
+
101
+ if (result.statusCode !== 200) {
102
+ result.body.destroy()
103
+ throw new XRPCError(
104
+ result.statusCode,
105
+ undefined,
106
+ `Error uploading blob ${report.did}`,
107
+ )
108
+ }
100
109
 
101
- return result
110
+ await finished(result.body.resume())
102
111
  }
103
112
 
104
113
  async uploadBlobOnService({
105
- subjectDid,
106
- subjectUri,
114
+ subjectDid: did,
115
+ subjectUri: uri,
107
116
  subjectBlobCids,
108
117
  }: {
109
118
  subjectDid: string
110
119
  subjectUri: string | null
111
120
  subjectBlobCids: string[]
112
- }): Promise<boolean> {
113
- const didDoc = await this.idResolver.did.resolve(subjectDid)
114
-
115
- if (!didDoc) {
116
- throw new Error('Error resolving DID')
117
- }
121
+ }): Promise<void> {
122
+ const didDoc = await this.idResolver.did.resolve(did)
123
+ if (!didDoc) throw new Error('Error resolving DID')
118
124
 
119
125
  const pds = getPdsEndpoint(didDoc)
126
+ if (!pds) throw new Error('Error resolving PDS')
120
127
 
121
- if (!pds) {
122
- throw new Error('Error resolving PDS')
123
- }
124
-
125
- // attempt to download and upload within the same retry block since the imageStream is not reusable
126
- const uploadResult = await Promise.all(
128
+ await allFulfilled(
127
129
  subjectBlobCids.map((cid) =>
128
130
  retryHttp(async () => {
129
- const { imageStream, contentType } = await this.getBlob({
130
- pds,
131
- cid,
132
- did: subjectDid,
133
- })
134
- return this.uploadBlob(
135
- { imageStream, contentType },
136
- { subjectDid, subjectUri },
137
- )
131
+ // attempt to download and upload within the same retry block since
132
+ // the blob stream is not reusable
133
+ const blob = await this.getBlob({ pds, cid, did })
134
+ return this.uploadBlob(blob, { did, uri })
138
135
  }),
139
136
  ),
140
- )
141
-
142
- if (uploadResult.includes(false)) {
143
- throw new Error(`Error uploading blob ${subjectUri}`)
144
- }
145
-
146
- return true
137
+ ).catch((err) => {
138
+ throw new XRPCError(
139
+ ResponseType.UpstreamFailure,
140
+ undefined,
141
+ 'Failed to process blobs',
142
+ undefined,
143
+ { cause: err },
144
+ )
145
+ })
147
146
  }
148
147
  }
149
148
 
150
149
  const basicAuth = (username: string, password: string) => {
151
150
  return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
152
151
  }
152
+
153
+ type Blob = {
154
+ type: string
155
+ stream: Readable
156
+ }
157
+
158
+ type GetBlobOptions = {
159
+ pds: string
160
+ did: string
161
+ cid: string
162
+ }
163
+
164
+ function getBlobUrl({ pds, did, cid }: GetBlobOptions): URL {
165
+ const url = new URL(`/xrpc/com.atproto.sync.getBlob`, pds)
166
+ url.searchParams.set('did', did)
167
+ url.searchParams.set('cid', cid)
168
+ return url
169
+ }
170
+
171
+ type ReportBlobOptions = {
172
+ did: string
173
+ uri: string | null
174
+ }
175
+
176
+ function reportBlobUrl(service: string, { did, uri }: ReportBlobOptions): URL {
177
+ const url = new URL(`/xrpc/com.atproto.unspecced.reportBlob`, service)
178
+ url.searchParams.set('did', did)
179
+ if (uri != null) url.searchParams.set('uri', uri)
180
+ return url
181
+ }
182
+
183
+ function asXrpcClientError(err: unknown, message: string) {
184
+ return new XRPCError(ResponseType.Unknown, undefined, message, undefined, {
185
+ cause: err,
186
+ })
187
+ }
@@ -11309,6 +11309,15 @@ export const schemaDict = {
11309
11309
  description:
11310
11310
  'If true, all other reports on content authored by this account will be resolved (acknowledged).',
11311
11311
  },
11312
+ policies: {
11313
+ type: 'array',
11314
+ maxLength: 5,
11315
+ items: {
11316
+ type: 'string',
11317
+ },
11318
+ description:
11319
+ 'Names/Keywords of the policies that drove the decision.',
11320
+ },
11312
11321
  },
11313
11322
  },
11314
11323
  modEventReverseTakedown: {
@@ -12345,6 +12354,14 @@ export const schemaDict = {
12345
12354
  type: 'string',
12346
12355
  },
12347
12356
  },
12357
+ policies: {
12358
+ type: 'array',
12359
+ items: {
12360
+ type: 'string',
12361
+ description:
12362
+ 'If specified, only events where the policy matches the given policy are returned',
12363
+ },
12364
+ },
12348
12365
  cursor: {
12349
12366
  type: 'string',
12350
12367
  },
@@ -174,6 +174,8 @@ export interface ModEventTakedown {
174
174
  durationInHours?: number
175
175
  /** If true, all other reports on content authored by this account will be resolved (acknowledged). */
176
176
  acknowledgeAccountSubjects?: boolean
177
+ /** Names/Keywords of the policies that drove the decision. */
178
+ policies?: string[]
177
179
  [k: string]: unknown
178
180
  }
179
181
 
@@ -40,6 +40,7 @@ export interface QueryParams {
40
40
  /** If specified, only events where all of these tags were removed are returned */
41
41
  removedTags?: string[]
42
42
  reportTypes?: string[]
43
+ policies?: string[]
43
44
  cursor?: string
44
45
  }
45
46
 
@@ -152,6 +152,7 @@ export class ModerationService {
152
152
  reportTypes?: string[]
153
153
  collections: string[]
154
154
  subjectType?: string
155
+ policies?: string[]
155
156
  }): Promise<{ cursor?: string; events: ModerationEventRow[] }> {
156
157
  const {
157
158
  subject,
@@ -172,6 +173,7 @@ export class ModerationService {
172
173
  reportTypes,
173
174
  collections,
174
175
  subjectType,
176
+ policies,
175
177
  } = opts
176
178
  const { ref } = this.db.db.dynamic
177
179
  let builder = this.db.db.selectFrom('moderation_event').selectAll()
@@ -264,6 +266,14 @@ export class ModerationService {
264
266
  if (reportTypes?.length) {
265
267
  builder = builder.where(sql`meta->>'reportType'`, 'in', reportTypes)
266
268
  }
269
+ if (policies?.length) {
270
+ builder = builder.where((qb) => {
271
+ policies.forEach((policy) => {
272
+ qb = qb.orWhere(sql`meta->>'policies'`, 'ilike', `%${policy}%`)
273
+ })
274
+ return qb
275
+ })
276
+ }
267
277
 
268
278
  const keyset = new TimeIdKeyset(
269
279
  ref(`moderation_event.createdAt`),
@@ -435,6 +445,10 @@ export class ModerationService {
435
445
  meta.acknowledgeAccountSubjects = true
436
446
  }
437
447
 
448
+ if (isModEventTakedown(event) && event.policies?.length) {
449
+ meta.policies = event.policies.join(',')
450
+ }
451
+
438
452
  // Keep trace of reports that came in while the reporter was in muted stated
439
453
  if (isModEventReport(event)) {
440
454
  const isReportingMuted = await this.isReportingMutedForSubject(createdBy)
@@ -137,6 +137,17 @@ export class ModerationViews {
137
137
  }
138
138
  }
139
139
 
140
+ if (
141
+ event.action === 'tools.ozone.moderation.defs#modEventTakedown' &&
142
+ typeof event.meta?.policies === 'string' &&
143
+ event.meta.policies.length > 0
144
+ ) {
145
+ eventView.event = {
146
+ ...eventView.event,
147
+ policies: event.meta.policies.split(','),
148
+ }
149
+ }
150
+
140
151
  if (event.action === 'tools.ozone.moderation.defs#modEventLabel') {
141
152
  eventView.event = {
142
153
  ...eventView.event,
@@ -1 +1,2 @@
1
1
  export const ProtectedTagSettingKey = 'tools.ozone.setting.protectedTags'
2
+ export const PolicyListSettingKey = 'tools.ozone.setting.policyList'
@@ -1,6 +1,6 @@
1
1
  import { Selectable } from 'kysely'
2
2
  import { Setting } from '../db/schema/setting'
3
- import { ProtectedTagSettingKey } from './constants'
3
+ import { PolicyListSettingKey, ProtectedTagSettingKey } from './constants'
4
4
  import { InvalidRequestError } from '@atproto/xrpc-server'
5
5
 
6
6
  export const settingValidators = new Map<
@@ -58,4 +58,31 @@ export const settingValidators = new Map<
58
58
  }
59
59
  },
60
60
  ],
61
+ [
62
+ PolicyListSettingKey,
63
+ async (setting: Partial<Selectable<Setting>>) => {
64
+ if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {
65
+ throw new InvalidRequestError(
66
+ 'Only admins should be able to manage policy list',
67
+ )
68
+ }
69
+
70
+ if (typeof setting.value !== 'object') {
71
+ throw new InvalidRequestError('Invalid value')
72
+ }
73
+ for (const [key, val] of Object.entries(setting.value)) {
74
+ if (!val || typeof val !== 'object') {
75
+ throw new InvalidRequestError(
76
+ `Invalid configuration for policy ${key}`,
77
+ )
78
+ }
79
+
80
+ if (!val['name'] || !val['description']) {
81
+ throw new InvalidRequestError(
82
+ `Must define a name and description for policy ${key}`,
83
+ )
84
+ }
85
+ }
86
+ },
87
+ ],
61
88
  ])
package/src/util.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { AxiosError } from 'axios'
1
+ import { createRetryable } from '@atproto/common'
2
+ import { ResponseType, XRPCError } from '@atproto/xrpc'
2
3
  import { parseList } from 'structured-headers'
3
- import { XRPCError, ResponseType } from '@atproto/xrpc'
4
- import { RetryOptions, retry } from '@atproto/common'
5
4
  import Database from './db'
6
5
 
7
6
  export const getSigningKeyId = async (
@@ -24,28 +23,17 @@ export const getSigningKeyId = async (
24
23
  return insertRes.id
25
24
  }
26
25
 
27
- export async function retryHttp<T>(
28
- fn: () => Promise<T>,
29
- opts: RetryOptions = {},
30
- ): Promise<T> {
31
- return retry(fn, { retryable: retryableHttp, ...opts })
32
- }
26
+ export const RETRYABLE_HTTP_STATUS_CODES = new Set([
27
+ 408, 425, 429, 500, 502, 503, 504, 522, 524,
28
+ ])
33
29
 
34
- export function retryableHttp(err: unknown) {
30
+ export const retryHttp = createRetryable((err: unknown) => {
35
31
  if (err instanceof XRPCError) {
36
32
  if (err.status === ResponseType.Unknown) return true
37
- return retryableHttpStatusCodes.has(err.status)
38
- }
39
- if (err instanceof AxiosError) {
40
- if (!err.response) return true
41
- return retryableHttpStatusCodes.has(err.response.status)
33
+ return RETRYABLE_HTTP_STATUS_CODES.has(err.status)
42
34
  }
43
35
  return false
44
- }
45
-
46
- const retryableHttpStatusCodes = new Set([
47
- 408, 425, 429, 500, 502, 503, 504, 522, 524,
48
- ])
36
+ })
49
37
 
50
38
  export type ParsedLabelers = {
51
39
  dids: string[]
package/tests/_util.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { type Express } from 'express'
2
+ import { Server } from 'node:http'
3
+ import { AddressInfo } from 'node:net'
1
4
  import { AtUri } from '@atproto/syntax'
2
5
  import { lexToJson } from '@atproto/lexicon'
3
6
  import { CID } from 'multiformats/cid'
@@ -195,3 +198,46 @@ export const stripViewerFromThread = <T>(thread: T): T => {
195
198
  }
196
199
  return thread
197
200
  }
201
+
202
+ export async function startServer(app: Express) {
203
+ return new Promise<{
204
+ origin: string
205
+ server: Server
206
+ stop: () => Promise<void>
207
+ }>((resolve, reject) => {
208
+ const onListen = () => {
209
+ const port = (server.address() as AddressInfo).port
210
+ resolve({
211
+ server,
212
+ origin: `http://localhost:${port}`,
213
+ stop: () => stopServer(server),
214
+ })
215
+ cleanup()
216
+ }
217
+ const onError = (err: Error) => {
218
+ reject(err)
219
+ cleanup()
220
+ }
221
+ const cleanup = () => {
222
+ server.removeListener('listening', onListen)
223
+ server.removeListener('error', onError)
224
+ }
225
+
226
+ const server = app
227
+ .listen(0)
228
+ .once('listening', onListen)
229
+ .once('error', onError)
230
+ })
231
+ }
232
+
233
+ export async function stopServer(server: Server) {
234
+ return new Promise<void>((resolve, reject) => {
235
+ server.close((err) => {
236
+ if (err) {
237
+ reject(err)
238
+ } else {
239
+ resolve()
240
+ }
241
+ })
242
+ })
243
+ }