@atproto/ozone 0.0.15 → 0.0.16
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/CHANGELOG.md +9 -0
- package/dist/api/proxied.d.ts +3 -0
- package/dist/auth-verifier.d.ts +72 -0
- package/dist/config/config.d.ts +9 -2
- package/dist/config/env.d.ts +4 -0
- package/dist/context.d.ts +3 -90
- package/dist/db/index.js.map +2 -2
- package/dist/index.js +29669 -29467
- package/dist/index.js.map +3 -3
- package/dist/lexicon/lexicons.d.ts +5 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -1
- package/dist/lexicon/types/com/atproto/admin/defs.d.ts +1 -0
- package/dist/mod-service/index.d.ts +16 -7
- package/dist/mod-service/views.d.ts +3 -3
- package/package.json +5 -5
- package/src/api/admin/createCommunicationTemplate.ts +2 -2
- package/src/api/admin/deleteCommunicationTemplate.ts +2 -2
- package/src/api/admin/emitModerationEvent.ts +23 -4
- package/src/api/admin/getModerationEvent.ts +1 -1
- package/src/api/admin/getRecord.ts +2 -2
- package/src/api/admin/getRepo.ts +2 -2
- package/src/api/admin/listCommunicationTemplates.ts +2 -2
- package/src/api/admin/queryModerationEvents.ts +1 -1
- package/src/api/admin/queryModerationStatuses.ts +1 -1
- package/src/api/admin/searchRepos.ts +1 -1
- package/src/api/admin/updateCommunicationTemplate.ts +2 -2
- package/src/api/index.ts +2 -0
- package/src/api/moderation/createReport.ts +2 -2
- package/src/api/proxied.ts +116 -0
- package/src/api/temp/fetchLabels.ts +2 -2
- package/src/api/well-known.ts +3 -3
- package/src/auth-verifier.ts +218 -0
- package/src/config/config.ts +18 -2
- package/src/config/env.ts +9 -1
- package/src/context.ts +24 -38
- package/src/daemon/context.ts +10 -4
- package/src/daemon/event-pusher.ts +3 -3
- package/src/lexicon/lexicons.ts +5 -0
- package/src/lexicon/types/app/bsky/actor/defs.ts +1 -1
- package/src/lexicon/types/com/atproto/admin/defs.ts +2 -0
- package/src/mod-service/index.ts +64 -7
- package/src/mod-service/views.ts +22 -18
- package/tests/moderation-events.test.ts +49 -0
- package/dist/auth.d.ts +0 -81
- package/src/auth.ts +0 -147
package/src/mod-service/views.ts
CHANGED
|
@@ -25,36 +25,39 @@ import {
|
|
|
25
25
|
import { REASONOTHER } from '../lexicon/types/com/atproto/moderation/defs'
|
|
26
26
|
import { subjectFromEventRow, subjectFromStatusRow } from './subject'
|
|
27
27
|
import { formatLabel } from './util'
|
|
28
|
+
import { httpLogger as log } from '../logger'
|
|
28
29
|
|
|
29
|
-
export type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
| undefined
|
|
36
|
-
>
|
|
30
|
+
export type AuthHeaders = {
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: string
|
|
33
|
+
}
|
|
34
|
+
}
|
|
37
35
|
|
|
38
36
|
export class ModerationViews {
|
|
39
37
|
constructor(
|
|
40
38
|
private db: Database,
|
|
41
39
|
private appviewAgent: AtpAgent,
|
|
42
|
-
private appviewAuth:
|
|
40
|
+
private appviewAuth: () => Promise<AuthHeaders>,
|
|
43
41
|
) {}
|
|
44
42
|
|
|
45
43
|
async getAccoutInfosByDid(dids: string[]): Promise<Map<string, AccountView>> {
|
|
46
44
|
if (dids.length === 0) return new Map()
|
|
47
45
|
const auth = await this.appviewAuth()
|
|
48
46
|
if (!auth) return new Map()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
|
|
47
|
+
try {
|
|
48
|
+
const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos(
|
|
49
|
+
{
|
|
50
|
+
dids: dedupeStrs(dids),
|
|
51
|
+
},
|
|
52
|
+
auth,
|
|
53
|
+
)
|
|
54
|
+
return res.data.infos.reduce((acc, cur) => {
|
|
55
|
+
return acc.set(cur.did, cur)
|
|
56
|
+
}, new Map<string, AccountView>())
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.error({ err, dids }, 'failed to resolve account infos from appview')
|
|
59
|
+
return new Map()
|
|
60
|
+
}
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
async repos(dids: string[]): Promise<Map<string, RepoView>> {
|
|
@@ -154,6 +157,7 @@ export class ModerationViews {
|
|
|
154
157
|
eventView.event = {
|
|
155
158
|
...eventView.event,
|
|
156
159
|
subjectLine: event.meta?.subjectLine ?? '',
|
|
160
|
+
content: event.meta?.content,
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
+
import EventEmitter, { once } from 'node:events'
|
|
3
|
+
import Mail from 'nodemailer/lib/mailer'
|
|
2
4
|
import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
|
|
3
5
|
import AtpAgent, {
|
|
4
6
|
ComAtprotoAdminDefs,
|
|
@@ -422,4 +424,51 @@ describe('moderation-events', () => {
|
|
|
422
424
|
})
|
|
423
425
|
})
|
|
424
426
|
})
|
|
427
|
+
|
|
428
|
+
describe('email event', () => {
|
|
429
|
+
let sendMailOriginal
|
|
430
|
+
const mailCatcher = new EventEmitter()
|
|
431
|
+
const getMailFrom = async (promise): Promise<Mail.Options> => {
|
|
432
|
+
const result = await Promise.all([once(mailCatcher, 'mail'), promise])
|
|
433
|
+
return result[0][0]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
beforeAll(() => {
|
|
437
|
+
const mailer = network.pds.ctx.moderationMailer
|
|
438
|
+
// Catch emails for use in tests
|
|
439
|
+
sendMailOriginal = mailer.transporter.sendMail
|
|
440
|
+
mailer.transporter.sendMail = async (opts) => {
|
|
441
|
+
const result = await sendMailOriginal.call(mailer.transporter, opts)
|
|
442
|
+
mailCatcher.emit('mail', opts)
|
|
443
|
+
return result
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
afterAll(() => {
|
|
448
|
+
network.pds.ctx.moderationMailer.transporter.sendMail = sendMailOriginal
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('sends email via pds.', async () => {
|
|
452
|
+
const mail = await getMailFrom(
|
|
453
|
+
emitModerationEvent({
|
|
454
|
+
event: {
|
|
455
|
+
$type: 'com.atproto.admin.defs#modEventEmail',
|
|
456
|
+
comment: 'Reaching out to Alice',
|
|
457
|
+
subjectLine: 'Hello',
|
|
458
|
+
content: 'Hey Alice, how are you?',
|
|
459
|
+
},
|
|
460
|
+
subject: {
|
|
461
|
+
$type: 'com.atproto.admin.defs#repoRef',
|
|
462
|
+
did: sc.dids.alice,
|
|
463
|
+
},
|
|
464
|
+
createdBy: sc.dids.bob,
|
|
465
|
+
}),
|
|
466
|
+
)
|
|
467
|
+
expect(mail).toEqual({
|
|
468
|
+
to: 'alice@test.com',
|
|
469
|
+
subject: 'Hello',
|
|
470
|
+
html: 'Hey Alice, how are you?',
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
})
|
|
425
474
|
})
|
package/dist/auth.d.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { IdResolver } from '@atproto/identity';
|
|
3
|
-
import { OzoneSecrets } from './config';
|
|
4
|
-
export declare const authVerifier: (idResolver: IdResolver, opts: {
|
|
5
|
-
aud: string | null;
|
|
6
|
-
}) => (reqCtx: {
|
|
7
|
-
req: express.Request;
|
|
8
|
-
res: express.Response;
|
|
9
|
-
}) => Promise<{
|
|
10
|
-
credentials: {
|
|
11
|
-
did: string;
|
|
12
|
-
};
|
|
13
|
-
artifacts: {
|
|
14
|
-
aud: string | null;
|
|
15
|
-
};
|
|
16
|
-
}>;
|
|
17
|
-
export declare const authOptionalVerifier: (idResolver: IdResolver, opts: {
|
|
18
|
-
aud: string | null;
|
|
19
|
-
}) => (reqCtx: {
|
|
20
|
-
req: express.Request;
|
|
21
|
-
res: express.Response;
|
|
22
|
-
}) => Promise<{
|
|
23
|
-
credentials: {
|
|
24
|
-
did: string;
|
|
25
|
-
};
|
|
26
|
-
artifacts: {
|
|
27
|
-
aud: string | null;
|
|
28
|
-
};
|
|
29
|
-
} | {
|
|
30
|
-
credentials: {
|
|
31
|
-
did: null;
|
|
32
|
-
};
|
|
33
|
-
}>;
|
|
34
|
-
export declare const authOptionalAccessOrRoleVerifier: (idResolver: IdResolver, secrets: OzoneSecrets, serverDid: string) => (ctx: {
|
|
35
|
-
req: express.Request;
|
|
36
|
-
res: express.Response;
|
|
37
|
-
}) => Promise<{
|
|
38
|
-
credentials: {
|
|
39
|
-
did: null;
|
|
40
|
-
type: "unauthed";
|
|
41
|
-
};
|
|
42
|
-
} | {
|
|
43
|
-
credentials: {
|
|
44
|
-
valid: boolean;
|
|
45
|
-
admin: boolean;
|
|
46
|
-
moderator: boolean;
|
|
47
|
-
triage: boolean;
|
|
48
|
-
type: "role";
|
|
49
|
-
};
|
|
50
|
-
} | {
|
|
51
|
-
credentials: {
|
|
52
|
-
did: string;
|
|
53
|
-
type: "access";
|
|
54
|
-
};
|
|
55
|
-
artifacts: {
|
|
56
|
-
aud: string | null;
|
|
57
|
-
};
|
|
58
|
-
}>;
|
|
59
|
-
export declare const roleVerifier: (secrets: OzoneSecrets) => (reqCtx: {
|
|
60
|
-
req: express.Request;
|
|
61
|
-
res: express.Response;
|
|
62
|
-
}) => Promise<{
|
|
63
|
-
credentials: {
|
|
64
|
-
valid: boolean;
|
|
65
|
-
admin: boolean;
|
|
66
|
-
moderator: boolean;
|
|
67
|
-
triage: boolean;
|
|
68
|
-
};
|
|
69
|
-
}>;
|
|
70
|
-
export declare const getRoleCredentials: (secrets: OzoneSecrets, req: express.Request) => {
|
|
71
|
-
valid: boolean;
|
|
72
|
-
admin: boolean;
|
|
73
|
-
moderator: boolean;
|
|
74
|
-
triage: boolean;
|
|
75
|
-
};
|
|
76
|
-
export declare const parseBasicAuth: (token: string) => {
|
|
77
|
-
username: string;
|
|
78
|
-
password: string;
|
|
79
|
-
} | null;
|
|
80
|
-
export declare const buildBasicAuth: (username: string, password: string) => string;
|
|
81
|
-
export declare const getJwtStrFromReq: (req: express.Request) => string | null;
|
package/src/auth.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import express from 'express'
|
|
2
|
-
import * as uint8arrays from 'uint8arrays'
|
|
3
|
-
import { AuthRequiredError, verifyJwt } from '@atproto/xrpc-server'
|
|
4
|
-
import { IdResolver } from '@atproto/identity'
|
|
5
|
-
import { OzoneSecrets } from './config'
|
|
6
|
-
|
|
7
|
-
const BASIC = 'Basic '
|
|
8
|
-
const BEARER = 'Bearer '
|
|
9
|
-
|
|
10
|
-
export const authVerifier = (
|
|
11
|
-
idResolver: IdResolver,
|
|
12
|
-
opts: { aud: string | null },
|
|
13
|
-
) => {
|
|
14
|
-
const getSigningKey = async (
|
|
15
|
-
did: string,
|
|
16
|
-
forceRefresh: boolean,
|
|
17
|
-
): Promise<string> => {
|
|
18
|
-
const atprotoData = await idResolver.did.resolveAtprotoData(
|
|
19
|
-
did,
|
|
20
|
-
forceRefresh,
|
|
21
|
-
)
|
|
22
|
-
return atprotoData.signingKey
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return async (reqCtx: { req: express.Request; res: express.Response }) => {
|
|
26
|
-
const jwtStr = getJwtStrFromReq(reqCtx.req)
|
|
27
|
-
if (!jwtStr) {
|
|
28
|
-
throw new AuthRequiredError('missing jwt', 'MissingJwt')
|
|
29
|
-
}
|
|
30
|
-
const payload = await verifyJwt(jwtStr, opts.aud, getSigningKey)
|
|
31
|
-
return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const authOptionalVerifier = (
|
|
36
|
-
idResolver: IdResolver,
|
|
37
|
-
opts: { aud: string | null },
|
|
38
|
-
) => {
|
|
39
|
-
const verifyAccess = authVerifier(idResolver, opts)
|
|
40
|
-
return async (reqCtx: { req: express.Request; res: express.Response }) => {
|
|
41
|
-
if (!reqCtx.req.headers.authorization) {
|
|
42
|
-
return { credentials: { did: null } }
|
|
43
|
-
}
|
|
44
|
-
return verifyAccess(reqCtx)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const authOptionalAccessOrRoleVerifier = (
|
|
49
|
-
idResolver: IdResolver,
|
|
50
|
-
secrets: OzoneSecrets,
|
|
51
|
-
serverDid: string,
|
|
52
|
-
) => {
|
|
53
|
-
const verifyAccess = authVerifier(idResolver, { aud: serverDid })
|
|
54
|
-
const verifyRole = roleVerifier(secrets)
|
|
55
|
-
return async (ctx: { req: express.Request; res: express.Response }) => {
|
|
56
|
-
const defaultUnAuthorizedCredentials = {
|
|
57
|
-
credentials: { did: null, type: 'unauthed' as const },
|
|
58
|
-
}
|
|
59
|
-
if (!ctx.req.headers.authorization) {
|
|
60
|
-
return defaultUnAuthorizedCredentials
|
|
61
|
-
}
|
|
62
|
-
// For non-admin tokens, we don't want to consider alternative verifiers and let it fail if it fails
|
|
63
|
-
const isRoleAuthToken = ctx.req.headers.authorization?.startsWith(BASIC)
|
|
64
|
-
if (isRoleAuthToken) {
|
|
65
|
-
const result = await verifyRole(ctx)
|
|
66
|
-
return {
|
|
67
|
-
...result,
|
|
68
|
-
credentials: {
|
|
69
|
-
type: 'role' as const,
|
|
70
|
-
...result.credentials,
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const result = await verifyAccess(ctx)
|
|
75
|
-
return {
|
|
76
|
-
...result,
|
|
77
|
-
credentials: {
|
|
78
|
-
type: 'access' as const,
|
|
79
|
-
...result.credentials,
|
|
80
|
-
},
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export const roleVerifier =
|
|
86
|
-
(secrets: OzoneSecrets) =>
|
|
87
|
-
async (reqCtx: { req: express.Request; res: express.Response }) => {
|
|
88
|
-
const credentials = getRoleCredentials(secrets, reqCtx.req)
|
|
89
|
-
if (!credentials.valid) {
|
|
90
|
-
throw new AuthRequiredError()
|
|
91
|
-
}
|
|
92
|
-
return { credentials }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const getRoleCredentials = (
|
|
96
|
-
secrets: OzoneSecrets,
|
|
97
|
-
req: express.Request,
|
|
98
|
-
) => {
|
|
99
|
-
const parsed = parseBasicAuth(req.headers.authorization || '')
|
|
100
|
-
const { username, password } = parsed ?? {}
|
|
101
|
-
if (username === 'admin' && password === secrets.triagePassword) {
|
|
102
|
-
return { valid: true, admin: false, moderator: false, triage: true }
|
|
103
|
-
}
|
|
104
|
-
if (username === 'admin' && password === secrets.moderatorPassword) {
|
|
105
|
-
return { valid: true, admin: false, moderator: true, triage: true }
|
|
106
|
-
}
|
|
107
|
-
if (username === 'admin' && password === secrets.adminPassword) {
|
|
108
|
-
return { valid: true, admin: true, moderator: true, triage: true }
|
|
109
|
-
}
|
|
110
|
-
return { valid: false, admin: false, moderator: false, triage: false }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export const parseBasicAuth = (
|
|
114
|
-
token: string,
|
|
115
|
-
): { username: string; password: string } | null => {
|
|
116
|
-
if (!token.startsWith(BASIC)) return null
|
|
117
|
-
const b64 = token.slice(BASIC.length)
|
|
118
|
-
let parsed: string[]
|
|
119
|
-
try {
|
|
120
|
-
parsed = uint8arrays
|
|
121
|
-
.toString(uint8arrays.fromString(b64, 'base64pad'), 'utf8')
|
|
122
|
-
.split(':')
|
|
123
|
-
} catch (err) {
|
|
124
|
-
return null
|
|
125
|
-
}
|
|
126
|
-
const [username, password] = parsed
|
|
127
|
-
if (!username || !password) return null
|
|
128
|
-
return { username, password }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export const buildBasicAuth = (username: string, password: string): string => {
|
|
132
|
-
return (
|
|
133
|
-
BASIC +
|
|
134
|
-
uint8arrays.toString(
|
|
135
|
-
uint8arrays.fromString(`${username}:${password}`, 'utf8'),
|
|
136
|
-
'base64pad',
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export const getJwtStrFromReq = (req: express.Request): string | null => {
|
|
142
|
-
const { authorization } = req.headers
|
|
143
|
-
if (!authorization?.startsWith(BEARER)) {
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
146
|
-
return authorization.slice(BEARER.length).trim()
|
|
147
|
-
}
|