@atproto/ozone 0.1.126 → 0.1.128

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/CHANGELOG.md +18 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +10 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  6. package/dist/api/moderation/emitEvent.js +2 -0
  7. package/dist/api/moderation/emitEvent.js.map +1 -1
  8. package/dist/api/moderation/queryEvents.d.ts.map +1 -1
  9. package/dist/api/moderation/queryEvents.js +2 -1
  10. package/dist/api/moderation/queryEvents.js.map +1 -1
  11. package/dist/api/report/createReport.d.ts.map +1 -1
  12. package/dist/api/report/createReport.js +2 -1
  13. package/dist/api/report/createReport.js.map +1 -1
  14. package/dist/api/safelink/addRule.d.ts +4 -0
  15. package/dist/api/safelink/addRule.d.ts.map +1 -0
  16. package/dist/api/safelink/addRule.js +37 -0
  17. package/dist/api/safelink/addRule.js.map +1 -0
  18. package/dist/api/safelink/queryEvents.d.ts +4 -0
  19. package/dist/api/safelink/queryEvents.d.ts.map +1 -0
  20. package/dist/api/safelink/queryEvents.js +29 -0
  21. package/dist/api/safelink/queryEvents.js.map +1 -0
  22. package/dist/api/safelink/queryRules.d.ts +4 -0
  23. package/dist/api/safelink/queryRules.d.ts.map +1 -0
  24. package/dist/api/safelink/queryRules.js +43 -0
  25. package/dist/api/safelink/queryRules.js.map +1 -0
  26. package/dist/api/safelink/removeRule.d.ts +4 -0
  27. package/dist/api/safelink/removeRule.d.ts.map +1 -0
  28. package/dist/api/safelink/removeRule.js +35 -0
  29. package/dist/api/safelink/removeRule.js.map +1 -0
  30. package/dist/api/safelink/updateRule.d.ts +4 -0
  31. package/dist/api/safelink/updateRule.d.ts.map +1 -0
  32. package/dist/api/safelink/updateRule.js +37 -0
  33. package/dist/api/safelink/updateRule.js.map +1 -0
  34. package/dist/api/util.d.ts +8 -0
  35. package/dist/api/util.d.ts.map +1 -1
  36. package/dist/api/util.js +33 -1
  37. package/dist/api/util.js.map +1 -1
  38. package/dist/context.d.ts +3 -0
  39. package/dist/context.d.ts.map +1 -1
  40. package/dist/context.js +12 -6
  41. package/dist/context.js.map +1 -1
  42. package/dist/db/migrations/20250609T110704000Z-safelink.d.ts +4 -0
  43. package/dist/db/migrations/20250609T110704000Z-safelink.d.ts.map +1 -0
  44. package/dist/db/migrations/20250609T110704000Z-safelink.js +51 -0
  45. package/dist/db/migrations/20250609T110704000Z-safelink.js.map +1 -0
  46. package/dist/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.d.ts +4 -0
  47. package/dist/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.d.ts.map +1 -0
  48. package/dist/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.js +20 -0
  49. package/dist/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.js.map +1 -0
  50. package/dist/db/migrations/index.d.ts +2 -0
  51. package/dist/db/migrations/index.d.ts.map +1 -1
  52. package/dist/db/migrations/index.js +3 -1
  53. package/dist/db/migrations/index.js.map +1 -1
  54. package/dist/db/schema/index.d.ts +2 -1
  55. package/dist/db/schema/index.d.ts.map +1 -1
  56. package/dist/db/schema/moderation_event.d.ts +6 -0
  57. package/dist/db/schema/moderation_event.d.ts.map +1 -1
  58. package/dist/db/schema/safelink.d.ts +31 -0
  59. package/dist/db/schema/safelink.d.ts.map +1 -0
  60. package/dist/db/schema/safelink.js +6 -0
  61. package/dist/db/schema/safelink.js.map +1 -0
  62. package/dist/lexicon/index.d.ts +15 -0
  63. package/dist/lexicon/index.d.ts.map +1 -1
  64. package/dist/lexicon/index.js +40 -1
  65. package/dist/lexicon/index.js.map +1 -1
  66. package/dist/lexicon/lexicons.d.ts +3427 -2489
  67. package/dist/lexicon/lexicons.d.ts.map +1 -1
  68. package/dist/lexicon/lexicons.js +499 -0
  69. package/dist/lexicon/lexicons.js.map +1 -1
  70. package/dist/lexicon/types/com/atproto/moderation/createReport.d.ts +14 -0
  71. package/dist/lexicon/types/com/atproto/moderation/createReport.d.ts.map +1 -1
  72. package/dist/lexicon/types/com/atproto/moderation/createReport.js +9 -0
  73. package/dist/lexicon/types/com/atproto/moderation/createReport.js.map +1 -1
  74. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +14 -0
  75. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  76. package/dist/lexicon/types/tools/ozone/moderation/defs.js +9 -0
  77. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  78. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts +1 -0
  79. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts.map +1 -1
  80. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +2 -0
  81. package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
  82. package/dist/lexicon/types/tools/ozone/safelink/addRule.d.ts +47 -0
  83. package/dist/lexicon/types/tools/ozone/safelink/addRule.d.ts.map +1 -0
  84. package/dist/lexicon/types/tools/ozone/safelink/addRule.js +7 -0
  85. package/dist/lexicon/types/tools/ozone/safelink/addRule.js.map +1 -0
  86. package/dist/lexicon/types/tools/ozone/safelink/defs.d.ts +47 -0
  87. package/dist/lexicon/types/tools/ozone/safelink/defs.d.ts.map +1 -0
  88. package/dist/lexicon/types/tools/ozone/safelink/defs.js +25 -0
  89. package/dist/lexicon/types/tools/ozone/safelink/defs.js.map +1 -0
  90. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.d.ts +51 -0
  91. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.d.ts.map +1 -0
  92. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.js +7 -0
  93. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.js.map +1 -0
  94. package/dist/lexicon/types/tools/ozone/safelink/queryRules.d.ts +57 -0
  95. package/dist/lexicon/types/tools/ozone/safelink/queryRules.d.ts.map +1 -0
  96. package/dist/lexicon/types/tools/ozone/safelink/queryRules.js +7 -0
  97. package/dist/lexicon/types/tools/ozone/safelink/queryRules.js.map +1 -0
  98. package/dist/lexicon/types/tools/ozone/safelink/removeRule.d.ts +45 -0
  99. package/dist/lexicon/types/tools/ozone/safelink/removeRule.d.ts.map +1 -0
  100. package/dist/lexicon/types/tools/ozone/safelink/removeRule.js +7 -0
  101. package/dist/lexicon/types/tools/ozone/safelink/removeRule.js.map +1 -0
  102. package/dist/lexicon/types/tools/ozone/safelink/updateRule.d.ts +47 -0
  103. package/dist/lexicon/types/tools/ozone/safelink/updateRule.d.ts.map +1 -0
  104. package/dist/lexicon/types/tools/ozone/safelink/updateRule.js +7 -0
  105. package/dist/lexicon/types/tools/ozone/safelink/updateRule.js.map +1 -0
  106. package/dist/mod-service/index.d.ts +13 -1
  107. package/dist/mod-service/index.d.ts.map +1 -1
  108. package/dist/mod-service/index.js +10 -3
  109. package/dist/mod-service/index.js.map +1 -1
  110. package/dist/mod-service/status.d.ts +6 -0
  111. package/dist/mod-service/status.d.ts.map +1 -1
  112. package/dist/mod-service/views.d.ts.map +1 -1
  113. package/dist/mod-service/views.js +6 -0
  114. package/dist/mod-service/views.js.map +1 -1
  115. package/dist/safelink/service.d.ts +69 -0
  116. package/dist/safelink/service.d.ts.map +1 -0
  117. package/dist/safelink/service.js +179 -0
  118. package/dist/safelink/service.js.map +1 -0
  119. package/package.json +5 -5
  120. package/src/api/index.ts +10 -0
  121. package/src/api/moderation/emitEvent.ts +2 -0
  122. package/src/api/moderation/queryEvents.ts +2 -0
  123. package/src/api/report/createReport.ts +2 -1
  124. package/src/api/safelink/addRule.ts +48 -0
  125. package/src/api/safelink/queryEvents.ts +32 -0
  126. package/src/api/safelink/queryRules.ts +58 -0
  127. package/src/api/safelink/removeRule.ts +42 -0
  128. package/src/api/safelink/updateRule.ts +48 -0
  129. package/src/api/util.ts +38 -0
  130. package/src/context.ts +11 -0
  131. package/src/db/migrations/20250609T110704000Z-safelink.ts +53 -0
  132. package/src/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.ts +18 -0
  133. package/src/db/migrations/index.ts +2 -0
  134. package/src/db/schema/index.ts +3 -1
  135. package/src/db/schema/moderation_event.ts +1 -0
  136. package/src/db/schema/safelink.ts +39 -0
  137. package/src/lexicon/index.ts +70 -0
  138. package/src/lexicon/lexicons.ts +509 -0
  139. package/src/lexicon/types/com/atproto/moderation/createReport.ts +20 -0
  140. package/src/lexicon/types/tools/ozone/moderation/defs.ts +21 -0
  141. package/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +1 -0
  142. package/src/lexicon/types/tools/ozone/moderation/queryEvents.ts +2 -0
  143. package/src/lexicon/types/tools/ozone/safelink/addRule.ts +64 -0
  144. package/src/lexicon/types/tools/ozone/safelink/defs.ts +76 -0
  145. package/src/lexicon/types/tools/ozone/safelink/queryEvents.ts +68 -0
  146. package/src/lexicon/types/tools/ozone/safelink/queryRules.ts +74 -0
  147. package/src/lexicon/types/tools/ozone/safelink/removeRule.ts +62 -0
  148. package/src/lexicon/types/tools/ozone/safelink/updateRule.ts +64 -0
  149. package/src/mod-service/index.ts +17 -2
  150. package/src/mod-service/views.ts +6 -0
  151. package/src/safelink/service.ts +304 -0
  152. package/tests/__snapshots__/safelink.test.ts.snap +179 -0
  153. package/tests/communication-templates.test.ts +7 -7
  154. package/tests/mod-tool.test.ts +128 -0
  155. package/tests/safelink.test.ts +534 -0
  156. package/tsconfig.build.tsbuildinfo +1 -1
  157. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,304 @@
