@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api/proxied.d.ts +3 -0
  3. package/dist/auth-verifier.d.ts +72 -0
  4. package/dist/config/config.d.ts +9 -2
  5. package/dist/config/env.d.ts +4 -0
  6. package/dist/context.d.ts +3 -90
  7. package/dist/db/index.js.map +2 -2
  8. package/dist/index.js +29669 -29467
  9. package/dist/index.js.map +3 -3
  10. package/dist/lexicon/lexicons.d.ts +5 -0
  11. package/dist/lexicon/types/app/bsky/actor/defs.d.ts +1 -1
  12. package/dist/lexicon/types/com/atproto/admin/defs.d.ts +1 -0
  13. package/dist/mod-service/index.d.ts +16 -7
  14. package/dist/mod-service/views.d.ts +3 -3
  15. package/package.json +5 -5
  16. package/src/api/admin/createCommunicationTemplate.ts +2 -2
  17. package/src/api/admin/deleteCommunicationTemplate.ts +2 -2
  18. package/src/api/admin/emitModerationEvent.ts +23 -4
  19. package/src/api/admin/getModerationEvent.ts +1 -1
  20. package/src/api/admin/getRecord.ts +2 -2
  21. package/src/api/admin/getRepo.ts +2 -2
  22. package/src/api/admin/listCommunicationTemplates.ts +2 -2
  23. package/src/api/admin/queryModerationEvents.ts +1 -1
  24. package/src/api/admin/queryModerationStatuses.ts +1 -1
  25. package/src/api/admin/searchRepos.ts +1 -1
  26. package/src/api/admin/updateCommunicationTemplate.ts +2 -2
  27. package/src/api/index.ts +2 -0
  28. package/src/api/moderation/createReport.ts +2 -2
  29. package/src/api/proxied.ts +116 -0
  30. package/src/api/temp/fetchLabels.ts +2 -2
  31. package/src/api/well-known.ts +3 -3
  32. package/src/auth-verifier.ts +218 -0
  33. package/src/config/config.ts +18 -2
  34. package/src/config/env.ts +9 -1
  35. package/src/context.ts +24 -38
  36. package/src/daemon/context.ts +10 -4
  37. package/src/daemon/event-pusher.ts +3 -3
  38. package/src/lexicon/lexicons.ts +5 -0
  39. package/src/lexicon/types/app/bsky/actor/defs.ts +1 -1
  40. package/src/lexicon/types/com/atproto/admin/defs.ts +2 -0
  41. package/src/mod-service/index.ts +64 -7
  42. package/src/mod-service/views.ts +22 -18
  43. package/tests/moderation-events.test.ts +49 -0
  44. package/dist/auth.d.ts +0 -81
  45. package/src/auth.ts +0 -147
@@ -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 AppviewAuth = () => Promise<
30
- | {
31
- headers: {
32
- authorization: string
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: 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
- const res = await this.appviewAgent.api.com.atproto.admin.getAccountInfos(
50
- {
51
- dids: dedupeStrs(dids),
52
- },
53
- auth,
54
- )
55
- return res.data.infos.reduce((acc, cur) => {
56
- return acc.set(cur.did, cur)
57
- }, new Map<string, AccountView>())
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
- }