@atproto/ozone 0.1.108 → 0.1.109

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 (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/api/health.js +1 -1
  3. package/dist/api/health.js.map +1 -1
  4. package/dist/api/verification/grantVerifications.d.ts.map +1 -1
  5. package/dist/api/verification/grantVerifications.js +9 -1
  6. package/dist/api/verification/grantVerifications.js.map +1 -1
  7. package/dist/background.js +1 -1
  8. package/dist/background.js.map +1 -1
  9. package/dist/error.js +1 -1
  10. package/dist/error.js.map +1 -1
  11. package/dist/jetstream/service.d.ts +1 -5
  12. package/dist/jetstream/service.d.ts.map +1 -1
  13. package/dist/jetstream/service.js +1 -1
  14. package/dist/jetstream/service.js.map +1 -1
  15. package/dist/lexicon/index.d.ts +3 -0
  16. package/dist/lexicon/index.d.ts.map +1 -1
  17. package/dist/lexicon/index.js +4 -1
  18. package/dist/lexicon/index.js.map +1 -1
  19. package/dist/lexicon/lexicons.d.ts +160 -0
  20. package/dist/lexicon/lexicons.d.ts.map +1 -1
  21. package/dist/lexicon/lexicons.js +81 -0
  22. package/dist/lexicon/lexicons.js.map +1 -1
  23. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +21 -0
  24. package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
  25. package/dist/lexicon/types/app/bsky/actor/defs.js +9 -0
  26. package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
  27. package/dist/lexicon/types/app/bsky/actor/status.d.ts +23 -0
  28. package/dist/lexicon/types/app/bsky/actor/status.d.ts.map +1 -0
  29. package/dist/lexicon/types/app/bsky/actor/status.js +19 -0
  30. package/dist/lexicon/types/app/bsky/actor/status.js.map +1 -0
  31. package/dist/mod-service/index.js +1 -1
  32. package/dist/mod-service/index.js.map +1 -1
  33. package/dist/mod-service/views.d.ts.map +1 -1
  34. package/dist/mod-service/views.js +2 -0
  35. package/dist/mod-service/views.js.map +1 -1
  36. package/dist/team/index.d.ts.map +1 -1
  37. package/dist/team/index.js +2 -2
  38. package/dist/team/index.js.map +1 -1
  39. package/package.json +9 -9
  40. package/src/api/health.ts +1 -1
  41. package/src/api/verification/grantVerifications.ts +17 -1
  42. package/src/background.ts +1 -1
  43. package/src/error.ts +1 -1
  44. package/src/jetstream/service.ts +2 -8
  45. package/src/lexicon/index.ts +3 -0
  46. package/src/lexicon/lexicons.ts +85 -0
  47. package/src/lexicon/types/app/bsky/actor/defs.ts +26 -0
  48. package/src/lexicon/types/app/bsky/actor/status.ts +40 -0
  49. package/src/mod-service/index.ts +1 -1
  50. package/src/mod-service/views.ts +4 -0
  51. package/src/team/index.ts +2 -5
  52. package/tests/expiring-label.test.ts +72 -0
  53. package/tests/verification.test.ts +30 -0
  54. package/tsconfig.build.tsbuildinfo +1 -1
  55. package/tsconfig.tests.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ozone",
3
- "version": "0.1.108",
3
+ "version": "0.1.109",
4
4
  "license": "MIT",
5
5
  "description": "Backend service for moderating the Bluesky network.",
6
6
  "keywords": [
@@ -35,14 +35,14 @@
35
35
  "uint8arrays": "3.0.0",
36
36
  "undici": "^6.14.1",
37
37
  "ws": "^8.12.0",
38
- "@atproto/api": "^0.15.5",
39
- "@atproto/common": "^0.4.10",
38
+ "@atproto/api": "^0.15.6",
39
+ "@atproto/common": "^0.4.11",
40
40
  "@atproto/crypto": "^0.4.4",
41
- "@atproto/identity": "^0.4.7",
42
- "@atproto/lexicon": "^0.4.10",
41
+ "@atproto/identity": "^0.4.8",
42
+ "@atproto/lexicon": "^0.4.11",
43
43
  "@atproto/syntax": "^0.4.0",
44
- "@atproto/xrpc": "^0.6.12",
45
- "@atproto/xrpc-server": "^0.7.17"
44
+ "@atproto/xrpc": "^0.7.0",
45
+ "@atproto/xrpc-server": "^0.7.18"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@did-plc/server": "^0.0.1",
@@ -54,8 +54,8 @@
54
54
  "jest": "^28.1.2",
55
55
  "ts-node": "^10.8.2",
56
56
  "typescript": "^5.6.3",
57
- "@atproto/lex-cli": "^0.8.0",
58
- "@atproto/pds": "^0.4.134"
57
+ "@atproto/lex-cli": "^0.8.1",
58
+ "@atproto/pds": "^0.4.135"
59
59
  },
60
60
  "scripts": {
61
61
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
package/src/api/health.ts CHANGED
@@ -17,7 +17,7 @@ export const createRouter = (ctx: AppContext): Router => {
17
17
  try {
18
18
  await sql`select 1`.execute(ctx.db.db)
19
19
  } catch (err) {
20
- req.log.error(err, 'failed health check')
20
+ req.log.error({ err }, 'failed health check')
21
21
  return res.status(503).send({ version, error: 'Service Unavailable' })
22
22
  }
23
23
  res.send({ version })
@@ -20,10 +20,26 @@ export default function (server: Server, ctx: AppContext) {
20
20
  }
21
21
 
22
22
  const modViews = ctx.modService(ctx.db).views
23
+ const profilesBefore = await modViews.getProfiles(
24
+ input.body.verifications.map((v) => v.subject),
25
+ )
26
+
27
+ // Filter out any subject for which, the current issuer already has a valid verification record indexed
28
+ const verificationsToBeGranted = input.body.verifications.filter(
29
+ (verificationInput) => {
30
+ const hasValidVerification = profilesBefore
31
+ .get(verificationInput.subject)
32
+ ?.verification?.verifications.find(
33
+ (v) => v.issuer === ctx.cfg.verifier?.did && v.isValid,
34
+ )
35
+ return !hasValidVerification
36
+ },
37
+ )
38
+
23
39
  const verificationIssuer = ctx.verificationIssuer(ctx.cfg.verifier)
24
40
  const verificationService = ctx.verificationService(ctx.db)
25
41
  const { grantedVerifications, failedVerifications } =
26
- await verificationIssuer.verify(input.body.verifications)
42
+ await verificationIssuer.verify(verificationsToBeGranted)
27
43
 
28
44
  if (!grantedVerifications.length) {
29
45
  return {
package/src/background.ts CHANGED
@@ -63,7 +63,7 @@ export class BackgroundQueue {
63
63
  await task(this.db, abortController.signal)
64
64
  } catch (err) {
65
65
  if (!isCausedBySignal(err, abortController.signal)) {
66
- dbLogger.error(err, 'background queue task failed')
66
+ dbLogger.error({ err }, 'background queue task failed')
67
67
  }
68
68
  } finally {
69
69
  abortController.abort()
package/src/error.ts CHANGED
@@ -3,7 +3,7 @@ import { XRPCError } from '@atproto/xrpc-server'
3
3
  import { httpLogger as log } from './logger'
4
4
 
5
5
  export const handler: ErrorRequestHandler = (err, _req, res, next) => {
6
- log.error(err, 'unexpected internal server error')
6
+ log.error({ err }, 'unexpected internal server error')
7
7
  if (res.headersSent) {
8
8
  return next(err)
9
9
  }
@@ -6,11 +6,7 @@ type OnCreateCallback<T extends JetstreamRecord> = (
6
6
  ) => Promise<void>
7
7
 
8
8
  export type JetstreamOptions = {
9
- /**
10
- * The full subscription endpoint to connect to.
11
- * @default "wss://jetstream1.us-east.bsky.network/subscribe"
12
- */
13
- endpoint?: string
9
+ endpoint: string
14
10
  /**
15
11
  * The record collections that you want to receive updates for.
16
12
  * Leave this empty to receive updates for all record collections.
@@ -61,9 +57,7 @@ export class Jetstream {
61
57
  public cursor?: number
62
58
 
63
59
  constructor(opts: JetstreamOptions) {
64
- this.url = new URL(
65
- opts.endpoint ?? 'wss://jetstream1.us-east.bsky.network/subscribe',
66
- )
60
+ this.url = new URL(opts.endpoint)
67
61
  opts.wantedCollections?.forEach((collection) => {
68
62
  this.url.searchParams.append('wantedCollections', collection)
69
63
  })
@@ -230,6 +230,9 @@ export const COM_ATPROTO_MODERATION = {
230
230
  DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',
231
231
  DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',
232
232
  }
233
+ export const APP_BSKY_ACTOR = {
234
+ StatusLive: 'app.bsky.actor.status#live',
235
+ }
233
236
  export const APP_BSKY_FEED = {
234
237
  DefsRequestLess: 'app.bsky.feed.defs#requestLess',
235
238
  DefsRequestMore: 'app.bsky.feed.defs#requestMore',
@@ -4568,6 +4568,10 @@ export const schemaDict = {
4568
4568
  type: 'ref',
4569
4569
  ref: 'lex:app.bsky.actor.defs#verificationState',
4570
4570
  },
4571
+ status: {
4572
+ type: 'ref',
4573
+ ref: 'lex:app.bsky.actor.defs#statusView',
4574
+ },
4571
4575
  },
4572
4576
  },
4573
4577
  profileView: {
@@ -4623,6 +4627,10 @@ export const schemaDict = {
4623
4627
  type: 'ref',
4624
4628
  ref: 'lex:app.bsky.actor.defs#verificationState',
4625
4629
  },
4630
+ status: {
4631
+ type: 'ref',
4632
+ ref: 'lex:app.bsky.actor.defs#statusView',
4633
+ },
4626
4634
  },
4627
4635
  },
4628
4636
  profileViewDetailed: {
@@ -4699,6 +4707,10 @@ export const schemaDict = {
4699
4707
  type: 'ref',
4700
4708
  ref: 'lex:app.bsky.actor.defs#verificationState',
4701
4709
  },
4710
+ status: {
4711
+ type: 'ref',
4712
+ ref: 'lex:app.bsky.actor.defs#statusView',
4713
+ },
4702
4714
  },
4703
4715
  },
4704
4716
  profileAssociated: {
@@ -5240,6 +5252,36 @@ export const schemaDict = {
5240
5252
  },
5241
5253
  },
5242
5254
  },
5255
+ statusView: {
5256
+ type: 'object',
5257
+ required: ['status', 'record'],
5258
+ properties: {
5259
+ status: {
5260
+ type: 'string',
5261
+ description: 'The status for the account.',
5262
+ knownValues: ['app.bsky.actor.status#live'],
5263
+ },
5264
+ record: {
5265
+ type: 'unknown',
5266
+ },
5267
+ embed: {
5268
+ type: 'union',
5269
+ description: 'An optional embed associated with the status.',
5270
+ refs: ['lex:app.bsky.embed.external#view'],
5271
+ },
5272
+ expiresAt: {
5273
+ type: 'string',
5274
+ description:
5275
+ 'The date when this status will expire. The application might choose to no longer return the status after expiration.',
5276
+ format: 'datetime',
5277
+ },
5278
+ isActive: {
5279
+ type: 'boolean',
5280
+ description:
5281
+ 'True if the status is not expired, false if it is expired. Only present if expiration was set.',
5282
+ },
5283
+ },
5284
+ },
5243
5285
  },
5244
5286
  },
5245
5287
  AppBskyActorGetPreferences: {
@@ -5569,6 +5611,48 @@ export const schemaDict = {
5569
5611
  },
5570
5612
  },
5571
5613
  },
5614
+ AppBskyActorStatus: {
5615
+ lexicon: 1,
5616
+ id: 'app.bsky.actor.status',
5617
+ defs: {
5618
+ main: {
5619
+ type: 'record',
5620
+ description: 'A declaration of a Bluesky account status.',
5621
+ key: 'literal:self',
5622
+ record: {
5623
+ type: 'object',
5624
+ required: ['status', 'createdAt'],
5625
+ properties: {
5626
+ status: {
5627
+ type: 'string',
5628
+ description: 'The status for the account.',
5629
+ knownValues: ['app.bsky.actor.status#live'],
5630
+ },
5631
+ embed: {
5632
+ type: 'union',
5633
+ description: 'An optional embed associated with the status.',
5634
+ refs: ['lex:app.bsky.embed.external'],
5635
+ },
5636
+ durationMinutes: {
5637
+ type: 'integer',
5638
+ description:
5639
+ 'The duration of the status in minutes. Applications can choose to impose minimum and maximum limits.',
5640
+ minimum: 1,
5641
+ },
5642
+ createdAt: {
5643
+ type: 'string',
5644
+ format: 'datetime',
5645
+ },
5646
+ },
5647
+ },
5648
+ },
5649
+ live: {
5650
+ type: 'token',
5651
+ description:
5652
+ 'Advertises an account as currently offering live content.',
5653
+ },
5654
+ },
5655
+ },
5572
5656
  AppBskyEmbedDefs: {
5573
5657
  lexicon: 1,
5574
5658
  id: 'app.bsky.embed.defs',
@@ -16113,6 +16197,7 @@ export const ids = {
16113
16197
  AppBskyActorPutPreferences: 'app.bsky.actor.putPreferences',
16114
16198
  AppBskyActorSearchActors: 'app.bsky.actor.searchActors',
16115
16199
  AppBskyActorSearchActorsTypeahead: 'app.bsky.actor.searchActorsTypeahead',
16200
+ AppBskyActorStatus: 'app.bsky.actor.status',
16116
16201
  AppBskyEmbedDefs: 'app.bsky.embed.defs',
16117
16202
  AppBskyEmbedExternal: 'app.bsky.embed.external',
16118
16203
  AppBskyEmbedImages: 'app.bsky.embed.images',
@@ -14,6 +14,7 @@ import type * as AppBskyGraphDefs from '../graph/defs.js'
14
14
  import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef.js'
15
15
  import type * as AppBskyFeedThreadgate from '../feed/threadgate.js'
16
16
  import type * as AppBskyFeedPostgate from '../feed/postgate.js'
17
+ import type * as AppBskyEmbedExternal from '../embed/external.js'
17
18
 
18
19
  const is$typed = _is$typed,
19
20
  validate = _validate
@@ -30,6 +31,7 @@ export interface ProfileViewBasic {
30
31
  labels?: ComAtprotoLabelDefs.Label[]
31
32
  createdAt?: string
32
33
  verification?: VerificationState
34
+ status?: StatusView
33
35
  }
34
36
 
35
37
  const hashProfileViewBasic = 'profileViewBasic'
@@ -55,6 +57,7 @@ export interface ProfileView {
55
57
  viewer?: ViewerState
56
58
  labels?: ComAtprotoLabelDefs.Label[]
57
59
  verification?: VerificationState
60
+ status?: StatusView
58
61
  }
59
62
 
60
63
  const hashProfileView = 'profileView'
@@ -86,6 +89,7 @@ export interface ProfileViewDetailed {
86
89
  labels?: ComAtprotoLabelDefs.Label[]
87
90
  pinnedPost?: ComAtprotoRepoStrongRef.Main
88
91
  verification?: VerificationState
92
+ status?: StatusView
89
93
  }
90
94
 
91
95
  const hashProfileViewDetailed = 'profileViewDetailed'
@@ -592,3 +596,25 @@ export function validatePostInteractionSettingsPref<V>(v: V) {
592
596
  hashPostInteractionSettingsPref,
593
597
  )
594
598
  }
599
+
600
+ export interface StatusView {
601
+ $type?: 'app.bsky.actor.defs#statusView'
602
+ /** The status for the account. */
603
+ status: 'app.bsky.actor.status#live' | (string & {})
604
+ record: { [_ in string]: unknown }
605
+ embed?: $Typed<AppBskyEmbedExternal.View> | { $type: string }
606
+ /** The date when this status will expire. The application might choose to no longer return the status after expiration. */
607
+ expiresAt?: string
608
+ /** True if the status is not expired, false if it is expired. Only present if expiration was set. */
609
+ isActive?: boolean
610
+ }
611
+
612
+ const hashStatusView = 'statusView'
613
+
614
+ export function isStatusView<V>(v: V) {
615
+ return is$typed(v, id, hashStatusView)
616
+ }
617
+
618
+ export function validateStatusView<V>(v: V) {
619
+ return validate<StatusView & V>(v, id, hashStatusView)
620
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+ import { CID } from 'multiformats/cid'
6
+ import { validate as _validate } from '../../../../lexicons'
7
+ import {
8
+ type $Typed,
9
+ is$typed as _is$typed,
10
+ type OmitKey,
11
+ } from '../../../../util'
12
+ import type * as AppBskyEmbedExternal from '../embed/external.js'
13
+
14
+ const is$typed = _is$typed,
15
+ validate = _validate
16
+ const id = 'app.bsky.actor.status'
17
+
18
+ export interface Record {
19
+ $type: 'app.bsky.actor.status'
20
+ /** The status for the account. */
21
+ status: 'app.bsky.actor.status#live' | (string & {})
22
+ embed?: $Typed<AppBskyEmbedExternal.Main> | { $type: string }
23
+ /** The duration of the status in minutes. Applications can choose to impose minimum and maximum limits. */
24
+ durationMinutes?: number
25
+ createdAt: string
26
+ [k: string]: unknown
27
+ }
28
+
29
+ const hashRecord = 'main'
30
+
31
+ export function isRecord<V>(v: V) {
32
+ return is$typed(v, id, hashRecord)
33
+ }
34
+
35
+ export function validateRecord<V>(v: V) {
36
+ return validate<Record & V>(v, id, hashRecord, true)
37
+ }
38
+
39
+ /** Advertises an account as currently offering live content. */
40
+ export const LIVE = `${id}#live`
@@ -727,7 +727,7 @@ export class ModerationService {
727
727
  this.eventPusher
728
728
  .attemptBlobEvent(evt.id)
729
729
  .catch((err) =>
730
- log.error({ err, ...evt }, 'failed to push blob event'),
730
+ log.error({ ...evt, err }, 'failed to push blob event'),
731
731
  ),
732
732
  ),
733
733
  )
@@ -544,10 +544,14 @@ export class ModerationViews {
544
544
  subjects: string[],
545
545
  includeNeg?: boolean,
546
546
  ): Promise<Map<string, Label[]>> {
547
+ const now = new Date().toISOString()
547
548
  const labels = new Map<string, Label[]>()
548
549
  const res = await this.db.db
549
550
  .selectFrom('label')
550
551
  .where('label.uri', 'in', subjects)
552
+ .where((qb) =>
553
+ qb.where('label.exp', 'is', null).orWhere('label.exp', '>', now),
554
+ )
551
555
  .if(!includeNeg, (qb) => qb.where('neg', '=', false))
552
556
  .selectAll()
553
557
  .execute()
package/src/team/index.ts CHANGED
@@ -229,11 +229,8 @@ export class TeamService {
229
229
  profiles.set(profile.did, profile)
230
230
  })
231
231
  }
232
- } catch (error) {
233
- httpLogger.error(
234
- { error, dids },
235
- 'Failed to get profiles for team members',
236
- )
232
+ } catch (err) {
233
+ httpLogger.error({ err, dids }, 'Failed to get profiles for team members')
237
234
  }
238
235
 
239
236
  return profiles
@@ -0,0 +1,72 @@
1
+ import AtpAgent from '@atproto/api'
2
+ import {
3
+ ModeratorClient,
4
+ SeedClient,
5
+ TestNetwork,
6
+ basicSeed,
7
+ } from '@atproto/dev-env'
8
+ import { ids } from '../src/lexicon/lexicons'
9
+
10
+ describe('expiring label', () => {
11
+ let network: TestNetwork
12
+ let sc: SeedClient
13
+ let modClient: ModeratorClient
14
+ let agent: AtpAgent
15
+
16
+ beforeAll(async () => {
17
+ network = await TestNetwork.create({
18
+ dbPostgresSchema: 'ozone_expiring_label_test',
19
+ })
20
+ sc = network.getSeedClient()
21
+ agent = network.ozone.getClient()
22
+ modClient = network.ozone.getModClient()
23
+ await basicSeed(sc)
24
+ await network.processAll()
25
+ })
26
+
27
+ afterAll(async () => {
28
+ await network.close()
29
+ })
30
+
31
+ const emitExpiringLabel = async (did: string) =>
32
+ modClient.emitEvent(
33
+ {
34
+ subject: { $type: 'com.atproto.admin.defs#repoRef', did },
35
+ event: {
36
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
37
+ comment: 'Testing expiring label',
38
+ createLabelVals: ['expiring'],
39
+ negateLabelVals: [],
40
+ durationInHours: 1,
41
+ },
42
+ createdBy: sc.dids.alice,
43
+ },
44
+ 'moderator',
45
+ )
46
+
47
+ it('Returns expiring label only within expiration period', async () => {
48
+ const getRepo = async (did: string) =>
49
+ agent.tools.ozone.moderation.getRepo(
50
+ { did },
51
+ {
52
+ headers: await network.ozone.modHeaders(
53
+ ids.ToolsOzoneModerationGetRepo,
54
+ ),
55
+ },
56
+ )
57
+
58
+ const now = new Date().toISOString()
59
+ await emitExpiringLabel(sc.dids.carol)
60
+ const { data: repoWithExpiringLabel } = await getRepo(sc.dids.carol)
61
+ expect(repoWithExpiringLabel.labels?.[0].val).toEqual('expiring')
62
+ // Manually expire the label in db
63
+ await network.ozone.ctx.db.db
64
+ .updateTable('label')
65
+ .set({ exp: now })
66
+ .where('uri', '=', sc.dids.carol)
67
+ .execute()
68
+
69
+ const { data: repoAfterExpiringLabel } = await getRepo(sc.dids.carol)
70
+ expect(repoAfterExpiringLabel.labels?.length).toEqual(0)
71
+ })
72
+ })
@@ -133,4 +133,34 @@ describe('verification', () => {
133
133
  )
134
134
  })
135
135
  })
136
+
137
+ it('does not publish record if a valid one already exists', async () => {
138
+ const { data: beforePublish } =
139
+ await adminAgent.tools.ozone.verification.listVerifications({
140
+ subjects: [sc.dids.bob],
141
+ })
142
+ const {
143
+ data: { verifications },
144
+ } = await adminAgent.tools.ozone.verification.grantVerifications({
145
+ verifications: [
146
+ {
147
+ subject: sc.dids.bob,
148
+ handle: sc.accounts[sc.dids.bob].handle,
149
+ displayName: 'bobby',
150
+ },
151
+ ],
152
+ })
153
+
154
+ const { data: afterPublish } =
155
+ await adminAgent.tools.ozone.verification.listVerifications({
156
+ subjects: [sc.dids.bob],
157
+ })
158
+
159
+ // assert that the response does not contain any new verification
160
+ expect(verifications.length).toEqual(0)
161
+ // assert that the list of verifications in db hasn't changed
162
+ expect(afterPublish.verifications.length).toEqual(
163
+ beforePublish.verifications.length,
164
+ )
165
+ })
136
166
  })