@atproto/ozone 0.1.126 → 0.1.127

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 (111) hide show
  1. package/CHANGELOG.md +9 -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/safelink/addRule.d.ts +4 -0
  6. package/dist/api/safelink/addRule.d.ts.map +1 -0
  7. package/dist/api/safelink/addRule.js +37 -0
  8. package/dist/api/safelink/addRule.js.map +1 -0
  9. package/dist/api/safelink/queryEvents.d.ts +4 -0
  10. package/dist/api/safelink/queryEvents.d.ts.map +1 -0
  11. package/dist/api/safelink/queryEvents.js +29 -0
  12. package/dist/api/safelink/queryEvents.js.map +1 -0
  13. package/dist/api/safelink/queryRules.d.ts +4 -0
  14. package/dist/api/safelink/queryRules.d.ts.map +1 -0
  15. package/dist/api/safelink/queryRules.js +43 -0
  16. package/dist/api/safelink/queryRules.js.map +1 -0
  17. package/dist/api/safelink/removeRule.d.ts +4 -0
  18. package/dist/api/safelink/removeRule.d.ts.map +1 -0
  19. package/dist/api/safelink/removeRule.js +35 -0
  20. package/dist/api/safelink/removeRule.js.map +1 -0
  21. package/dist/api/safelink/updateRule.d.ts +4 -0
  22. package/dist/api/safelink/updateRule.d.ts.map +1 -0
  23. package/dist/api/safelink/updateRule.js +37 -0
  24. package/dist/api/safelink/updateRule.js.map +1 -0
  25. package/dist/api/util.d.ts +8 -0
  26. package/dist/api/util.d.ts.map +1 -1
  27. package/dist/api/util.js +33 -1
  28. package/dist/api/util.js.map +1 -1
  29. package/dist/context.d.ts +3 -0
  30. package/dist/context.d.ts.map +1 -1
  31. package/dist/context.js +12 -6
  32. package/dist/context.js.map +1 -1
  33. package/dist/db/migrations/20250609T110704000Z-safelink.d.ts +4 -0
  34. package/dist/db/migrations/20250609T110704000Z-safelink.d.ts.map +1 -0
  35. package/dist/db/migrations/20250609T110704000Z-safelink.js +51 -0
  36. package/dist/db/migrations/20250609T110704000Z-safelink.js.map +1 -0
  37. package/dist/db/migrations/index.d.ts +1 -0
  38. package/dist/db/migrations/index.d.ts.map +1 -1
  39. package/dist/db/migrations/index.js +2 -1
  40. package/dist/db/migrations/index.js.map +1 -1
  41. package/dist/db/schema/index.d.ts +2 -1
  42. package/dist/db/schema/index.d.ts.map +1 -1
  43. package/dist/db/schema/safelink.d.ts +31 -0
  44. package/dist/db/schema/safelink.d.ts.map +1 -0
  45. package/dist/db/schema/safelink.js +6 -0
  46. package/dist/db/schema/safelink.js.map +1 -0
  47. package/dist/lexicon/index.d.ts +15 -0
  48. package/dist/lexicon/index.d.ts.map +1 -1
  49. package/dist/lexicon/index.js +40 -1
  50. package/dist/lexicon/index.js.map +1 -1
  51. package/dist/lexicon/lexicons.d.ts +10972 -10140
  52. package/dist/lexicon/lexicons.d.ts.map +1 -1
  53. package/dist/lexicon/lexicons.js +446 -0
  54. package/dist/lexicon/lexicons.js.map +1 -1
  55. package/dist/lexicon/types/tools/ozone/safelink/addRule.d.ts +47 -0
  56. package/dist/lexicon/types/tools/ozone/safelink/addRule.d.ts.map +1 -0
  57. package/dist/lexicon/types/tools/ozone/safelink/addRule.js +7 -0
  58. package/dist/lexicon/types/tools/ozone/safelink/addRule.js.map +1 -0
  59. package/dist/lexicon/types/tools/ozone/safelink/defs.d.ts +47 -0
  60. package/dist/lexicon/types/tools/ozone/safelink/defs.d.ts.map +1 -0
  61. package/dist/lexicon/types/tools/ozone/safelink/defs.js +25 -0
  62. package/dist/lexicon/types/tools/ozone/safelink/defs.js.map +1 -0
  63. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.d.ts +51 -0
  64. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.d.ts.map +1 -0
  65. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.js +7 -0
  66. package/dist/lexicon/types/tools/ozone/safelink/queryEvents.js.map +1 -0
  67. package/dist/lexicon/types/tools/ozone/safelink/queryRules.d.ts +57 -0
  68. package/dist/lexicon/types/tools/ozone/safelink/queryRules.d.ts.map +1 -0
  69. package/dist/lexicon/types/tools/ozone/safelink/queryRules.js +7 -0
  70. package/dist/lexicon/types/tools/ozone/safelink/queryRules.js.map +1 -0
  71. package/dist/lexicon/types/tools/ozone/safelink/removeRule.d.ts +45 -0
  72. package/dist/lexicon/types/tools/ozone/safelink/removeRule.d.ts.map +1 -0
  73. package/dist/lexicon/types/tools/ozone/safelink/removeRule.js +7 -0
  74. package/dist/lexicon/types/tools/ozone/safelink/removeRule.js.map +1 -0
  75. package/dist/lexicon/types/tools/ozone/safelink/updateRule.d.ts +47 -0
  76. package/dist/lexicon/types/tools/ozone/safelink/updateRule.d.ts.map +1 -0
  77. package/dist/lexicon/types/tools/ozone/safelink/updateRule.js +7 -0
  78. package/dist/lexicon/types/tools/ozone/safelink/updateRule.js.map +1 -0
  79. package/dist/mod-service/status.d.ts +6 -0
  80. package/dist/mod-service/status.d.ts.map +1 -1
  81. package/dist/safelink/service.d.ts +69 -0
  82. package/dist/safelink/service.d.ts.map +1 -0
  83. package/dist/safelink/service.js +179 -0
  84. package/dist/safelink/service.js.map +1 -0
  85. package/package.json +4 -4
  86. package/src/api/index.ts +10 -0
  87. package/src/api/safelink/addRule.ts +48 -0
  88. package/src/api/safelink/queryEvents.ts +32 -0
  89. package/src/api/safelink/queryRules.ts +58 -0
  90. package/src/api/safelink/removeRule.ts +42 -0
  91. package/src/api/safelink/updateRule.ts +48 -0
  92. package/src/api/util.ts +38 -0
  93. package/src/context.ts +11 -0
  94. package/src/db/migrations/20250609T110704000Z-safelink.ts +53 -0
  95. package/src/db/migrations/index.ts +1 -0
  96. package/src/db/schema/index.ts +3 -1
  97. package/src/db/schema/safelink.ts +39 -0
  98. package/src/lexicon/index.ts +70 -0
  99. package/src/lexicon/lexicons.ts +451 -0
  100. package/src/lexicon/types/tools/ozone/safelink/addRule.ts +64 -0
  101. package/src/lexicon/types/tools/ozone/safelink/defs.ts +76 -0
  102. package/src/lexicon/types/tools/ozone/safelink/queryEvents.ts +68 -0
  103. package/src/lexicon/types/tools/ozone/safelink/queryRules.ts +74 -0
  104. package/src/lexicon/types/tools/ozone/safelink/removeRule.ts +62 -0
  105. package/src/lexicon/types/tools/ozone/safelink/updateRule.ts +64 -0
  106. package/src/safelink/service.ts +304 -0
  107. package/tests/__snapshots__/safelink.test.ts.snap +179 -0
  108. package/tests/communication-templates.test.ts +7 -7
  109. package/tests/safelink.test.ts +534 -0
  110. package/tsconfig.build.tsbuildinfo +1 -1
  111. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SafelinkRuleService = void 0;
