@atproto/ozone 0.0.16 → 0.0.17-next.1

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 (157) hide show
  1. package/dist/api/util.d.ts +10 -0
  2. package/dist/auth-verifier.d.ts +8 -12
  3. package/dist/communication-service/template.d.ts +2 -2
  4. package/dist/config/config.d.ts +6 -0
  5. package/dist/config/env.d.ts +3 -2
  6. package/dist/config/secrets.d.ts +0 -2
  7. package/dist/context.d.ts +6 -0
  8. package/dist/daemon/blob-diverter.d.ts +26 -0
  9. package/dist/daemon/event-pusher.d.ts +6 -0
  10. package/dist/daemon/index.d.ts +1 -0
  11. package/dist/db/index.js +21 -1
  12. package/dist/db/index.js.map +3 -3
  13. package/dist/db/migrations/20240228T003647759Z-add-label-sigs.d.ts +3 -0
  14. package/dist/db/migrations/index.d.ts +1 -0
  15. package/dist/db/schema/index.d.ts +2 -1
  16. package/dist/db/schema/label.d.ts +4 -0
  17. package/dist/db/schema/moderation_event.d.ts +1 -1
  18. package/dist/db/schema/moderation_subject_status.d.ts +2 -2
  19. package/dist/db/schema/signing_key.d.ts +9 -0
  20. package/dist/index.js +10400 -10313
  21. package/dist/index.js.map +3 -3
  22. package/dist/lexicon/index.d.ts +55 -27
  23. package/dist/lexicon/lexicons.d.ts +5046 -4757
  24. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +23 -1
  25. package/dist/lexicon/types/app/bsky/embed/record.d.ts +2 -1
  26. package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
  27. package/dist/lexicon/types/app/bsky/graph/defs.d.ts +3 -0
  28. package/dist/lexicon/types/app/bsky/labeler/defs.d.ts +41 -0
  29. package/dist/lexicon/types/app/bsky/labeler/getServices.d.ts +36 -0
  30. package/dist/lexicon/types/app/bsky/labeler/service.d.ts +14 -0
  31. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +0 -304
  32. package/dist/lexicon/types/com/atproto/label/defs.d.ts +23 -0
  33. package/dist/lexicon/types/{com/atproto/admin/createCommunicationTemplate.d.ts → tools/ozone/communication/createTemplate.d.ts} +2 -2
  34. package/dist/lexicon/types/tools/ozone/communication/defs.d.ts +14 -0
  35. package/dist/lexicon/types/{com/atproto/admin/listCommunicationTemplates.d.ts → tools/ozone/communication/listTemplates.d.ts} +2 -2
  36. package/dist/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.d.ts → tools/ozone/communication/updateTemplate.d.ts} +2 -2
  37. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +269 -0
  38. package/dist/lexicon/types/{com/atproto/admin/emitModerationEvent.d.ts → tools/ozone/moderation/emitEvent.d.ts} +5 -4
  39. package/dist/lexicon/types/{com/atproto/admin/getModerationEvent.d.ts → tools/ozone/moderation/getEvent.d.ts} +2 -2
  40. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.d.ts +2 -2
  41. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.d.ts +2 -2
  42. package/dist/lexicon/types/{com/atproto/admin/queryModerationEvents.d.ts → tools/ozone/moderation/queryEvents.d.ts} +2 -2
  43. package/dist/lexicon/types/{com/atproto/admin/queryModerationStatuses.d.ts → tools/ozone/moderation/queryStatuses.d.ts} +2 -2
  44. package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.d.ts +2 -2
  45. package/dist/mod-service/index.d.ts +16 -15
  46. package/dist/mod-service/subject.d.ts +1 -1
  47. package/dist/mod-service/types.d.ts +2 -2
  48. package/dist/mod-service/util.d.ts +6 -0
  49. package/dist/mod-service/views.d.ts +9 -3
  50. package/dist/sequencer/sequencer.d.ts +6 -4
  51. package/dist/util.d.ts +2 -0
  52. package/package.json +9 -8
  53. package/src/api/{admin/createCommunicationTemplate.ts → communication/createTemplate.ts} +2 -2
  54. package/src/api/{admin/deleteCommunicationTemplate.ts → communication/deleteTemplate.ts} +2 -2
  55. package/src/api/{admin/listCommunicationTemplates.ts → communication/listTemplates.ts} +2 -2
  56. package/src/api/{admin/updateCommunicationTemplate.ts → communication/updateTemplate.ts} +2 -2
  57. package/src/api/index.ts +21 -21
  58. package/src/api/{temp → label}/fetchLabels.ts +5 -3
  59. package/src/api/label/queryLabels.ts +4 -2
  60. package/src/api/moderation/emitEvent.ts +218 -0
  61. package/src/api/{admin/getModerationEvent.ts → moderation/getEvent.ts} +2 -2
  62. package/src/api/{admin → moderation}/getRecord.ts +3 -3
  63. package/src/api/{admin → moderation}/getRepo.ts +3 -3
  64. package/src/api/{admin/queryModerationEvents.ts → moderation/queryEvents.ts} +3 -3
  65. package/src/api/{admin/queryModerationStatuses.ts → moderation/queryStatuses.ts} +3 -3
  66. package/src/api/{admin → moderation}/searchRepos.ts +2 -2
  67. package/src/api/proxied.ts +8 -8
  68. package/src/api/{moderation → report}/createReport.ts +2 -3
  69. package/src/api/util.ts +119 -0
  70. package/src/auth-verifier.ts +20 -30
  71. package/src/communication-service/template.ts +2 -2
  72. package/src/config/config.ts +24 -7
  73. package/src/config/env.ts +6 -4
  74. package/src/config/secrets.ts +0 -6
  75. package/src/context.ts +36 -12
  76. package/src/daemon/blob-diverter.ts +150 -0
  77. package/src/daemon/context.ts +11 -7
  78. package/src/daemon/event-pusher.ts +58 -15
  79. package/src/daemon/index.ts +1 -0
  80. package/src/db/migrations/20240228T003647759Z-add-label-sigs.ts +25 -0
  81. package/src/db/migrations/index.ts +1 -0
  82. package/src/db/schema/index.ts +2 -0
  83. package/src/db/schema/label.ts +3 -0
  84. package/src/db/schema/moderation_event.ts +11 -11
  85. package/src/db/schema/moderation_subject_status.ts +7 -2
  86. package/src/db/schema/signing_key.ts +10 -0
  87. package/src/lexicon/index.ts +200 -137
  88. package/src/lexicon/lexicons.ts +6310 -6012
  89. package/src/lexicon/types/app/bsky/actor/defs.ts +57 -1
  90. package/src/lexicon/types/app/bsky/embed/record.ts +2 -0
  91. package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
  92. package/src/lexicon/types/app/bsky/graph/defs.ts +3 -0
  93. package/src/lexicon/types/app/bsky/labeler/defs.ts +93 -0
  94. package/src/lexicon/types/app/bsky/labeler/getServices.ts +51 -0
  95. package/src/lexicon/types/app/bsky/labeler/service.ts +31 -0
  96. package/src/lexicon/types/com/atproto/admin/defs.ts +0 -694
  97. package/src/lexicon/types/com/atproto/label/defs.ts +78 -0
  98. package/src/lexicon/types/{com/atproto/admin/createCommunicationTemplate.ts → tools/ozone/communication/createTemplate.ts} +2 -2
  99. package/src/lexicon/types/tools/ozone/communication/defs.ts +35 -0
  100. package/src/lexicon/types/{com/atproto/admin/listCommunicationTemplates.ts → tools/ozone/communication/listTemplates.ts} +2 -2
  101. package/src/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.ts → tools/ozone/communication/updateTemplate.ts} +2 -2
  102. package/src/lexicon/types/tools/ozone/moderation/defs.ts +641 -0
  103. package/src/lexicon/types/{com/atproto/admin/emitModerationEvent.ts → tools/ozone/moderation/emitEvent.ts} +15 -14
  104. package/src/lexicon/types/{com/atproto/admin/getModerationEvent.ts → tools/ozone/moderation/getEvent.ts} +2 -2
  105. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.ts +2 -2
  106. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.ts +2 -2
  107. package/src/lexicon/types/{com/atproto/admin/queryModerationEvents.ts → tools/ozone/moderation/queryEvents.ts} +3 -3
  108. package/src/lexicon/types/{com/atproto/admin/queryModerationStatuses.ts → tools/ozone/moderation/queryStatuses.ts} +2 -2
  109. package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.ts +2 -2
  110. package/src/mod-service/index.ts +46 -50
  111. package/src/mod-service/lang.ts +1 -1
  112. package/src/mod-service/status.ts +60 -41
  113. package/src/mod-service/subject.ts +1 -1
  114. package/src/mod-service/types.ts +10 -10
  115. package/src/mod-service/util.ts +49 -5
  116. package/src/mod-service/views.ts +45 -18
  117. package/src/sequencer/sequencer.ts +12 -11
  118. package/src/util.ts +21 -0
  119. package/tests/__snapshots__/blob-divert.test.ts.snap +22 -0
  120. package/tests/__snapshots__/get-record.test.ts.snap +14 -6
  121. package/tests/__snapshots__/get-repo.test.ts.snap +7 -3
  122. package/tests/__snapshots__/moderation-events.test.ts.snap +8 -8
  123. package/tests/__snapshots__/moderation-statuses.test.ts.snap +6 -6
  124. package/tests/_util.ts +5 -0
  125. package/tests/blob-divert.test.ts +87 -0
  126. package/tests/communication-templates.test.ts +33 -37
  127. package/tests/db.test.ts +6 -6
  128. package/tests/get-record.test.ts +22 -12
  129. package/tests/get-repo.test.ts +33 -21
  130. package/tests/moderation-appeals.test.ts +39 -67
  131. package/tests/moderation-events.test.ts +99 -142
  132. package/tests/moderation-status-tags.test.ts +20 -37
  133. package/tests/moderation-statuses.test.ts +132 -65
  134. package/tests/moderation.test.ts +147 -301
  135. package/tests/query-labels.test.ts +86 -10
  136. package/tests/repo-search.test.ts +18 -11
  137. package/tests/sequencer.test.ts +6 -3
  138. package/dist/api/admin/util.d.ts +0 -5
  139. package/dist/api/moderation/util.d.ts +0 -4
  140. package/src/api/admin/emitModerationEvent.ts +0 -170
  141. package/src/api/admin/util.ts +0 -54
  142. package/src/api/moderation/util.ts +0 -67
  143. /package/dist/api/{admin/createCommunicationTemplate.d.ts → communication/createTemplate.d.ts} +0 -0
  144. /package/dist/api/{admin/deleteCommunicationTemplate.d.ts → communication/deleteTemplate.d.ts} +0 -0
  145. /package/dist/api/{admin/emitModerationEvent.d.ts → communication/listTemplates.d.ts} +0 -0
  146. /package/dist/api/{admin/getModerationEvent.d.ts → communication/updateTemplate.d.ts} +0 -0
  147. /package/dist/api/{temp → label}/fetchLabels.d.ts +0 -0
  148. /package/dist/api/{admin/getRecord.d.ts → moderation/emitEvent.d.ts} +0 -0
  149. /package/dist/api/{admin/getRepo.d.ts → moderation/getEvent.d.ts} +0 -0
  150. /package/dist/api/{admin/listCommunicationTemplates.d.ts → moderation/getRecord.d.ts} +0 -0
  151. /package/dist/api/{admin/queryModerationEvents.d.ts → moderation/getRepo.d.ts} +0 -0
  152. /package/dist/api/{admin/queryModerationStatuses.d.ts → moderation/queryEvents.d.ts} +0 -0
  153. /package/dist/api/{admin/searchRepos.d.ts → moderation/queryStatuses.d.ts} +0 -0
  154. /package/dist/api/{admin/updateCommunicationTemplate.d.ts → moderation/searchRepos.d.ts} +0 -0
  155. /package/dist/api/{moderation → report}/createReport.d.ts +0 -0
  156. /package/dist/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.d.ts → tools/ozone/communication/deleteTemplate.d.ts} +0 -0
  157. /package/src/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.ts → tools/ozone/communication/deleteTemplate.ts} +0 -0
