@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.
- package/dist/api/util.d.ts +10 -0
- package/dist/auth-verifier.d.ts +8 -12
- package/dist/communication-service/template.d.ts +2 -2
- package/dist/config/config.d.ts +6 -0
- package/dist/config/env.d.ts +3 -2
- package/dist/config/secrets.d.ts +0 -2
- package/dist/context.d.ts +6 -0
- package/dist/daemon/blob-diverter.d.ts +26 -0
- package/dist/daemon/event-pusher.d.ts +6 -0
- package/dist/daemon/index.d.ts +1 -0
- package/dist/db/index.js +21 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20240228T003647759Z-add-label-sigs.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/schema/index.d.ts +2 -1
- package/dist/db/schema/label.d.ts +4 -0
- package/dist/db/schema/moderation_event.d.ts +1 -1
- package/dist/db/schema/moderation_subject_status.d.ts +2 -2
- package/dist/db/schema/signing_key.d.ts +9 -0
- package/dist/index.js +10400 -10313
- package/dist/index.js.map +3 -3
- package/dist/lexicon/index.d.ts +55 -27
- package/dist/lexicon/lexicons.d.ts +5046 -4757
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +23 -1
- package/dist/lexicon/types/app/bsky/embed/record.d.ts +2 -1
- package/dist/lexicon/types/app/bsky/feed/defs.d.ts +1 -0
- package/dist/lexicon/types/app/bsky/graph/defs.d.ts +3 -0
- package/dist/lexicon/types/app/bsky/labeler/defs.d.ts +41 -0
- package/dist/lexicon/types/app/bsky/labeler/getServices.d.ts +36 -0
- package/dist/lexicon/types/app/bsky/labeler/service.d.ts +14 -0
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +0 -304
- package/dist/lexicon/types/com/atproto/label/defs.d.ts +23 -0
- package/dist/lexicon/types/{com/atproto/admin/createCommunicationTemplate.d.ts → tools/ozone/communication/createTemplate.d.ts} +2 -2
- package/dist/lexicon/types/tools/ozone/communication/defs.d.ts +14 -0
- package/dist/lexicon/types/{com/atproto/admin/listCommunicationTemplates.d.ts → tools/ozone/communication/listTemplates.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.d.ts → tools/ozone/communication/updateTemplate.d.ts} +2 -2
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +269 -0
- package/dist/lexicon/types/{com/atproto/admin/emitModerationEvent.d.ts → tools/ozone/moderation/emitEvent.d.ts} +5 -4
- package/dist/lexicon/types/{com/atproto/admin/getModerationEvent.d.ts → tools/ozone/moderation/getEvent.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.d.ts +2 -2
- package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.d.ts +2 -2
- package/dist/lexicon/types/{com/atproto/admin/queryModerationEvents.d.ts → tools/ozone/moderation/queryEvents.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin/queryModerationStatuses.d.ts → tools/ozone/moderation/queryStatuses.d.ts} +2 -2
- package/dist/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.d.ts +2 -2
- package/dist/mod-service/index.d.ts +16 -15
- package/dist/mod-service/subject.d.ts +1 -1
- package/dist/mod-service/types.d.ts +2 -2
- package/dist/mod-service/util.d.ts +6 -0
- package/dist/mod-service/views.d.ts +9 -3
- package/dist/sequencer/sequencer.d.ts +6 -4
- package/dist/util.d.ts +2 -0
- package/package.json +9 -8
- package/src/api/{admin/createCommunicationTemplate.ts → communication/createTemplate.ts} +2 -2
- package/src/api/{admin/deleteCommunicationTemplate.ts → communication/deleteTemplate.ts} +2 -2
- package/src/api/{admin/listCommunicationTemplates.ts → communication/listTemplates.ts} +2 -2
- package/src/api/{admin/updateCommunicationTemplate.ts → communication/updateTemplate.ts} +2 -2
- package/src/api/index.ts +21 -21
- package/src/api/{temp → label}/fetchLabels.ts +5 -3
- package/src/api/label/queryLabels.ts +4 -2
- package/src/api/moderation/emitEvent.ts +218 -0
- package/src/api/{admin/getModerationEvent.ts → moderation/getEvent.ts} +2 -2
- package/src/api/{admin → moderation}/getRecord.ts +3 -3
- package/src/api/{admin → moderation}/getRepo.ts +3 -3
- package/src/api/{admin/queryModerationEvents.ts → moderation/queryEvents.ts} +3 -3
- package/src/api/{admin/queryModerationStatuses.ts → moderation/queryStatuses.ts} +3 -3
- package/src/api/{admin → moderation}/searchRepos.ts +2 -2
- package/src/api/proxied.ts +8 -8
- package/src/api/{moderation → report}/createReport.ts +2 -3
- package/src/api/util.ts +119 -0
- package/src/auth-verifier.ts +20 -30
- package/src/communication-service/template.ts +2 -2
- package/src/config/config.ts +24 -7
- package/src/config/env.ts +6 -4
- package/src/config/secrets.ts +0 -6
- package/src/context.ts +36 -12
- package/src/daemon/blob-diverter.ts +150 -0
- package/src/daemon/context.ts +11 -7
- package/src/daemon/event-pusher.ts +58 -15
- package/src/daemon/index.ts +1 -0
- package/src/db/migrations/20240228T003647759Z-add-label-sigs.ts +25 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/index.ts +2 -0
- package/src/db/schema/label.ts +3 -0
- package/src/db/schema/moderation_event.ts +11 -11
- package/src/db/schema/moderation_subject_status.ts +7 -2
- package/src/db/schema/signing_key.ts +10 -0
- package/src/lexicon/index.ts +200 -137
- package/src/lexicon/lexicons.ts +6310 -6012
- package/src/lexicon/types/app/bsky/actor/defs.ts +57 -1
- package/src/lexicon/types/app/bsky/embed/record.ts +2 -0
- package/src/lexicon/types/app/bsky/feed/defs.ts +1 -0
- package/src/lexicon/types/app/bsky/graph/defs.ts +3 -0
- package/src/lexicon/types/app/bsky/labeler/defs.ts +93 -0
- package/src/lexicon/types/app/bsky/labeler/getServices.ts +51 -0
- package/src/lexicon/types/app/bsky/labeler/service.ts +31 -0
- package/src/lexicon/types/com/atproto/admin/defs.ts +0 -694
- package/src/lexicon/types/com/atproto/label/defs.ts +78 -0
- package/src/lexicon/types/{com/atproto/admin/createCommunicationTemplate.ts → tools/ozone/communication/createTemplate.ts} +2 -2
- package/src/lexicon/types/tools/ozone/communication/defs.ts +35 -0
- package/src/lexicon/types/{com/atproto/admin/listCommunicationTemplates.ts → tools/ozone/communication/listTemplates.ts} +2 -2
- package/src/lexicon/types/{com/atproto/admin/updateCommunicationTemplate.ts → tools/ozone/communication/updateTemplate.ts} +2 -2
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +641 -0
- package/src/lexicon/types/{com/atproto/admin/emitModerationEvent.ts → tools/ozone/moderation/emitEvent.ts} +15 -14
- package/src/lexicon/types/{com/atproto/admin/getModerationEvent.ts → tools/ozone/moderation/getEvent.ts} +2 -2
- package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRecord.ts +2 -2
- package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/getRepo.ts +2 -2
- package/src/lexicon/types/{com/atproto/admin/queryModerationEvents.ts → tools/ozone/moderation/queryEvents.ts} +3 -3
- package/src/lexicon/types/{com/atproto/admin/queryModerationStatuses.ts → tools/ozone/moderation/queryStatuses.ts} +2 -2
- package/src/lexicon/types/{com/atproto/admin → tools/ozone/moderation}/searchRepos.ts +2 -2
- package/src/mod-service/index.ts +46 -50
- package/src/mod-service/lang.ts +1 -1
- package/src/mod-service/status.ts +60 -41
- package/src/mod-service/subject.ts +1 -1
- package/src/mod-service/types.ts +10 -10
- package/src/mod-service/util.ts +49 -5
- package/src/mod-service/views.ts +45 -18
- package/src/sequencer/sequencer.ts +12 -11
- package/src/util.ts +21 -0
- package/tests/__snapshots__/blob-divert.test.ts.snap +22 -0
- package/tests/__snapshots__/get-record.test.ts.snap +14 -6
- package/tests/__snapshots__/get-repo.test.ts.snap +7 -3
- package/tests/__snapshots__/moderation-events.test.ts.snap +8 -8
- package/tests/__snapshots__/moderation-statuses.test.ts.snap +6 -6
- package/tests/_util.ts +5 -0
- package/tests/blob-divert.test.ts +87 -0
- package/tests/communication-templates.test.ts +33 -37
- package/tests/db.test.ts +6 -6
- package/tests/get-record.test.ts +22 -12
- package/tests/get-repo.test.ts +33 -21
- package/tests/moderation-appeals.test.ts +39 -67
- package/tests/moderation-events.test.ts +99 -142
- package/tests/moderation-status-tags.test.ts +20 -37
- package/tests/moderation-statuses.test.ts +132 -65
- package/tests/moderation.test.ts +147 -301
- package/tests/query-labels.test.ts +86 -10
- package/tests/repo-search.test.ts +18 -11
- package/tests/sequencer.test.ts +6 -3
- package/dist/api/admin/util.d.ts +0 -5
- package/dist/api/moderation/util.d.ts +0 -4
- package/src/api/admin/emitModerationEvent.ts +0 -170
- package/src/api/admin/util.ts +0 -54
- package/src/api/moderation/util.ts +0 -67
- /package/dist/api/{admin/createCommunicationTemplate.d.ts → communication/createTemplate.d.ts} +0 -0
- /package/dist/api/{admin/deleteCommunicationTemplate.d.ts → communication/deleteTemplate.d.ts} +0 -0
- /package/dist/api/{admin/emitModerationEvent.d.ts → communication/listTemplates.d.ts} +0 -0
- /package/dist/api/{admin/getModerationEvent.d.ts → communication/updateTemplate.d.ts} +0 -0
- /package/dist/api/{temp → label}/fetchLabels.d.ts +0 -0
- /package/dist/api/{admin/getRecord.d.ts → moderation/emitEvent.d.ts} +0 -0
- /package/dist/api/{admin/getRepo.d.ts → moderation/getEvent.d.ts} +0 -0
- /package/dist/api/{admin/listCommunicationTemplates.d.ts → moderation/getRecord.d.ts} +0 -0
- /package/dist/api/{admin/queryModerationEvents.d.ts → moderation/getRepo.d.ts} +0 -0
- /package/dist/api/{admin/queryModerationStatuses.d.ts → moderation/queryEvents.d.ts} +0 -0
- /package/dist/api/{admin/searchRepos.d.ts → moderation/queryStatuses.d.ts} +0 -0
- /package/dist/api/{admin/updateCommunicationTemplate.d.ts → moderation/searchRepos.d.ts} +0 -0
- /package/dist/api/{moderation → report}/createReport.d.ts +0 -0
- /package/dist/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.d.ts → tools/ozone/communication/deleteTemplate.d.ts} +0 -0
- /package/src/lexicon/types/{com/atproto/admin/deleteCommunicationTemplate.ts → tools/ozone/communication/deleteTemplate.ts} +0 -0
package/src/auth-verifier.ts
CHANGED
|
@@ -7,16 +7,16 @@ type ReqCtx = {
|
|
|
7
7
|
req: express.Request
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
type
|
|
10
|
+
export type AdminTokenOutput = {
|
|
11
11
|
credentials: {
|
|
12
|
-
type: '
|
|
13
|
-
isAdmin:
|
|
14
|
-
isModerator:
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
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
|
-
|
|
137
|
+
standardOptionalOrAdminToken = async (
|
|
142
138
|
reqCtx: ReqCtx,
|
|
143
|
-
): Promise<StandardOutput |
|
|
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.
|
|
143
|
+
return this.adminToken(reqCtx)
|
|
148
144
|
} else {
|
|
149
145
|
return this.nullCreds()
|
|
150
146
|
}
|
|
151
147
|
}
|
|
152
148
|
|
|
153
|
-
|
|
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: '
|
|
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 {
|
|
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>):
|
|
93
|
+
view(template: Selectable<CommunicationTemplate>): TemplateView {
|
|
94
94
|
return {
|
|
95
95
|
id: `${template.id}`,
|
|
96
96
|
name: template.name,
|
package/src/config/config.ts
CHANGED
|
@@ -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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
}
|
package/src/config/secrets.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/daemon/context.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
}
|