4
+ const xrpc_server_1 = require("@atproto/xrpc-server");
5
+ class SafelinkRuleService {
6
+ constructor(db) {
7
+ Object.defineProperty(this, "db", {
8
+ enumerable: true,
9
+ configurable: true,
10
+ writable: true,
11
+ value: db
12
+ });
13
+ }
14
+ static creator() {
15
+ return (db) => new SafelinkRuleService(db);
16
+ }
17
+ formatEvent(event) {
18
+ return {
19
+ id: event.id,
20
+ eventType: event.eventType,
21
+ url: event.url,
22
+ pattern: event.pattern,
23
+ action: event.action,
24
+ reason: event.reason,
25
+ createdBy: event.createdBy,
26
+ createdAt: new Date(event.createdAt).toISOString(),
27
+ comment: event.comment || undefined,
28
+ };
29
+ }
30
+ async addRule({ url, pattern, action, reason, createdBy, comment, }) {
31
+ const existingRule = await this.getActiveRule(url, pattern);
32
+ if (existingRule) {
33
+ throw new xrpc_server_1.InvalidRequestError('A rule for this URL/domain already exists', 'RuleAlreadyExists');
34
+ }
35
+ const now = new Date().toISOString();
36
+ const rule = {
37
+ url,
38
+ pattern,
39
+ action,
40
+ reason,
41
+ createdBy,
42
+ comment: comment || null,
43
+ createdAt: now,
44
+ };
45
+ return await this.db.transaction(async (txn) => {
46
+ const event = await txn.db
47
+ .insertInto('safelink_event')
48
+ .values({
49
+ eventType: 'addRule',
50
+ ...rule,
51
+ })
52
+ .returningAll()
53
+ .executeTakeFirstOrThrow();
54
+ await txn.db
55
+ .insertInto('safelink_rule')
56
+ .values({ ...rule, updatedAt: now })
57
+ .execute();
58
+ return event;
59
+ });
60
+ }
61
+ async updateRule({ url, pattern, action, reason, createdBy, comment, }) {
62
+ const existingRule = await this.getActiveRule(url, pattern);
63
+ if (!existingRule) {
64
+ throw new xrpc_server_1.InvalidRequestError('No active rule found for this URL/domain', 'RuleNotFound');
65
+ }
66
+ const now = new Date().toISOString();
67
+ const rule = {
68
+ action,
69
+ reason,
70
+ createdBy,
71
+ comment: comment || null,
72
+ };
73
+ return await this.db.transaction(async (txn) => {
74
+ const event = await txn.db
75
+ .insertInto('safelink_event')
76
+ .values({
77
+ createdAt: now,
78
+ url: existingRule.url,
79
+ pattern: existingRule.pattern,
80
+ eventType: 'updateRule',
81
+ ...rule,
82
+ })
83
+ .returningAll()
84
+ .executeTakeFirstOrThrow();
85
+ await txn.db
86
+ .updateTable('safelink_rule')
87
+ .set(rule)
88
+ .where('url', '=', existingRule.url)
89
+ .where('pattern', '=', existingRule.pattern)
90
+ .execute();
91
+ return event;
92
+ });
93
+ }
94
+ async removeRule({ url, pattern, createdBy, comment, }) {
95
+ const existingRule = await this.getActiveRule(url, pattern);
96
+ if (!existingRule) {
97
+ throw new xrpc_server_1.InvalidRequestError('No active rule found for this URL/domain', 'RuleNotFound');
98
+ }
99
+ return await this.db.transaction(async (txn) => {
100
+ const event = await txn.db
101
+ .insertInto('safelink_event')
102
+ .values({
103
+ eventType: 'removeRule',
104
+ url,
105
+ pattern,
106
+ action: existingRule.action,
107
+ reason: existingRule.reason,
108
+ createdBy,
109
+ comment: comment || null,
110
+ createdAt: new Date().toISOString(),
111
+ })
112
+ .returningAll()
113
+ .executeTakeFirstOrThrow();
114
+ await txn.db
115
+ .deleteFrom('safelink_rule')
116
+ .where('url', '=', url)
117
+ .where('pattern', '=', pattern)
118
+ .execute();
119
+ return event;
120
+ });
121
+ }
122
+ async getActiveRule(url, pattern) {
123
+ const rule = await this.db.db
124
+ .selectFrom('safelink_rule')
125
+ .selectAll()
126
+ .where('url', '=', url)
127
+ .where('pattern', '=', pattern)
128
+ .executeTakeFirst();
129
+ if (!rule) {
130
+ return null;
131
+ }
132
+ return rule;
133
+ }
134
+ async getActiveRules({ cursor, limit = 50, urls, patternType, actions, reason, createdBy, direction = 'desc', } = {}) {
135
+ let query = this.db.db.selectFrom('safelink_rule').selectAll();
136
+ if (urls && urls.length > 0) {
137
+ query = query.where('url', 'in', urls);
138
+ }
139
+ if (patternType) {
140
+ query = query.where('pattern', '=', patternType);
141
+ }
142
+ if (actions && actions.length > 0) {
143
+ query = query.where('action', 'in', actions);
144
+ }
145
+ if (reason) {
146
+ query = query.where('reason', '=', reason);
147
+ }
148
+ if (createdBy) {
149
+ query = query.where('createdBy', '=', createdBy);
150
+ }
151
+ if (cursor) {
152
+ query = query.where('id', direction === 'asc' ? '>' : '<', parseInt(cursor, 10));
153
+ }
154
+ const rules = await query.orderBy('id', direction).limit(limit).execute();
155
+ return {
156
+ rules,
157
+ cursor: rules.at(-1)?.id?.toString(),
158
+ };
159
+ }
160
+ async queryEvents({ cursor, limit = 50, urls, patternType, direction = 'desc', } = {}) {
161
+ let query = this.db.db.selectFrom('safelink_event').selectAll();
162
+ if (urls && urls.length > 0) {
163
+ query = query.where('url', 'in', urls);
164
+ }
165
+ if (patternType) {
166
+ query = query.where('pattern', '=', patternType);
167
+ }
168
+ if (cursor) {
169
+ query = query.where('id', direction === 'asc' ? '>' : '<', parseInt(cursor, 10));
170
+ }
171
+ const events = await query.orderBy('id', direction).limit(limit).execute();
172
+ return {
173
+ events,
174
+ cursor: events.at(-1)?.id?.toString(),
175
+ };
176
+ }
177
+ }
178
+ exports.SafelinkRuleService = SafelinkRuleService;
179
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/safelink/service.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ozone",
3
- "version": "0.1.126",
3
+ "version": "0.1.127",
4
4
  "license": "MIT",