@@ -7,16 +7,16 @@ type ReqCtx = {
7
7
  req: express.Request
8
8
  }
9
9
 
10
- type RoleOutput = {
10
+ export type AdminTokenOutput = {
11
11
  credentials: {
12
- type: 'role'
13
- isAdmin: boolean
14
- isModerator: boolean
12
+ type: 'admin_token'
13
+ isAdmin: true
14
+ isModerator: true
15
15
  isTriage: true
16
16
  }
17
17
  }
18
18
 
19
- type ModeratorOutput = {
19
+ export type ModeratorOutput = {
20
20
  credentials: {
21
21
  type: 'moderator'
22
22
  aud: string
@@ -51,8 +51,6 @@ export type AuthVerifierOpts = {
51
51
  moderators: string[]
52
52
  triage: string[]
53
53
  adminPassword: string
54
- moderatorPassword: string
55
- triagePassword: string
56
54
  }
57
55
 
58
56
  export class AuthVerifier {
@@ -61,8 +59,6 @@ export class AuthVerifier {
61
59
  moderators: string[]
62
60
  triage: string[]
63
61
  private adminPassword: string
64
- private moderatorPassword: string
65
- private triagePassword: string
66
62
 
67
63
  constructor(public idResolver: IdResolver, opts: AuthVerifierOpts) {
68
64
  this.serviceDid = opts.serviceDid
@@ -70,15 +66,15 @@ export class AuthVerifier {
70
66
  this.moderators = opts.moderators
71
67
  this.triage = opts.triage
72
68
  this.adminPassword = opts.adminPassword
73
- this.moderatorPassword = opts.moderatorPassword
74
- this.triagePassword = opts.triagePassword
75
69
  }
76
70
 
77
- modOrRole = async (reqCtx: ReqCtx): Promise<ModeratorOutput | RoleOutput> => {
78
- if (isBearerToken(reqCtx.req)) {
79
- return this.moderator(reqCtx)
71
+ modOrAdminToken = async (
72
+ reqCtx: ReqCtx,
73
+ ): Promise<ModeratorOutput | AdminTokenOutput> => {
74
+ if (isBasicToken(reqCtx.req)) {
75
+ return this.adminToken(reqCtx)
80
76
  } else {
81
- return this.role(reqCtx)
77
+ return this.moderator(reqCtx)
82
78
  }
83
79
  }
84
80
 
@@ -138,36 +134,30 @@ export class AuthVerifier {
138
134
  return this.nullCreds()
139
135
  }
140
136
 
141
- standardOptionalOrRole = async (
137
+ standardOptionalOrAdminToken = async (
142
138
  reqCtx: ReqCtx,
143
- ): Promise<StandardOutput | RoleOutput | NullOutput> => {
139
+ ): Promise<StandardOutput | AdminTokenOutput | NullOutput> => {
144
140
  if (isBearerToken(reqCtx.req)) {
145
141
  return this.standard(reqCtx)
146
142
  } else if (isBasicToken(reqCtx.req)) {
147
- return this.role(reqCtx)
143
+ return this.adminToken(reqCtx)
148
144
  } else {
149
145
  return this.nullCreds()
150
146
  }
151
147
  }
152
148
 
153
- role = async (reqCtx: ReqCtx): Promise<RoleOutput> => {
149
+ adminToken = async (reqCtx: ReqCtx): Promise<AdminTokenOutput> => {
154
150
  const parsed = parseBasicAuth(reqCtx.req.headers.authorization ?? '')
155
151
  const { username, password } = parsed ?? {}
156
- if (username !== 'admin') {
157
- throw new AuthRequiredError()
158
- }
159
- const isAdmin = password === this.adminPassword
160
- const isModerator = isAdmin || password === this.moderatorPassword
161
- const isTriage = isModerator || password === this.triagePassword
162
- if (!isTriage) {
152
+ if (username !== 'admin' || password !== this.adminPassword) {
163
153
  throw new AuthRequiredError()
164
154
  }
165
155
  return {
166
156
  credentials: {
167
- type: 'role',
168
- isAdmin,
169
- isModerator,
170
- isTriage,
157
+ type: 'admin_token',
158
+ isAdmin: true,
159
+ isModerator: true,
160
+ isTriage: true,
171
161
  },
172
162
  }
173
163
  }
@@ -1,7 +1,7 @@
1
1
  import Database from '../db'
2
2
  import { Selectable } from 'kysely'
3
3
  import { CommunicationTemplate } from '../db/schema/communication_template'
4
- import { CommunicationTemplateView } from '../lexicon/types/com/atproto/admin/defs'
4
+ import { TemplateView } from '../lexicon/types/tools/ozone/communication/defs'
5
5
 
6
6
  export type CommunicationTemplateServiceCreator = (
7
7
  db: Database,
@@ -90,7 +90,7 @@ export class CommunicationTemplateService {
90
90
  .execute()
91
91
  }
92
92
 
93
- view(template: Selectable<CommunicationTemplate>): CommunicationTemplateView {
93
+ view(template: Selectable<CommunicationTemplate>): TemplateView {
94
94
  return {
95
95
  id: `${template.id}`,
96
96
  name: template.name,
@@ -25,18 +25,20 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
25
25
  poolIdleTimeoutMs: env.dbPoolIdleTimeoutMs,
26
26
  }
27
27
 
28
- assert(env.appviewUrl)
29
- assert(env.appviewDid)
28
+ assert(env.appviewUrl && env.appviewDid)
30
29
  const appviewCfg: OzoneConfig['appview'] = {
31
30
  url: env.appviewUrl,
32
31
  did: env.appviewDid,
32
+ pushEvents: !!env.appviewPushEvents,
33
33
  }
34
34
 
35
- assert(env.pdsUrl)
36
- assert(env.pdsDid)
37
- const pdsCfg: OzoneConfig['pds'] = {
38
- url: env.pdsUrl,
39
- did: env.pdsDid,
35
+ let pdsCfg: OzoneConfig['pds'] = null
36
+ if (env.pdsUrl || env.pdsDid) {
37
+ assert(env.pdsUrl && env.pdsDid)
38
+ pdsCfg = {
39
+ url: env.pdsUrl,
40
+ did: env.pdsDid,
41
+ }
40
42
  }
41
43
 
42
44
  const cdnCfg: OzoneConfig['cdn'] = {
@@ -48,6 +50,13 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
48
50
  plcUrl: env.didPlcUrl,
49
51
  }
50
52
 
53
+ const blobDivertServiceCfg =
54
+ env.blobDivertUrl && env.blobDivertAdminPassword
55
+ ? {
56
+ url: env.blobDivertUrl,
57
+ adminPassword: env.blobDivertAdminPassword,
58
+ }
59
+ : null
51
60
  const accessCfg: OzoneConfig['access'] = {
52
61
  admins: env.adminDids,
53
62
  moderators: env.moderatorDids,
@@ -61,6 +70,7 @@ export const envToCfg = (env: OzoneEnvironment): OzoneConfig => {
61
70
  pds: pdsCfg,
62
71
  cdn: cdnCfg,
63
72
  identity: identityCfg,
73
+ blobDivert: blobDivertServiceCfg,
64
74
  access: accessCfg,
65
75
  }
66
76
  }
@@ -72,6 +82,7 @@ export type OzoneConfig = {
72
82
  pds: PdsConfig | null
73
83
  cdn: CdnConfig
74
84
  identity: IdentityConfig
85
+ blobDivert: BlobDivertConfig | null
75
86
  access: AccessConfig
76
87
  }
77
88
 
@@ -83,6 +94,11 @@ export type ServiceConfig = {
83
94
  devMode?: boolean
84
95
  }
85
96
 
97
+ export type BlobDivertConfig = {
98
+ url: string
99
+ adminPassword: string
100
+ }
101
+
86
102
  export type DatabaseConfig = {
87
103
  postgresUrl: string
88
104
  postgresSchema?: string
@@ -94,6 +110,7 @@ export type DatabaseConfig = {
94
110
  export type AppviewConfig = {
95
111
  url: string
96
112
  did: string
113
+ pushEvents: boolean
97
114
  }
98
115
 
99
116
  export type PdsConfig = {
package/src/config/env.ts CHANGED
@@ -10,6 +10,7 @@ export const readEnv = (): OzoneEnvironment => {
10
10
  serverDid: envStr('OZONE_SERVER_DID'),
11
11
  appviewUrl: envStr('OZONE_APPVIEW_URL'),
12
12
  appviewDid: envStr('OZONE_APPVIEW_DID'),
13
+ appviewPushEvents: envBool('OZONE_APPVIEW_PUSH_EVENTS'),
13
14
  pdsUrl: envStr('OZONE_PDS_URL'),
14
15
  pdsDid: envStr('OZONE_PDS_DID'),
15
16
  dbPostgresUrl: envStr('OZONE_DB_POSTGRES_URL'),
@@ -23,9 +24,9 @@ export const readEnv = (): OzoneEnvironment => {
23
24
  moderatorDids: envList('OZONE_MODERATOR_DIDS'),
24
25
  triageDids: envList('OZONE_TRIAGE_DIDS'),
25
26
  adminPassword: envStr('OZONE_ADMIN_PASSWORD'),
26
- moderatorPassword: envStr('OZONE_MODERATOR_PASSWORD'),
27
- triagePassword: envStr('OZONE_TRIAGE_PASSWORD'),
28
27
  signingKeyHex: envStr('OZONE_SIGNING_KEY_HEX'),
28
+ blobDivertUrl: envStr('OZONE_BLOB_DIVERT_URL'),
29
+ blobDivertAdminPassword: envStr('OZONE_BLOB_DIVERT_ADMIN_PASSWORD'),
29
30
  }
30
31
  }
31
32
 
@@ -38,6 +39,7 @@ export type OzoneEnvironment = {
38
39
  serverDid?: string
39
40
  appviewUrl?: string
40
41
  appviewDid?: string
42
+ appviewPushEvents?: boolean
41
43
  pdsUrl?: string
42
44
  pdsDid?: string
43
45
  dbPostgresUrl?: string
@@ -51,7 +53,7 @@ export type OzoneEnvironment = {
51
53
  moderatorDids: string[]
52
54
  triageDids: string[]
53
55
  adminPassword?: string
54
- moderatorPassword?: string
55
- triagePassword?: string
56
56
  signingKeyHex?: string
57
+ blobDivertUrl?: string
58
+ blobDivertAdminPassword?: string
57
59
  }
@@ -3,21 +3,15 @@ import { OzoneEnvironment } from './env'
3
3
 
4
4
  export const envToSecrets = (env: OzoneEnvironment): OzoneSecrets => {
5
5
  assert(env.adminPassword)
6
- assert(env.moderatorPassword)
7
- assert(env.triagePassword)
8
6
  assert(env.signingKeyHex)
9
7
 
10
8
  return {
11
9
  adminPassword: env.adminPassword,
12
- moderatorPassword: env.moderatorPassword,
13
- triagePassword: env.triagePassword,
14
10
  signingKeyHex: env.signingKeyHex,
15
11
  }
16
12
  }
17
13
 
18
14
  export type OzoneSecrets = {
19
15
  adminPassword: string
20
- moderatorPassword: string
21
- triagePassword: string
22
16
  signingKeyHex: string
23
17
  }
package/src/context.ts CHANGED
@@ -14,8 +14,10 @@ import {
14
14
  CommunicationTemplateService,
15
15
  CommunicationTemplateServiceCreator,
16
16
  } from './communication-service/template'
17
+ import { BlobDiverter } from './daemon/blob-diverter'
17
18
  import { AuthVerifier } from './auth-verifier'
18
19
  import { ImageInvalidator } from './image-invalidator'
20
+ import { getSigningKeyId } from './util'
19
21
 
20
22
  export type AppContextOptions = {
21
23
  db: Database
@@ -24,7 +26,9 @@ export type AppContextOptions = {
24
26
  communicationTemplateService: CommunicationTemplateServiceCreator
25
27
  appviewAgent: AtpAgent
26
28
  pdsAgent: AtpAgent | undefined
29
+ blobDiverter?: BlobDiverter
27
30
  signingKey: Keypair
31
+ signingKeyId: number
28
32
  idResolver: IdResolver
29
33
  imgInvalidator?: ImageInvalidator
30
34
  backgroundQueue: BackgroundQueue
@@ -48,11 +52,16 @@ export class AppContext {
48
52
  poolIdleTimeoutMs: cfg.db.poolIdleTimeoutMs,
49
53
  })
50
54
  const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex)
55
+ const signingKeyId = await getSigningKeyId(db, signingKey.did())
51
56
  const appviewAgent = new AtpAgent({ service: cfg.appview.url })
52
57
  const pdsAgent = cfg.pds
53
58
  ? new AtpAgent({ service: cfg.pds.url })
54
59
  : undefined
55
60
 
61
+ const idResolver = new IdResolver({
62
+ plcUrl: cfg.identity.plcUrl,
63
+ })
64
+
56
65
  const createAuthHeaders = (aud: string) =>
57
66
  createServiceAuthHeaders({
58
67
  iss: `${cfg.service.did}#atproto_labeler`,
@@ -61,30 +70,31 @@ export class AppContext {
61
70
  })
62
71
 
63
72
  const backgroundQueue = new BackgroundQueue(db)
73
+ const blobDiverter = cfg.blobDivert
74
+ ? new BlobDiverter(db, {
75
+ idResolver,
76
+ serviceConfig: cfg.blobDivert,
77
+ })
78
+ : undefined
64
79
  const eventPusher = new EventPusher(db, createAuthHeaders, {
65
- appview: cfg.appview,
80
+ appview: cfg.appview.pushEvents ? cfg.appview : undefined,
66
81
  pds: cfg.pds ?? undefined,
67
82
  })
68
-
69
- const idResolver = new IdResolver({
70
- plcUrl: cfg.identity.plcUrl,
71
- })
72
-
73
83
  const modService = ModerationService.creator(
84
+ signingKey,
85
+ signingKeyId,
74
86
  cfg,
75
87
  backgroundQueue,
76
88
  idResolver,
77
89
  eventPusher,
78
90
  appviewAgent,
79
91
  createAuthHeaders,
80
- cfg.service.did,
81
92
  overrides?.imgInvalidator,
82
- cfg.cdn.paths,
83
93
  )
84
94
 
85
95
  const communicationTemplateService = CommunicationTemplateService.creator()
86
96
 
87
- const sequencer = new Sequencer(db)
97
+ const sequencer = new Sequencer(modService(db))
88
98
 
89
99
  const authVerifier = new AuthVerifier(idResolver, {
90
100
  serviceDid: cfg.service.did,
@@ -92,8 +102,6 @@ export class AppContext {
92
102
  moderators: cfg.access.moderators,
93
103
  triage: cfg.access.triage,
94
104
  adminPassword: secrets.adminPassword,
95
- moderatorPassword: secrets.moderatorPassword,
96
- triagePassword: secrets.triagePassword,
97
105
  })
98
106
 
99
107
  return new AppContext(
@@ -105,10 +113,12 @@ export class AppContext {
105
113
  appviewAgent,
106
114
  pdsAgent,
107
115
  signingKey,
116
+ signingKeyId,
108
117
  idResolver,
109
118
  backgroundQueue,
110
119
  sequencer,
111
120
  authVerifier,
121
+ blobDiverter,
112
122
  ...(overrides ?? {}),
113
123
  },
114
124
  secrets,
@@ -135,6 +145,10 @@ export class AppContext {
135
145
  return this.opts.modService
136
146
  }
137
147
 
148
+ get blobDiverter(): BlobDiverter | undefined {
149
+ return this.opts.blobDiverter
150
+ }
151
+
138
152
  get communicationTemplateService(): CommunicationTemplateServiceCreator {
139
153
  return this.opts.communicationTemplateService
140
154
  }
@@ -151,6 +165,10 @@ export class AppContext {
151
165
  return this.opts.signingKey
152
166
  }
153
167
 
168
+ get signingKeyId(): number {
169
+ return this.opts.signingKeyId
170
+ }
171
+
154
172
  get plcClient(): plc.Client {
155
173
  return new plc.Client(this.cfg.identity.plcUrl)
156
174
  }
@@ -190,6 +208,12 @@ export class AppContext {
190
208
  async appviewAuth() {
191
209
  return this.serviceAuthHeaders(this.cfg.appview.did)
192
210
  }
193
- }
194
211
 
212
+ devOverride(overrides: Partial<AppContextOptions>) {
213
+ this.opts = {
214
+ ...this.opts,
215
+ ...overrides,
216
+ }
217
+ }
218
+ }
195
219
  export default AppContext
@@ -0,0 +1,150 @@
1
+ import {
2
+ VerifyCidTransform,
3
+ forwardStreamErrors,
4
+ getPdsEndpoint,
5
+ } from '@atproto/common'
6
+ import { IdResolver } from '@atproto/identity'
7
+ import axios from 'axios'
8
+ import { Readable } from 'stream'
9
+ import { CID } from 'multiformats/cid'
10
+
11
+ import Database from '../db'
12
+ import { retryHttp } from '../util'
13
+ import { BlobDivertConfig } from '../config'
14
+
15
+ export class BlobDiverter {
16
+ serviceConfig: BlobDivertConfig
17
+ idResolver: IdResolver
18
+
19
+ constructor(
20
+ public db: Database,
21
+ services: {
22
+ idResolver: IdResolver
23
+ serviceConfig: BlobDivertConfig
24
+ },
25
+ ) {
26
+ this.serviceConfig = services.serviceConfig
27
+ this.idResolver = services.idResolver
28
+ }
29
+
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),
56
+ }
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
+
77
+ return result.status === 200
78
+ }
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(this.serviceConfig.url)
91
+ url.searchParams.set('did', subjectDid)
92
+ if (subjectUri) url.searchParams.set('uri', subjectUri)
93
+ const result = await this.sendImage({
94
+ url: url.toString(),
95
+ imageStream,
96
+ contentType,
97
+ })
98
+
99
+ return result
100
+ }
101
+
102
+ async uploadBlobOnService({
103
+ subjectDid,
104
+ subjectUri,
105
+ subjectBlobCids,
106
+ }: {
107
+ subjectDid: string
108
+ subjectUri: string
109
+ subjectBlobCids: string[]
110
+ }): Promise<boolean> {
111
+ const didDoc = await this.idResolver.did.resolve(subjectDid)
112
+
113
+ if (!didDoc) {
114
+ throw new Error('Error resolving DID')
115
+ }
116
+
117
+ const pds = getPdsEndpoint(didDoc)
118
+
119
+ if (!pds) {
120
+ throw new Error('Error resolving PDS')
121
+ }
122
+
123
+ // attempt to download and upload within the same retry block since the imageStream is not reusable
124
+ const uploadResult = await Promise.all(
125
+ subjectBlobCids.map((cid) =>
126
+ retryHttp(async () => {
127
+ const { imageStream, contentType } = await this.getBlob({
128
+ pds,
129
+ cid,
130
+ did: subjectDid,
131
+ })
132
+ return this.uploadBlob(
133
+ { imageStream, contentType },
134
+ { subjectDid, subjectUri },
135
+ )
136
+ }),
137
+ ),
138
+ )
139
+
140
+ if (uploadResult.includes(false)) {
141
+ throw new Error(`Error uploading blob ${subjectUri}`)
142
+ }
143
+
144
+ return true
145
+ }
146
+ }
147
+
148
+ const basicAuth = (username: string, password: string) => {
149
+ return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64')
150
+ }
@@ -1,5 +1,6 @@
1
1
  import { Keypair, Secp256k1Keypair } from '@atproto/crypto'
2
2
  import { createServiceAuthHeaders } from '@atproto/xrpc-server'
3
+ import { IdResolver } from '@atproto/identity'
3
4
  import AtpAgent from '@atproto/api'
4
5
  import { OzoneConfig, OzoneSecrets } from '../config'
5
6
  import { Database } from '../db'
@@ -7,7 +8,7 @@ import { EventPusher } from './event-pusher'
7
8
  import { EventReverser } from './event-reverser'
8
9
  import { ModerationService, ModerationServiceCreator } from '../mod-service'
9
10
  import { BackgroundQueue } from '../background'
10
- import { IdResolver } from '@atproto/identity'
11
+ import { getSigningKeyId } from '../util'
11
12
 
12
13
  export type DaemonContextOptions = {
13
14
  db: Database
@@ -31,33 +32,36 @@ export class DaemonContext {
31
32
  schema: cfg.db.postgresSchema,
32
33
  })
33
34
  const signingKey = await Secp256k1Keypair.import(secrets.signingKeyHex)
35
+ const signingKeyId = await getSigningKeyId(db, signingKey.did())
36
+
37
+ const idResolver = new IdResolver({
38
+ plcUrl: cfg.identity.plcUrl,
39
+ })
34
40
 
35
41
  const appviewAgent = new AtpAgent({ service: cfg.appview.url })
36
42
  const createAuthHeaders = (aud: string) =>
37
43
  createServiceAuthHeaders({
38
- iss: cfg.service.did,
44
+ iss: `${cfg.service.did}#atproto_labeler`,
39
45
  aud,
40
46
  keypair: signingKey,
41
47
  })
42
48
 
43
49
  const eventPusher = new EventPusher(db, createAuthHeaders, {
44
- appview: cfg.appview,
50
+ appview: cfg.appview.pushEvents ? cfg.appview : undefined,
45
51
  pds: cfg.pds ?? undefined,
46
52
  })
47
53
 
48
54
  const backgroundQueue = new BackgroundQueue(db)
49
- const idResolver = new IdResolver({
50
- plcUrl: cfg.identity.plcUrl,
51
- })
52
55
 
53
56
  const modService = ModerationService.creator(
57
+ signingKey,
58
+ signingKeyId,
54
59
  cfg,
55
60
  backgroundQueue,
56
61
  idResolver,
57
62
  eventPusher,
58
63
  appviewAgent,
59
64
  createAuthHeaders,
60
- cfg.service.did,
61
65
  )
62
66
 
63
67
  const eventReverser = new EventReverser(db, modService)
@@ -1,10 +1,13 @@
1
+ import assert from 'node:assert'
1
2
  import AtpAgent from '@atproto/api'
2
3
  import { SECOND } from '@atproto/common'
3
4
  import Database from '../db'
5
+ import { RepoPushEventType } from '../db/schema/repo_push_event'
4
6
  import { retryHttp } from '../util'
5
7
  import { dbLogger } from '../logger'
6
8
  import { InputSchema } from '../lexicon/types/com/atproto/admin/updateSubjectStatus'
7
- import assert from 'assert'
9
+ import { BlobPushEvent } from '../db/schema/blob_push_event'
10
+ import { Insertable, Selectable } from 'kysely'
8
11
 
9
12
  type EventSubject = InputSchema['subject']
10
13
 
@@ -74,6 +77,13 @@ export class EventPusher {
74
77
  this.poll(this.blobPollState, () => this.pushBlobEvents())
75
78
  }
76
79
 
80
+ get takedowns(): RepoPushEventType[] {
81
+ const takedowns: RepoPushEventType[] = []
82
+ if (this.pds) takedowns.push('pds_takedown')
83
+ if (this.appview) takedowns.push('appview_takedown')
84
+ return takedowns
85
+ }
86
+
77
87
  poll(state: PollState, fn: () => Promise<void>) {
78
88
  if (this.destroyed) return
79
89
  state.promise = fn()
@@ -277,20 +287,53 @@ export class EventPusher {
277
287
  subject,
278
288
  evt.takedownRef,
279
289
  )
280
- await dbTxn.db
281
- .updateTable('blob_push_event')
282
- .set(
283
- succeeded
284
- ? { confirmedAt: new Date() }
285
- : {
286
- lastAttempted: new Date(),
287
- attempts: (evt.attempts ?? 0) + 1,
288
- },
289
- )
290
- .where('subjectDid', '=', evt.subjectDid)
291
- .where('subjectBlobCid', '=', evt.subjectBlobCid)
292
- .where('eventType', '=', evt.eventType)
293
- .execute()
290
+ await this.markBlobEventAttempt(dbTxn, evt, succeeded)
294
291
  })
295
292
  }
293
+
294
+ async markBlobEventAttempt(
295
+ dbTxn: Database,
296
+ event: Selectable<BlobPushEvent>,
297
+ succeeded: boolean,
298
+ ) {
299
+ await dbTxn.db
300
+ .updateTable('blob_push_event')
301
+ .set(
302
+ succeeded
303
+ ? { confirmedAt: new Date() }
304
+ : {
305
+ lastAttempted: new Date(),
306
+ attempts: (event.attempts ?? 0) + 1,
307
+ },
308
+ )
309
+ .where('subjectDid', '=', event.subjectDid)
310
+ .where('subjectBlobCid', '=', event.subjectBlobCid)
311
+ .where('eventType', '=', event.eventType)
312
+ .execute()
313
+ }
314
+
315
+ async logBlobPushEvent(
316
+ blobValues: Insertable<BlobPushEvent>[],
317
+ takedownRef?: string | null,
318
+ ) {
319
+ return this.db.db
320
+ .insertInto('blob_push_event')
321
+ .values(blobValues)
322
+ .onConflict((oc) =>
323
+ oc.columns(['subjectDid', 'subjectBlobCid', 'eventType']).doUpdateSet({
324
+ takedownRef,
325
+ confirmedAt: null,
326
+ attempts: 0,
327
+ lastAttempted: null,
328
+ }),
329
+ )
330
+ .returning([
331
+ 'id',
332
+ 'subjectDid',
333
+ 'subjectUri',
334
+ 'subjectBlobCid',
335
+ 'eventType',
336
+ ])
337
+ .execute()
338
+ }
296
339
  }