1
+ import { Selectable } from 'kysely'
2
+ import { ToolsOzoneSafelinkDefs } from '@atproto/api'
3
+ import { InvalidRequestError } from '@atproto/xrpc-server'
4
+ import {
5
+ SafelinkActionType,
6
+ SafelinkPatternType,
7
+ SafelinkReasonType,
8
+ } from '../api/util'
9
+ import { Database } from '../db'
10
+ import { SafelinkEvent, SafelinkRule } from '../db/schema/safelink'
11
+
12
+ export type SafelinkRuleServiceCreator = (db: Database) => SafelinkRuleService
13
+
14
+ export class SafelinkRuleService {
15
+ constructor(public db: Database) {}
16
+
17
+ static creator() {
18
+ return (db: Database) => new SafelinkRuleService(db)
19
+ }
20
+
21
+ formatEvent(event: Selectable<SafelinkEvent>): ToolsOzoneSafelinkDefs.Event {
22
+ return {
23
+ id: event.id,
24
+ eventType: event.eventType,
25
+ url: event.url,
26
+ pattern: event.pattern,
27
+ action: event.action,
28
+ reason: event.reason,
29
+ createdBy: event.createdBy,
30
+ createdAt: new Date(event.createdAt).toISOString(),
31
+ comment: event.comment || undefined,
32
+ }
33
+ }
34
+
35
+ async addRule({
36
+ url,
37
+ pattern,
38
+ action,
39
+ reason,
40
+ createdBy,
41
+ comment,
42
+ }: {
43
+ url: string
44
+ pattern: SafelinkPatternType
45
+ action: SafelinkActionType
46
+ reason: SafelinkReasonType
47
+ createdBy: string
48
+ comment?: string
49
+ }): Promise<Selectable<SafelinkEvent>> {
50
+ const existingRule = await this.getActiveRule(url, pattern)
51
+ if (existingRule) {
52
+ throw new InvalidRequestError(
53
+ 'A rule for this URL/domain already exists',
54
+ 'RuleAlreadyExists',
55
+ )
56
+ }
57
+
58
+ const now = new Date().toISOString()
59
+ const rule = {
60
+ url,
61
+ pattern,
62
+ action,
63
+ reason,
64
+ createdBy,
65
+ comment: comment || null,
66
+ createdAt: now,
67
+ }
68
+
69
+ return await this.db.transaction(async (txn) => {
70
+ const event = await txn.db
71
+ .insertInto('safelink_event')
72
+ .values({
73
+ eventType: 'addRule',
74
+ ...rule,
75
+ })
76
+ .returningAll()
77
+ .executeTakeFirstOrThrow()
78
+
79
+ await txn.db
80
+ .insertInto('safelink_rule')
81
+ .values({ ...rule, updatedAt: now })
82
+ .execute()
83
+
84
+ return event
85
+ })
86
+ }
87
+
88
+ async updateRule({
89
+ url,
90
+ pattern,
91
+ action,
92
+ reason,
93
+ createdBy,
94
+ comment,
95
+ }: {
96
+ url: string
97
+ pattern: SafelinkPatternType
98
+ action: SafelinkActionType
99
+ reason: SafelinkReasonType
100
+ createdBy: string
101
+ comment?: string
102
+ }): Promise<Selectable<SafelinkEvent>> {
103
+ const existingRule = await this.getActiveRule(url, pattern)
104
+ if (!existingRule) {
105
+ throw new InvalidRequestError(
106
+ 'No active rule found for this URL/domain',
107
+ 'RuleNotFound',
108
+ )
109
+ }
110
+
111
+ const now = new Date().toISOString()
112
+ const rule = {
113
+ action,
114
+ reason,
115
+ createdBy,
116
+ comment: comment || null,
117
+ }
118
+
119
+ return await this.db.transaction(async (txn) => {
120
+ const event = await txn.db
121
+ .insertInto('safelink_event')
122
+ .values({
123
+ createdAt: now,
124
+ url: existingRule.url,
125
+ pattern: existingRule.pattern,
126
+ eventType: 'updateRule',
127
+ ...rule,
128
+ })
129
+ .returningAll()
130
+ .executeTakeFirstOrThrow()
131
+
132
+ await txn.db
133
+ .updateTable('safelink_rule')
134
+ .set(rule)
135
+ .where('url', '=', existingRule.url)
136
+ .where('pattern', '=', existingRule.pattern)
137
+ .execute()
138
+
139
+ return event
140
+ })
141
+ }
142
+
143
+ async removeRule({
144
+ url,
145
+ pattern,
146
+ createdBy,
147
+ comment,
148
+ }: {
149
+ url: string
150
+ pattern: SafelinkPatternType
151
+ createdBy: string
152
+ comment?: string
153
+ }): Promise<Selectable<SafelinkEvent>> {
154
+ const existingRule = await this.getActiveRule(url, pattern)
155
+ if (!existingRule) {
156
+ throw new InvalidRequestError(
157
+ 'No active rule found for this URL/domain',
158
+ 'RuleNotFound',
159
+ )
160
+ }
161
+
162
+ return await this.db.transaction(async (txn) => {
163
+ const event = await txn.db
164
+ .insertInto('safelink_event')
165
+ .values({
166
+ eventType: 'removeRule',
167
+ url,
168
+ pattern,
169
+ action: existingRule.action,
170
+ reason: existingRule.reason,
171
+ createdBy,
172
+ comment: comment || null,
173
+ createdAt: new Date().toISOString(),
174
+ })
175
+ .returningAll()
176
+ .executeTakeFirstOrThrow()
177
+
178
+ await txn.db
179
+ .deleteFrom('safelink_rule')
180
+ .where('url', '=', url)
181
+ .where('pattern', '=', pattern)
182
+ .execute()
183
+
184
+ return event
185
+ })
186
+ }
187
+
188
+ async getActiveRule(url: string, pattern: SafelinkPatternType) {
189
+ const rule = await this.db.db
190
+ .selectFrom('safelink_rule')
191
+ .selectAll()
192
+ .where('url', '=', url)
193
+ .where('pattern', '=', pattern)
194
+ .executeTakeFirst()
195
+
196
+ if (!rule) {
197
+ return null
198
+ }
199
+
200
+ return rule
201
+ }
202
+
203
+ async getActiveRules({
204
+ cursor,
205
+ limit = 50,
206
+ urls,
207
+ patternType,
208
+ actions,
209
+ reason,
210
+ createdBy,
211
+ direction = 'desc',
212
+ }: {
213
+ cursor?: string
214
+ limit?: number
215
+ urls?: string[]
216
+ patternType?: SafelinkPatternType
217
+ actions?: SafelinkActionType[]
218
+ reason?: SafelinkReasonType
219
+ createdBy?: string
220
+ direction?: 'asc' | 'desc'
221
+ } = {}): Promise<{
222
+ rules: Selectable<SafelinkRule>[]
223
+ cursor?: string
224
+ }> {
225
+ let query = this.db.db.selectFrom('safelink_rule').selectAll()
226
+
227
+ if (urls && urls.length > 0) {
228
+ query = query.where('url', 'in', urls)
229
+ }
230
+
231
+ if (patternType) {
232
+ query = query.where('pattern', '=', patternType)
233
+ }
234
+
235
+ if (actions && actions.length > 0) {
236
+ query = query.where('action', 'in', actions)
237
+ }
238
+
239
+ if (reason) {
240
+ query = query.where('reason', '=', reason)
241
+ }
242
+
243
+ if (createdBy) {
244
+ query = query.where('createdBy', '=', createdBy)
245
+ }
246
+
247
+ if (cursor) {
248
+ query = query.where(
249
+ 'id',
250
+ direction === 'asc' ? '>' : '<',
251
+ parseInt(cursor, 10),
252
+ )
253
+ }
254
+
255
+ const rules = await query.orderBy('id', direction).limit(limit).execute()
256
+
257
+ return {
258
+ rules,
259
+ cursor: rules.at(-1)?.id?.toString(),
260
+ }
261
+ }
262
+
263
+ async queryEvents({
264
+ cursor,
265
+ limit = 50,
266
+ urls,
267
+ patternType,
268
+ direction = 'desc',
269
+ }: {
270
+ cursor?: string
271
+ limit?: number
272
+ urls?: string[]
273
+ patternType?: SafelinkPatternType
274
+ direction?: 'asc' | 'desc'
275
+ } = {}): Promise<{
276
+ events: Selectable<SafelinkEvent>[]
277
+ cursor?: string
278
+ }> {
279
+ let query = this.db.db.selectFrom('safelink_event').selectAll()
280
+
281
+ if (urls && urls.length > 0) {
282
+ query = query.where('url', 'in', urls)
283
+ }
284
+
285
+ if (patternType) {
286
+ query = query.where('pattern', '=', patternType)
287
+ }
288
+
289
+ if (cursor) {
290
+ query = query.where(
291
+ 'id',
292
+ direction === 'asc' ? '>' : '<',
293
+ parseInt(cursor, 10),
294
+ )
295
+ }
296
+
297
+ const events = await query.orderBy('id', direction).limit(limit).execute()
298
+
299
+ return {
300
+ events,
301
+ cursor: events.at(-1)?.id?.toString(),
302
+ }
303
+ }
304
+ }
@@ -0,0 +1,179 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`safelink management addRule allows admins to add rules 1`] = `
4
+ Object {
5
+ "action": "block",
6
+ "comment": "Known phishing domain targeting users",
7
+ "createdAt": "1970-01-01T00:00:00.000Z",
8
+ "createdBy": "user(0)",
9
+ "eventType": "addRule",
10
+ "id": 1,
11
+ "pattern": "domain",
12
+ "reason": "phishing",
13
+ "url": "https://malicious-site.com",
14
+ }
15
+ `;
16
+
17
+ exports[`safelink management queryEvents allows querying safelink events 1`] = `
18
+ Array [
19
+ Object {
20
+ "action": "block",
21
+ "comment": "Escalated to block",
22
+ "createdAt": "1970-01-01T00:00:00.000Z",
23
+ "createdBy": "user(0)",
24
+ "eventType": "updateRule",
25
+ "id": 10,
26
+ "pattern": "domain",
27
+ "reason": "phishing",
28
+ "url": "https://events-test.com",
29
+ },
30
+ Object {
31
+ "action": "warn",
32
+ "comment": "Initial rule creation",
33
+ "createdAt": "1970-01-01T00:00:00.000Z",
34
+ "createdBy": "user(0)",
35
+ "eventType": "addRule",
36
+ "id": 9,
37
+ "pattern": "domain",
38
+ "reason": "spam",
39
+ "url": "https://events-test.com",
40
+ },
41
+ Object {
42
+ "action": "warn",
43
+ "createdAt": "1970-01-01T00:00:00.000Z",
44
+ "createdBy": "user(0)",
45
+ "eventType": "addRule",
46
+ "id": 8,
47
+ "pattern": "url",
48
+ "reason": "spam",
49
+ "url": "https://query-test2.com/specific-path",
50
+ },
51
+ Object {
52
+ "action": "block",
53
+ "createdAt": "1970-01-01T00:00:00.000Z",
54
+ "createdBy": "user(0)",
55
+ "eventType": "addRule",
56
+ "id": 7,
57
+ "pattern": "domain",
58
+ "reason": "phishing",
59
+ "url": "https://query-test1.com",
60
+ },
61
+ Object {
62
+ "action": "block",
63
+ "createdAt": "1970-01-01T00:00:00.000Z",
64
+ "createdBy": "user(0)",
65
+ "eventType": "addRule",
66
+ "id": 6,
67
+ "pattern": "domain",
68
+ "reason": "spam",
69
+ "url": "https://remove-test2.com",
70
+ },
71
+ Object {
72
+ "action": "block",
73
+ "comment": "Removing rule - false positive",
74
+ "createdAt": "1970-01-01T00:00:00.000Z",
75
+ "createdBy": "user(0)",
76
+ "eventType": "removeRule",
77
+ "id": 5,
78
+ "pattern": "url",
79
+ "reason": "csam",
80
+ "url": "https://remove-test.com",
81
+ },
82
+ Object {
83
+ "action": "block",
84
+ "comment": "Rule to be removed",
85
+ "createdAt": "1970-01-01T00:00:00.000Z",
86
+ "createdBy": "user(0)",
87
+ "eventType": "addRule",
88
+ "id": 4,
89
+ "pattern": "url",
90
+ "reason": "csam",
91
+ "url": "https://remove-test.com",
92
+ },
93
+ Object {
94
+ "action": "block",
95
+ "comment": "Updated: confirmed phishing site",
96
+ "createdAt": "1970-01-01T00:00:00.000Z",
97
+ "createdBy": "user(0)",
98
+ "eventType": "updateRule",
99
+ "id": 3,
100
+ "pattern": "domain",
101
+ "reason": "phishing",
102
+ "url": "https://update-test.com",
103
+ },
104
+ Object {
105
+ "action": "warn",
106
+ "comment": "Initially marked as spam",
107
+ "createdAt": "1970-01-01T00:00:00.000Z",
108
+ "createdBy": "user(0)",
109
+ "eventType": "addRule",
110
+ "id": 2,
111
+ "pattern": "domain",
112
+ "reason": "spam",
113
+ "url": "https://update-test.com",
114
+ },
115
+ Object {
116
+ "action": "block",
117
+ "comment": "Known phishing domain targeting users",
118
+ "createdAt": "1970-01-01T00:00:00.000Z",
119
+ "createdBy": "user(0)",
120
+ "eventType": "addRule",
121
+ "id": 1,
122
+ "pattern": "domain",
123
+ "reason": "phishing",
124
+ "url": "https://malicious-site.com",
125
+ },
126
+ ]
127
+ `;
128
+
129
+ exports[`safelink management queryRules allows querying all active rules 1`] = `
130
+ Array [
131
+ Object {
132
+ "action": "warn",
133
+ "createdAt": "1970-01-01T00:00:00.000Z",
134
+ "createdBy": "user(0)",
135
+ "pattern": "url",
136
+ "reason": "spam",
137
+ "updatedAt": "1970-01-01T00:00:00.000Z",
138
+ "url": "https://query-test2.com/specific-path",
139
+ },
140
+ Object {
141
+ "action": "block",
142
+ "createdAt": "1970-01-01T00:00:00.000Z",
143
+ "createdBy": "user(0)",
144
+ "pattern": "domain",
145
+ "reason": "phishing",
146
+ "updatedAt": "1970-01-01T00:00:00.000Z",
147
+ "url": "https://query-test1.com",
148
+ },
149
+ Object {
150
+ "action": "block",
151
+ "createdAt": "1970-01-01T00:00:00.000Z",
152
+ "createdBy": "user(0)",
153
+ "pattern": "domain",
154
+ "reason": "spam",
155
+ "updatedAt": "1970-01-01T00:00:00.000Z",
156
+ "url": "https://remove-test2.com",
157
+ },
158
+ Object {
159
+ "action": "block",
160
+ "comment": "Updated: confirmed phishing site",
161
+ "createdAt": "1970-01-01T00:00:00.000Z",
162
+ "createdBy": "user(0)",
163
+ "pattern": "domain",
164
+ "reason": "phishing",
165
+ "updatedAt": "1970-01-01T00:00:00.000Z",
166
+ "url": "https://update-test.com",
167
+ },
168
+ Object {
169
+ "action": "block",
170
+ "comment": "Known phishing domain targeting users",
171
+ "createdAt": "1970-01-01T00:00:00.000Z",
172
+ "createdBy": "user(0)",
173
+ "pattern": "domain",
174
+ "reason": "phishing",
175
+ "updatedAt": "1970-01-01T00:00:00.000Z",
176
+ "url": "https://malicious-site.com",
177
+ },
178
+ ]
179
+ `;
@@ -28,7 +28,7 @@ describe('communication-templates', () => {
28
28
  }
29
29
 
30
30
  const listTemplates = async () => {
31
- const { data } = await agent.api.tools.ozone.communication.listTemplates(
31
+ const { data } = await agent.tools.ozone.communication.listTemplates(
32
32
  {},
33
33
  {
34
34
  headers: await network.ozone.modHeaders(
@@ -42,7 +42,7 @@ describe('communication-templates', () => {
42
42
 
43
43
  describe('create templates', () => {
44
44
  it('only allows admins to create new templates', async () => {
45
- const moderatorReq = agent.api.tools.ozone.communication.createTemplate(
45
+ const moderatorReq = agent.tools.ozone.communication.createTemplate(
46
46
  { ...templateOne, createdBy: sc.dids.bob },
47
47
  {
48
48
  encoding: 'application/json',
@@ -55,7 +55,7 @@ describe('communication-templates', () => {
55
55
  await expect(moderatorReq).rejects.toThrow(
56
56
  'Must be a moderator to create a communication template',
57
57
  )
58
- const modReq = await agent.api.tools.ozone.communication.createTemplate(
58
+ const modReq = await agent.tools.ozone.communication.createTemplate(
59
59
  { ...templateOne, createdBy: sc.dids.bob },
60
60
  {
61
61
  encoding: 'application/json',
@@ -82,7 +82,7 @@ describe('communication-templates', () => {
82
82
  ...templateOne,
83
83
  name: 'Test template 2',
84
84
  }
85
- await agent.api.tools.ozone.communication.createTemplate(
85
+ await agent.tools.ozone.communication.createTemplate(
86
86
  { ...templateTwo, createdBy: sc.dids.bob },
87
87
  {
88
88
  encoding: 'application/json',
@@ -100,7 +100,7 @@ describe('communication-templates', () => {
100
100
  })
101
101
  describe('update template', () => {
102
102
  it('allows moderators to update a template by id', async () => {
103
- const { data } = await agent.api.tools.ozone.communication.updateTemplate(
103
+ const { data } = await agent.tools.ozone.communication.updateTemplate(
104
104
  { id: '1', updatedBy: sc.dids.bob, name: '1 Test template' },
105
105
  {
106
106
  encoding: 'application/json',
@@ -117,7 +117,7 @@ describe('communication-templates', () => {
117
117
  })
118
118
  describe('delete template', () => {
119
119
  it('allows admins to remove a template by id', async () => {
120
- const modReq = agent.api.tools.ozone.communication.deleteTemplate(
120
+ const modReq = agent.tools.ozone.communication.deleteTemplate(
121
121
  { id: '1' },
122
122
  {
123
123
  encoding: 'application/json',
@@ -132,7 +132,7 @@ describe('communication-templates', () => {
132
132
  'Must be a moderator to delete a communication template',
133
133
  )
134
134
 
135
- await agent.api.tools.ozone.communication.deleteTemplate(
135
+ await agent.tools.ozone.communication.deleteTemplate(
136
136
  { id: '1' },
137
137
  {
138
138
  encoding: 'application/json',
@@ -0,0 +1,128 @@
1
+ import {
2
+ ModeratorClient,
3
+ SeedClient,
4
+ TestNetwork,
5
+ basicSeed,
6
+ } from '@atproto/dev-env'
7
+
8
+ describe('mod-tool tracking', () => {
9
+ let network: TestNetwork
10
+ let sc: SeedClient
11
+ let modClient: ModeratorClient
12
+
13
+ beforeAll(async () => {
14
+ network = await TestNetwork.create({
15
+ dbPostgresSchema: 'ozone_mod_tool_test',
16
+ })
17
+ sc = network.getSeedClient()
18
+ modClient = network.ozone.getModClient()
19
+ await basicSeed(sc)
20
+ await network.processAll()
21
+ })
22
+
23
+ afterAll(async () => {
24
+ await network.close()
25
+ })
26
+
27
+ it('stores and returns modTool with name and meta metadata', async () => {
28
+ const subject = {
29
+ $type: 'com.atproto.repo.strongRef' as const,
30
+ uri: sc.posts[sc.dids.bob][0].ref.uriStr,
31
+ cid: sc.posts[sc.dids.bob][0].ref.cidStr,
32
+ }
33
+
34
+ const modTool = {
35
+ name: 'automod/1.1.3',
36
+ meta: {
37
+ confidence: 85,
38
+ rules: ['high_risk_country', 'spam_detection'],
39
+ version: '1.1.3',
40
+ environment: 'production',
41
+ },
42
+ }
43
+
44
+ const emittedEvent = await modClient.emitEvent({
45
+ event: {
46
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
47
+ createLabelVals: ['spam'],
48
+ negateLabelVals: [],
49
+ },
50
+ subject,
51
+ modTool,
52
+ })
53
+
54
+ expect(emittedEvent.modTool).toEqual(modTool)
55
+ expect(emittedEvent.modTool?.name).toBe('automod/1.1.3')
56
+ expect(emittedEvent.modTool?.meta).toEqual({
57
+ confidence: 85,
58
+ rules: ['high_risk_country', 'spam_detection'],
59
+ version: '1.1.3',
60
+ environment: 'production',
61
+ })
62
+
63
+ const queryResult = await modClient.queryEvents({
64
+ subject: subject.uri,
65
+ })
66
+
67
+ const foundEvent = queryResult.events.find((e) => e.id === emittedEvent.id)
68
+ expect(foundEvent?.modTool).toEqual(modTool)
69
+ })
70
+
71
+ it('filters events by modTool name', async () => {
72
+ const subject = {
73
+ $type: 'com.atproto.repo.strongRef' as const,
74
+ uri: sc.posts[sc.dids.alice][0].ref.uriStr,
75
+ cid: sc.posts[sc.dids.alice][0].ref.cidStr,
76
+ }
77
+
78
+ const event1 = await modClient.emitEvent({
79
+ event: {
80
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
81
+ createLabelVals: ['test1'],
82
+ negateLabelVals: [],
83
+ },
84
+ subject,
85
+ modTool: { name: 'automod/1.1.3' },
86
+ })
87
+
88
+ const event2 = await modClient.emitEvent({
89
+ event: {
90
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
91
+ createLabelVals: ['test2'],
92
+ negateLabelVals: [],
93
+ },
94
+ subject,
95
+ modTool: { name: 'ozone-web/1.0.0' },
96
+ })
97
+
98
+ const event3 = await modClient.emitEvent({
99
+ event: {
100
+ $type: 'tools.ozone.moderation.defs#modEventLabel',
101
+ createLabelVals: ['test3'],
102
+ negateLabelVals: [],
103
+ },
104
+ subject,
105
+ modTool: { name: 'mobile-app/2.1.0' },
106
+ })
107
+
108
+ const automodResults = await modClient.queryEvents({
109
+ subject: subject.uri,
110
+ modTool: ['automod/1.1.3'],
111
+ })
112
+
113
+ expect(automodResults.events).toHaveLength(1)
114
+ expect(automodResults.events[0].id).toBe(event1.id)
115
+ expect(automodResults.events[0].modTool?.name).toBe('automod/1.1.3')
116
+
117
+ const multipleResults = await modClient.queryEvents({
118
+ subject: subject.uri,
119
+ modTool: ['automod/1.1.3', 'ozone-web/1.0.0'],
120
+ })
121
+
122
+ expect(multipleResults.events).toHaveLength(2)
123
+ const eventIds = multipleResults.events.map((e) => e.id)
124
+ expect(eventIds).toContain(event1.id)
125
+ expect(eventIds).toContain(event2.id)
126
+ expect(eventIds).not.toContain(event3.id)
127
+ })
128
+ })