5
5
  "description": "Backend service for moderating the Bluesky network.",
6
6
  "keywords": [
@@ -35,12 +35,12 @@
35
35
  "uint8arrays": "3.0.0",
36
36
  "undici": "^6.14.1",
37
37
  "ws": "^8.12.0",
38
- "@atproto/api": "^0.15.21",
38
+ "@atproto/api": "^0.15.22",
39
39
  "@atproto/common": "^0.4.11",
40
40
  "@atproto/crypto": "^0.4.4",
41
+ "@atproto/identity": "^0.4.8",
41
42
  "@atproto/lexicon": "^0.4.11",
42
43
  "@atproto/syntax": "^0.4.0",
43
- "@atproto/identity": "^0.4.8",
44
44
  "@atproto/xrpc": "^0.7.0",
45
45
  "@atproto/xrpc-server": "^0.8.0"
46
46
  },
@@ -55,7 +55,7 @@
55
55
  "ts-node": "^10.8.2",
56
56
  "typescript": "^5.6.3",
57
57
  "@atproto/lex-cli": "^0.8.3",
58
- "@atproto/pds": "^0.4.156"
58
+ "@atproto/pds": "^0.4.157"
59
59
  },
60
60
  "scripts": {
61
61
  "codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
package/src/api/index.ts CHANGED
@@ -21,6 +21,11 @@ import queryStatuses from './moderation/queryStatuses'
21
21
  import searchRepos from './moderation/searchRepos'
22
22
  import proxied from './proxied'
23
23
  import createReport from './report/createReport'
24
+ import addSafelinkRule from './safelink/addRule'
25
+ import querySafelinkEvents from './safelink/queryEvents'
26
+ import querySafelinkRules from './safelink/queryRules'
27
+ import removeSafelinkRule from './safelink/removeRule'
28
+ import updateSafelinkRule from './safelink/updateRule'
24
29
  import getConfig from './server/getConfig'
25
30
  import setAddValues from './set/addValues'
26
31
  import deleteSet from './set/deleteSet'
@@ -82,5 +87,10 @@ export default function (server: Server, ctx: AppContext) {
82
87
  grantVerifications(server, ctx)
83
88
  revokeVerifications(server, ctx)
84
89
  listVerifications(server, ctx)
90
+ addSafelinkRule(server, ctx)
91
+ updateSafelinkRule(server, ctx)
92
+ removeSafelinkRule(server, ctx)
93
+ querySafelinkEvents(server, ctx)
94
+ querySafelinkRules(server, ctx)
85
95
  return server
86
96
  }
@@ -0,0 +1,48 @@
1
+ import { AuthRequiredError } from '@atproto/xrpc-server'
2
+ import { AppContext } from '../../context'
3
+ import { Server } from '../../lexicon'
4
+ import {
5
+ getSafelinkAction,
6
+ getSafelinkPattern,
7
+ getSafelinkReason,
8
+ } from '../util'
9
+
10
+ export default function (server: Server, ctx: AppContext) {
11
+ server.tools.ozone.safelink.addRule({
12
+ auth: ctx.authVerifier.modOrAdminToken,
13
+ handler: async ({ input, auth }) => {
14
+ const access = auth.credentials
15
+ const db = ctx.db
16
+ const { url, pattern, action, reason, comment, createdBy } = input.body
17
+
18
+ if (!access.isModerator) {
19
+ throw new AuthRequiredError('Must be a moderator to add URL rules')
20
+ }
21
+
22
+ if (access.type === 'admin_token' && !createdBy) {
23
+ throw new AuthRequiredError(
24
+ 'Must specify createdBy when using admin auth',
25
+ )
26
+ }
27
+
28
+ const safelinkRuleService = ctx.safelinkRuleService(db)
29
+
30
+ const event = await safelinkRuleService.addRule({
31
+ url,
32
+ pattern: getSafelinkPattern(pattern),
33
+ action: getSafelinkAction(action),
34
+ reason: getSafelinkReason(reason),
35
+ createdBy:
36
+ access.type === 'admin_token'
37
+ ? createdBy || ctx.cfg.service.did
38
+ : access.iss,
39
+ comment,
40
+ })
41
+
42
+ return {
43
+ encoding: 'application/json',
44
+ body: safelinkRuleService.formatEvent(event),
45
+ }
46
+ },
47
+ })
48
+ }
@@ -0,0 +1,32 @@
1
+ import { AppContext } from '../../context'
2
+ import { Server } from '../../lexicon'
3
+ import { getSafelinkPattern } from '../util'
4
+
5
+ export default function (server: Server, ctx: AppContext) {
6
+ server.tools.ozone.safelink.queryEvents({
7
+ auth: ctx.authVerifier.modOrAdminToken,
8
+ handler: async ({ input }) => {
9
+ const db = ctx.db
10
+ const { cursor, limit, urls, patternType, sortDirection } = input.body
11
+
12
+ const safelinkRuleService = ctx.safelinkRuleService(db)
13
+ const result = await safelinkRuleService.queryEvents({
14
+ cursor,
15
+ limit,
16
+ urls,
17
+ patternType: patternType ? getSafelinkPattern(patternType) : undefined,
18
+ direction: sortDirection as 'asc' | 'desc' | undefined,
19
+ })
20
+
21
+ return {
22
+ encoding: 'application/json',
23
+ body: {
24
+ cursor: result.cursor,
25
+ events: result.events.map((event) =>
26
+ safelinkRuleService.formatEvent(event),
27
+ ),
28
+ },
29
+ }
30
+ },
31
+ })
32
+ }
@@ -0,0 +1,58 @@
1
+ import { AppContext } from '../../context'
2
+ import { Server } from '../../lexicon'
3
+ import {
4
+ getSafelinkAction,
5
+ getSafelinkPattern,
6
+ getSafelinkReason,
7
+ } from '../util'
8
+
9
+ export default function (server: Server, ctx: AppContext) {
10
+ server.tools.ozone.safelink.queryRules({
11
+ auth: ctx.authVerifier.modOrAdminToken,
12
+ handler: async ({ input }) => {
13
+ const db = ctx.db
14
+ const {
15
+ cursor,
16
+ limit,
17
+ urls,
18
+ patternType,
19
+ actions,
20
+ reason,
21
+ createdBy,
22
+ sortDirection,
23
+ } = input.body
24
+
25
+ const safelinkRuleService = ctx.safelinkRuleService(db)
26
+ const result = await safelinkRuleService.getActiveRules({
27
+ cursor,
28
+ limit,
29
+ urls,
30
+ patternType: patternType ? getSafelinkPattern(patternType) : undefined,
31
+ actions:
32
+ actions && actions.length > 0
33
+ ? actions.map(getSafelinkAction)
34
+ : undefined,
35
+ reason: reason ? getSafelinkReason(reason) : undefined,
36
+ createdBy,
37
+ direction: sortDirection as 'asc' | 'desc' | undefined,
38
+ })
39
+
40
+ return {
41
+ encoding: 'application/json',
42
+ body: {
43
+ cursor: result.cursor,
44
+ rules: result.rules.map((rule) => ({
45
+ url: rule.url,
46
+ pattern: rule.pattern,
47
+ action: rule.action,
48
+ reason: rule.reason,
49
+ createdBy: rule.createdBy,
50
+ createdAt: new Date(rule.createdAt).toISOString(),
51
+ updatedAt: new Date(rule.updatedAt).toISOString(),
52
+ comment: rule.comment || undefined,
53
+ })),
54
+ },
55
+ }
56
+ },
57
+ })
58
+ }
@@ -0,0 +1,42 @@
1
+ import { AuthRequiredError } from '@atproto/xrpc-server'
2
+ import { AppContext } from '../../context'
3
+ import { Server } from '../../lexicon'
4
+ import { getSafelinkPattern } from '../util'
5
+
6
+ export default function (server: Server, ctx: AppContext) {
7
+ server.tools.ozone.safelink.removeRule({
8
+ auth: ctx.authVerifier.modOrAdminToken,
9
+ handler: async ({ input, auth }) => {
10
+ const access = auth.credentials
11
+ const db = ctx.db
12
+ const { url, pattern, comment, createdBy } = input.body
13
+
14
+ if (!access.isModerator) {
15
+ throw new AuthRequiredError('Must be a moderator to remove URL rules')
16
+ }
17
+
18
+ if (access.type === 'admin_token' && !createdBy) {
19
+ throw new AuthRequiredError(
20
+ 'Must specify createdBy when using admin auth',
21
+ )
22
+ }
23
+
24
+ const safelinkRuleService = ctx.safelinkRuleService(db)
25
+
26
+ const event = await safelinkRuleService.removeRule({
27
+ url,
28
+ pattern: getSafelinkPattern(pattern),
29
+ createdBy:
30
+ access.type === 'admin_token'
31
+ ? createdBy || ctx.cfg.service.did
32
+ : access.iss,
33
+ comment,
34
+ })
35
+
36
+ return {
37
+ encoding: 'application/json',
38
+ body: safelinkRuleService.formatEvent(event),
39
+ }
40
+ },
41
+ })
42
+ }
@@ -0,0 +1,48 @@
1
+ import { AuthRequiredError } from '@atproto/xrpc-server'
2
+ import { AppContext } from '../../context'
3
+ import { Server } from '../../lexicon'
4
+ import {
5
+ getSafelinkAction,
6
+ getSafelinkPattern,
7
+ getSafelinkReason,
8
+ } from '../util'
9
+
10
+ export default function (server: Server, ctx: AppContext) {
11
+ server.tools.ozone.safelink.updateRule({
12
+ auth: ctx.authVerifier.modOrAdminToken,
13
+ handler: async ({ input, auth }) => {
14
+ const access = auth.credentials
15
+ const db = ctx.db
16
+ const { url, pattern, action, reason, comment, createdBy } = input.body
17
+
18
+ if (!access.isModerator) {
19
+ throw new AuthRequiredError('Must be a moderator to update URL rules')
20
+ }
21
+
22
+ if (access.type === 'admin_token' && !createdBy) {
23
+ throw new AuthRequiredError(
24
+ 'Must specify createdBy when using admin auth',
25
+ )
26
+ }
27
+
28
+ const safelinkRuleService = ctx.safelinkRuleService(db)
29
+
30
+ const event = await safelinkRuleService.updateRule({
31
+ url,
32
+ pattern: getSafelinkPattern(pattern),
33
+ action: getSafelinkAction(action),
34
+ reason: getSafelinkReason(reason),
35
+ createdBy:
36
+ access.type === 'admin_token'
37
+ ? createdBy || ctx.cfg.service.did
38
+ : access.iss,
39
+ comment,
40
+ })
41
+
42
+ return {
43
+ encoding: 'application/json',
44
+ body: safelinkRuleService.formatEvent(event),
45
+ }
46
+ },
47
+ })
48
+ }
package/src/api/util.ts CHANGED
@@ -191,3 +191,41 @@ const memberRoles = new Set([
191
191
  ROLETRIAGE,
192
192
  ROLEVERIFIER,
193
193
  ])
194
+
195
+ export const getSafelinkPattern = (pattern: string): SafelinkPatternType => {
196
+ if (safelinkPatterns.has(pattern)) {
197
+ return pattern as SafelinkPatternType
198
+ }
199
+ throw new InvalidRequestError('Invalid safelink pattern type')
200
+ }
201
+
202
+ export const getSafelinkAction = (action: string): SafelinkActionType => {
203
+ if (safelinkActions.has(action)) {
204
+ return action as SafelinkActionType
205
+ }
206
+ throw new InvalidRequestError('Invalid safelink action type')
207
+ }
208
+
209
+ export const getSafelinkReason = (reason: string): SafelinkReasonType => {
210
+ if (safelinkReasons.has(reason)) {
211
+ return reason as SafelinkReasonType
212
+ }
213
+ throw new InvalidRequestError('Invalid safelink reason type')
214
+ }
215
+
216
+ export const getSafelinkEventType = (eventType: string): SafelinkEventType => {
217
+ if (safelinkEventTypes.has(eventType)) {
218
+ return eventType as SafelinkEventType
219
+ }
220
+ throw new InvalidRequestError('Invalid safelink event type')
221
+ }
222
+
223
+ export type SafelinkEventType = 'addRule' | 'updateRule' | 'removeRule'
224
+ export type SafelinkPatternType = 'domain' | 'url'
225
+ export type SafelinkActionType = 'block' | 'warn' | 'whitelist'
226
+ export type SafelinkReasonType = 'csam' | 'spam' | 'phishing' | 'none'
227
+
228
+ const safelinkPatterns = new Set(['domain', 'url'])
229
+ const safelinkActions = new Set(['block', 'warn', 'whitelist'])
230
+ const safelinkReasons = new Set(['csam', 'spam', 'phishing', 'none'])
231
+ const safelinkEventTypes = new Set(['addRule', 'updateRule', 'removeRule'])
package/src/context.ts CHANGED
@@ -17,6 +17,10 @@ import { BlobDiverter } from './daemon/blob-diverter'
17
17
  import { Database } from './db'
18
18
  import { ImageInvalidator } from './image-invalidator'
19
19
  import { ModerationService, ModerationServiceCreator } from './mod-service'
20
+ import {
21
+ SafelinkRuleService,
22
+ SafelinkRuleServiceCreator,
23
+ } from './safelink/service'
20
24
  import { Sequencer } from './sequencer/sequencer'
21
25
  import { SetService, SetServiceCreator } from './set/service'
22
26
  import { SettingService, SettingServiceCreator } from './setting/service'
@@ -42,6 +46,7 @@ export type AppContextOptions = {
42
46
  cfg: OzoneConfig
43
47
  modService: ModerationServiceCreator
44
48
  communicationTemplateService: CommunicationTemplateServiceCreator
49
+ safelinkRuleService: SafelinkRuleServiceCreator
45
50
  setService: SetServiceCreator
46
51
  settingService: SettingServiceCreator
47
52
  teamService: TeamServiceCreator
@@ -130,6 +135,7 @@ export class AppContext {
130
135
  )
131
136
 
132
137
  const communicationTemplateService = CommunicationTemplateService.creator()
138
+ const safelinkRuleService = SafelinkRuleService.creator()
133
139
  const teamService = TeamService.creator(
134
140
  appviewAgent,
135
141
  cfg.appview.did,
@@ -154,6 +160,7 @@ export class AppContext {
154
160
  cfg,
155
161
  modService,
156
162
  communicationTemplateService,
163
+ safelinkRuleService,
157
164
  teamService,
158
165
  setService,
159
166
  settingService,
@@ -204,6 +211,10 @@ export class AppContext {
204
211
  return this.opts.communicationTemplateService
205
212
  }
206
213
 
214
+ get safelinkRuleService(): SafelinkRuleServiceCreator {
215
+ return this.opts.safelinkRuleService
216
+ }
217
+
207
218
  get teamService(): TeamServiceCreator {
208
219
  return this.opts.teamService
209
220
  }
@@ -0,0 +1,53 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ await db.schema
5
+ .createTable('safelink_event')
6
+ .addColumn('id', 'bigserial', (col) => col.primaryKey())
7
+ .addColumn('eventType', 'varchar', (col) => col.notNull())
8
+ .addColumn('url', 'varchar', (col) => col.notNull())
9
+ .addColumn('pattern', 'varchar', (col) => col.notNull())
10
+ .addColumn('action', 'varchar', (col) => col.notNull())
11
+ .addColumn('reason', 'varchar', (col) => col.notNull())
12
+ .addColumn('createdBy', 'varchar', (col) => col.notNull())
13
+ .addColumn('createdAt', 'varchar', (col) => col.notNull())
14
+ .addColumn('comment', 'text')
15
+ .execute()
16
+
17
+ await db.schema
18
+ .createTable('safelink_rule')
19
+ .addColumn('id', 'bigserial', (col) => col.primaryKey())
20
+ .addColumn('url', 'varchar', (col) => col.notNull())
21
+ .addColumn('pattern', 'varchar', (col) => col.notNull())
22
+ .addColumn('action', 'varchar', (col) => col.notNull())
23
+ .addColumn('reason', 'varchar', (col) => col.notNull())
24
+ .addColumn('createdBy', 'varchar', (col) => col.notNull())
25
+ .addColumn('createdAt', 'varchar', (col) => col.notNull())
26
+ .addColumn('updatedAt', 'varchar', (col) => col.notNull())
27
+ .addColumn('comment', 'text')
28
+ .addUniqueConstraint('safelink_rule_url_pattern_key', ['url', 'pattern'])
29
+ .execute()
30
+
31
+ await db.schema
32
+ .createIndex('safelink_event_url_pattern_idx')
33
+ .on('safelink_event')
34
+ .columns(['url', 'pattern'])
35
+ .execute()
36
+
37
+ await db.schema
38
+ .createIndex('safelink_rule_action_idx')
39
+ .on('safelink_rule')
40
+ .column('action')
41
+ .execute()
42
+
43
+ await db.schema
44
+ .createIndex('safelink_rule_reason_idx')
45
+ .on('safelink_rule')
46
+ .column('reason')
47
+ .execute()
48
+ }
49
+
50
+ export async function down(db: Kysely<unknown>): Promise<void> {
51
+ await db.schema.dropTable('safelink_rule').execute()
52
+ await db.schema.dropTable('safelink_event').execute()
53
+ }
@@ -25,3 +25,4 @@ export * as _20250221T132135150Z from './20250221T132135150Z-member-details'
25
25
  export * as _20250404T201720309Z from './20250404T201720309Z-subject-status-sort-idxs'
26
26
  export * as _20250415T201720309Z from './20250415T201720309Z-verification'
27
27
  export * as _20250417T201720309Z from './20250417T201720309Z-firehose-cursor'
28
+ export * as _20250609T110704000Z from './20250609T110704000Z-safelink'
@@ -13,6 +13,7 @@ import * as set from './ozone_set'
13
13
  import * as recordEventsStats from './record_events_stats'
14
14
  import * as recordPushEvent from './record_push_event'
15
15
  import * as repoPushEvent from './repo_push_event'
16
+ import * as safelink from './safelink'
16
17
  import * as setting from './setting'
17
18
  import * as signingKey from './signing_key'
18
19
  import * as verification from './verification'
@@ -33,7 +34,8 @@ export type DatabaseSchemaType = modEvent.PartialDB &
33
34
  accountRecordEventsStats.PartialDB &
34
35
  accountRecordStatusStats.PartialDB &
35
36
  verification.PartialDB &
36
- firehoseCursor.PartialDB
37
+ firehoseCursor.PartialDB &
38
+ safelink.PartialDB
37
39
 
38
40
  export type DatabaseSchema = Kysely<DatabaseSchemaType>
39
41