@kkuffour/solid-moderation-plugin 0.2.3 → 0.3.0
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/.data/.internal/idp/keys/cookie-secret$.json +1 -0
- package/.data/.internal/idp/keys/jwks$.json +1 -0
- package/.data/.internal/setup/current-base-url$.json +1 -0
- package/PLUGIN_DEVELOPER_GUIDE.md +213 -0
- package/README.md +25 -28
- package/components/context.jsonld +11 -0
- package/config/default.json +14 -28
- package/dist/ModerationHandler.d.ts +21 -0
- package/dist/ModerationHandler.d.ts.map +1 -0
- package/dist/ModerationHandler.js +158 -0
- package/dist/ModerationHandler.js.map +1 -0
- package/dist/ModerationHandler.jsonld +126 -0
- package/dist/components/components.jsonld +4 -9
- package/dist/components/context.jsonld +6 -245
- package/dist/index.d.ts +1 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/providers/SightEngineProvider.jsonld +2 -2
- package/package.json +10 -10
- package/src/ModerationHandler.ts +189 -0
- package/src/index.ts +1 -6
- package/ARCHITECTURE.md +0 -52
- package/CONFIG-GUIDE.md +0 -49
- package/DEVELOPMENT.md +0 -129
- package/ENV-VARIABLES.md +0 -137
- package/INSTALLATION.md +0 -90
- package/MIGRATION.md +0 -81
- package/PRODUCTION.md +0 -186
- package/PUBLISHING.md +0 -104
- package/TESTING.md +0 -93
- package/css-moderation-spec.md +0 -1422
- package/dist/ModerationConfig.d.ts +0 -16
- package/dist/ModerationConfig.d.ts.map +0 -1
- package/dist/ModerationConfig.js +0 -18
- package/dist/ModerationConfig.js.map +0 -1
- package/dist/ModerationConfig.jsonld +0 -66
- package/dist/ModerationMixin.d.ts +0 -13
- package/dist/ModerationMixin.d.ts.map +0 -1
- package/dist/ModerationMixin.js +0 -136
- package/dist/ModerationMixin.js.map +0 -1
- package/dist/ModerationMixin.jsonld +0 -180
- package/dist/ModerationOperationHandler.d.ts +0 -16
- package/dist/ModerationOperationHandler.d.ts.map +0 -1
- package/dist/ModerationOperationHandler.js +0 -45
- package/dist/ModerationOperationHandler.js.map +0 -1
- package/dist/ModerationOperationHandler.jsonld +0 -140
- package/dist/ModerationRecord.d.ts +0 -20
- package/dist/ModerationRecord.d.ts.map +0 -1
- package/dist/ModerationRecord.js +0 -3
- package/dist/ModerationRecord.js.map +0 -1
- package/dist/ModerationRecord.jsonld +0 -59
- package/dist/ModerationResourceStore.d.ts +0 -30
- package/dist/ModerationResourceStore.d.ts.map +0 -1
- package/dist/ModerationResourceStore.js +0 -167
- package/dist/ModerationResourceStore.js.map +0 -1
- package/dist/ModerationResourceStore.jsonld +0 -157
- package/dist/ModerationStore.d.ts +0 -12
- package/dist/ModerationStore.d.ts.map +0 -1
- package/dist/ModerationStore.js +0 -37
- package/dist/ModerationStore.js.map +0 -1
- package/dist/ModerationStore.jsonld +0 -59
- package/dist/util/GuardedStream.d.ts +0 -33
- package/dist/util/GuardedStream.d.ts.map +0 -1
- package/dist/util/GuardedStream.js +0 -89
- package/dist/util/GuardedStream.js.map +0 -1
- package/simple-test.json +0 -7
- package/src/ModerationConfig.ts +0 -29
- package/src/ModerationMixin.ts +0 -153
- package/src/ModerationOperationHandler.ts +0 -64
- package/src/ModerationRecord.ts +0 -19
- package/src/ModerationResourceStore.ts +0 -227
- package/src/ModerationStore.ts +0 -41
- package/src/util/GuardedStream.ts +0 -101
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"key":"idp/keys/cookie-secret","payload":["59d073a4fc66124c74d4d8f2d32f0383ef82d91122b25ee81ad691a46dc785203c2720e6e47d9ab7f19bf8970f460cb672267c6c47064e91f36285542b8d694b"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"key":"idp/keys/jwks","payload":{"keys":[{"kty":"EC","x":"ttiH782m1M7AgKeQtsBJbJ6wx453vyKjJxho6vS7GpQ","y":"38doyGzPtVycO8Su7jVHDiY3n1zDofPNlUqYBut8FSA","crv":"P-256","d":"lVK7R5xL1Oph5Vh7FZPOZNwr24QTRu3HECdC8DLKd9Y","alg":"ES256"}]}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"key":"setup/current-base-url","payload":"http://localhost:3009/"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Plugin Developer Guide: Using the ExtensionHandler
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
To create a plugin that extends Community Solid Server functionality:
|
|
6
|
+
|
|
7
|
+
### 1. Create Your Handler Class
|
|
8
|
+
|
|
9
|
+
Your handler must extend `OperationHandler` from CSS:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { OperationHandler } from '@solid/community-server';
|
|
13
|
+
import type { OperationHandlerInput, ResponseDescription } from '@solid/community-server';
|
|
14
|
+
import { NotFoundHttpError } from '@solid/community-server';
|
|
15
|
+
|
|
16
|
+
export class MyPluginHandler extends OperationHandler {
|
|
17
|
+
public async handle(input: OperationHandlerInput): Promise<ResponseDescription> {
|
|
18
|
+
const { operation } = input;
|
|
19
|
+
|
|
20
|
+
// Your custom logic here
|
|
21
|
+
// Example: Check if operation should be handled
|
|
22
|
+
if (this.shouldHandle(operation)) {
|
|
23
|
+
// Process the operation
|
|
24
|
+
return this.processOperation(operation);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Pass to next handler in chain
|
|
28
|
+
throw new NotFoundHttpError();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private shouldHandle(operation: Operation): boolean {
|
|
32
|
+
// Your logic to determine if this operation should be handled
|
|
33
|
+
return operation.method === 'POST' || operation.method === 'PUT';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async processOperation(operation: Operation): Promise<ResponseDescription> {
|
|
37
|
+
// Your processing logic
|
|
38
|
+
// Return a ResponseDescription or throw an error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Create Components.js Configuration
|
|
44
|
+
|
|
45
|
+
Create a config file (e.g., `config/default.json`) in your plugin package:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
|
50
|
+
"import": ["css:config/file.json"],
|
|
51
|
+
"@graph": [
|
|
52
|
+
{
|
|
53
|
+
"@id": "urn:solid-server:default:ExtensionHandler",
|
|
54
|
+
"@type": "MyPluginHandler",
|
|
55
|
+
"MyPluginHandler:config": { "@id": "urn:my-plugin:config" }
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"@id": "urn:my-plugin:config",
|
|
59
|
+
"@type": "MyPluginConfig",
|
|
60
|
+
"MyPluginConfig:enabled": true,
|
|
61
|
+
"MyPluginConfig:apiKey": "@ENV:MY_PLUGIN_API_KEY"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Package Structure
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
my-solid-plugin/
|
|
71
|
+
├── package.json
|
|
72
|
+
├── src/
|
|
73
|
+
│ ├── MyPluginHandler.ts
|
|
74
|
+
│ └── index.ts
|
|
75
|
+
├── config/
|
|
76
|
+
│ └── default.json
|
|
77
|
+
└── components/
|
|
78
|
+
└── context.jsonld
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. Package.json Setup
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"name": "@myorg/solid-plugin",
|
|
86
|
+
"version": "1.0.0",
|
|
87
|
+
"main": "dist/index.js",
|
|
88
|
+
"types": "dist/index.d.ts",
|
|
89
|
+
"lsd:module": true,
|
|
90
|
+
"lsd:components": "components/components.jsonld",
|
|
91
|
+
"lsd:contexts": {
|
|
92
|
+
"https://linkedsoftwaredependencies.org/bundles/npm/@myorg/solid-plugin/^1.0.0/components/context.jsonld": "components/context.jsonld"
|
|
93
|
+
},
|
|
94
|
+
"peerDependencies": {
|
|
95
|
+
"@solid/community-server": "^7.0.0"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. User Installation
|
|
101
|
+
|
|
102
|
+
Users install both CSS and your plugin:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm install @solid/community-server
|
|
106
|
+
npm install @myorg/solid-plugin
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 6. User Configuration
|
|
110
|
+
|
|
111
|
+
Users run CSS with your plugin config:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx community-solid-server \
|
|
115
|
+
-c @css:config/file.json \
|
|
116
|
+
-c @myorg/solid-plugin:config/default.json \
|
|
117
|
+
-f data/ -p 3000
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or create a combined config file:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
|
125
|
+
"import": [
|
|
126
|
+
"css:config/file.json",
|
|
127
|
+
"@myorg/solid-plugin:config/default.json"
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Key Points
|
|
133
|
+
|
|
134
|
+
### The Extension Point
|
|
135
|
+
|
|
136
|
+
**URN**: `urn:solid-server:default:ExtensionHandler`
|
|
137
|
+
|
|
138
|
+
**Interface**: `OperationHandler`
|
|
139
|
+
|
|
140
|
+
**Position**: First in the handler chain (runs before GET, POST, PUT, etc.)
|
|
141
|
+
|
|
142
|
+
**Behavior**: Your handler is called for EVERY operation. If you don't handle it, throw `NotFoundHttpError` to pass to the next handler.
|
|
143
|
+
|
|
144
|
+
### Handler Chain Flow
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Request → ExtensionHandler (your plugin) → GET/POST/PUT/etc handlers → Response
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
If your handler throws `NotFoundHttpError`, the request continues to standard handlers.
|
|
151
|
+
|
|
152
|
+
If your handler returns a `ResponseDescription`, the request is complete.
|
|
153
|
+
|
|
154
|
+
### What You Can Do
|
|
155
|
+
|
|
156
|
+
- **Content moderation**: Scan uploads before they're stored
|
|
157
|
+
- **Rate limiting**: Track and limit requests per user
|
|
158
|
+
- **Virus scanning**: Check files for malware
|
|
159
|
+
- **Custom validation**: Enforce business rules
|
|
160
|
+
- **Analytics**: Log operations for analysis
|
|
161
|
+
- **Webhooks**: Notify external services
|
|
162
|
+
- **Data transformation**: Modify data before storage
|
|
163
|
+
|
|
164
|
+
### What You Cannot Do
|
|
165
|
+
|
|
166
|
+
- Override core CSS components (GET/POST/PUT handlers, ResourceStore, etc.)
|
|
167
|
+
- Change the handler chain order
|
|
168
|
+
- Remove the extension point
|
|
169
|
+
|
|
170
|
+
## Example: Content Moderation Plugin
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
export class ModerationHandler extends OperationHandler {
|
|
174
|
+
private readonly moderationClient: ModerationClient;
|
|
175
|
+
|
|
176
|
+
public constructor(moderationClient: ModerationClient) {
|
|
177
|
+
super();
|
|
178
|
+
this.moderationClient = moderationClient;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
|
182
|
+
// Only moderate POST and PUT operations with content
|
|
183
|
+
if ((operation.method === 'POST' || operation.method === 'PUT') && operation.body) {
|
|
184
|
+
const result = await this.moderationClient.scan(operation.body);
|
|
185
|
+
|
|
186
|
+
if (result.isInappropriate) {
|
|
187
|
+
throw new ForbiddenHttpError(`Content blocked: ${result.reason}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Pass to next handler
|
|
192
|
+
throw new NotFoundHttpError();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Testing Your Plugin
|
|
198
|
+
|
|
199
|
+
1. Install CSS and your plugin locally
|
|
200
|
+
2. Create a test config that uses your plugin
|
|
201
|
+
3. Start the server: `npm start -- -c test-config.json -f data/ -p 3000`
|
|
202
|
+
4. Test operations: `curl -X POST http://localhost:3000/test -d "data"`
|
|
203
|
+
5. Verify your handler is called and behaves correctly
|
|
204
|
+
|
|
205
|
+
## Reference Implementation
|
|
206
|
+
|
|
207
|
+
See `@solid/access-token-verifier` for a complete example of a CSS plugin package.
|
|
208
|
+
|
|
209
|
+
## Support
|
|
210
|
+
|
|
211
|
+
- CSS Documentation: https://communitysolidserver.github.io/CommunitySolidServer/
|
|
212
|
+
- Components.js: https://componentsjs.readthedocs.io/
|
|
213
|
+
- CSS Discussions: https://github.com/CommunitySolidServer/CommunitySolidServer/discussions
|
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# @solid
|
|
1
|
+
# @kkuffour/solid-moderation-plugin
|
|
2
2
|
|
|
3
3
|
Content moderation plugin for Community Solid Server using SightEngine API.
|
|
4
4
|
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
✅ **Functional** - Uses CSS ExtensionHandler pattern (v0.3.0+)
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
|
-
npm install @solid/
|
|
12
|
+
npm install @solid/community-server
|
|
13
|
+
npm install @kkuffour/solid-moderation-plugin
|
|
9
14
|
```
|
|
10
15
|
|
|
11
16
|
## Configuration
|
|
@@ -13,41 +18,33 @@ npm install @solid/moderation-plugin
|
|
|
13
18
|
Set environment variables:
|
|
14
19
|
|
|
15
20
|
```bash
|
|
16
|
-
export SIGHTENGINE_API_USER=your_api_user
|
|
17
|
-
export SIGHTENGINE_API_SECRET=your_api_secret
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
Create a configuration file that imports the plugin:
|
|
21
|
-
|
|
22
|
-
```json
|
|
23
|
-
{
|
|
24
|
-
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
|
25
|
-
"import": [
|
|
26
|
-
"css:config/file.json",
|
|
27
|
-
"@solid/moderation-plugin:config/default.json"
|
|
28
|
-
]
|
|
29
|
-
}
|
|
21
|
+
export SIGHTENGINE_API_USER="your_api_user"
|
|
22
|
+
export SIGHTENGINE_API_SECRET="your_api_secret"
|
|
30
23
|
```
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
Run CSS with the plugin:
|
|
33
26
|
|
|
34
27
|
```bash
|
|
35
|
-
npx
|
|
28
|
+
npx community-solid-server \
|
|
29
|
+
-c @css:config/file.json \
|
|
30
|
+
-c @kkuffour/solid-moderation-plugin:config/default.json \
|
|
31
|
+
-f data/ -p 3000
|
|
36
32
|
```
|
|
37
33
|
|
|
38
|
-
##
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
The plugin uses CSS's `ExtensionHandler` pattern to intercept POST/PUT/PATCH operations before they reach storage:
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- Audit logging
|
|
45
|
-
- Fail-open policy (allows content if API fails)
|
|
38
|
+
1. Handler runs first in the operation chain
|
|
39
|
+
2. Moderates image/video/text content using SightEngine API
|
|
40
|
+
3. Blocks inappropriate content with 403 Forbidden
|
|
41
|
+
4. Passes approved content to standard handlers
|
|
46
42
|
|
|
47
|
-
##
|
|
43
|
+
## Components
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
- `ModerationHandler` - OperationHandler that moderates POST/PUT/PATCH operations
|
|
46
|
+
- `SightEngineProvider` - SightEngine API client
|
|
50
47
|
|
|
51
48
|
## License
|
|
52
49
|
|
|
53
|
-
MIT
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@context": [
|
|
3
|
+
"https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
|
4
|
+
{
|
|
5
|
+
"ksmp": "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/",
|
|
6
|
+
|
|
7
|
+
"ModerationHandler": "ksmp:ModerationHandler",
|
|
8
|
+
"SightEngineProvider": "ksmp:SightEngineProvider"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
package/config/default.json
CHANGED
|
@@ -1,39 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"@context": [
|
|
3
3
|
"https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
|
4
|
-
"https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.
|
|
5
|
-
],
|
|
6
|
-
"import": [
|
|
7
|
-
"css:config/file.json"
|
|
4
|
+
"https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/components/context.jsonld"
|
|
8
5
|
],
|
|
9
6
|
"@graph": [
|
|
10
7
|
{
|
|
11
|
-
"
|
|
12
|
-
"@id": "urn:solid-
|
|
13
|
-
"@type": "
|
|
14
|
-
"
|
|
15
|
-
"
|
|
8
|
+
"comment": "TEST: If you see this in logs, config is loaded",
|
|
9
|
+
"@id": "urn:solid-server:default:ExtensionHandler",
|
|
10
|
+
"@type": "ModerationHandler",
|
|
11
|
+
"ModerationHandler:_client": { "@id": "urn:moderation:SightEngineClient" },
|
|
12
|
+
"ModerationHandler:_enabled": true,
|
|
13
|
+
"ModerationHandler:_imageNudityThreshold": 0.1,
|
|
14
|
+
"ModerationHandler:_textSexualThreshold": 0.1,
|
|
15
|
+
"ModerationHandler:_textToxicThreshold": 0.1,
|
|
16
|
+
"ModerationHandler:_videoNudityThreshold": 0.1
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
|
-
"@
|
|
19
|
-
"@
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" },
|
|
23
|
-
"accessor": { "@id": "urn:solid-server:default:DataAccessor" },
|
|
24
|
-
"metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" }
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"@comment": "Moderation wrapper for ResourceStore",
|
|
28
|
-
"@id": "urn:solid-server:default:ResourceStore",
|
|
29
|
-
"@type": "ModerationResourceStore",
|
|
30
|
-
"ModerationResourceStore:_source": { "@id": "urn:solid-server:default:ResourceStore_Original" },
|
|
31
|
-
"ModerationResourceStore:_client": { "@id": "urn:solid-moderation:SightEngineProvider" },
|
|
32
|
-
"ModerationResourceStore:_enabled": true,
|
|
33
|
-
"ModerationResourceStore:_imageNudityThreshold": 0.5,
|
|
34
|
-
"ModerationResourceStore:_textSexualThreshold": 0.5,
|
|
35
|
-
"ModerationResourceStore:_textToxicThreshold": 0.5,
|
|
36
|
-
"ModerationResourceStore:_videoNudityThreshold": 0.5
|
|
19
|
+
"@id": "urn:moderation:SightEngineClient",
|
|
20
|
+
"@type": "SightEngineProvider",
|
|
21
|
+
"SightEngineProvider:_apiUser": "@ENV:SIGHTENGINE_API_USER",
|
|
22
|
+
"SightEngineProvider:_apiSecret": "@ENV:SIGHTENGINE_API_SECRET"
|
|
37
23
|
}
|
|
38
24
|
]
|
|
39
25
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { OperationHandler } from '@solid/community-server';
|
|
2
|
+
import type { OperationHandlerInput, ResponseDescription } from '@solid/community-server';
|
|
3
|
+
import type { SightEngineProvider } from './providers/SightEngineProvider';
|
|
4
|
+
export declare class ModerationHandler extends OperationHandler {
|
|
5
|
+
private readonly logger;
|
|
6
|
+
private readonly client;
|
|
7
|
+
private readonly enabled;
|
|
8
|
+
private readonly imageNudityThreshold;
|
|
9
|
+
private readonly textSexualThreshold;
|
|
10
|
+
private readonly textToxicThreshold;
|
|
11
|
+
private readonly videoNudityThreshold;
|
|
12
|
+
constructor(client: SightEngineProvider, enabled: boolean, imageNudityThreshold: number, textSexualThreshold: number, textToxicThreshold: number, videoNudityThreshold: number);
|
|
13
|
+
handle({ operation }: OperationHandlerInput): Promise<ResponseDescription>;
|
|
14
|
+
private shouldModerate;
|
|
15
|
+
private isModeratable;
|
|
16
|
+
private moderateImage;
|
|
17
|
+
private moderateVideo;
|
|
18
|
+
private moderateText;
|
|
19
|
+
private getFileExtension;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=ModerationHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModerationHandler.d.ts","sourceRoot":"","sources":["../src/ModerationHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0E,MAAM,yBAAyB,CAAC;AACnI,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE1F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAE3E,qBAAa,iBAAkB,SAAQ,gBAAgB;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAS;IAC7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;gBAG5C,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,OAAO,EAChB,oBAAoB,EAAE,MAAM,EAC5B,mBAAmB,EAAE,MAAM,EAC3B,kBAAkB,EAAE,MAAM,EAC1B,oBAAoB,EAAE,MAAM;IAejB,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,qBAAqB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAwDvF,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,aAAa;YAMP,aAAa;YAyBb,aAAa;YAyBb,YAAY;IAyB1B,OAAO,CAAC,gBAAgB;CAWzB"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ModerationHandler = void 0;
|
|
4
|
+
const community_server_1 = require("@solid/community-server");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
class ModerationHandler extends community_server_1.OperationHandler {
|
|
7
|
+
constructor(client, enabled, imageNudityThreshold, textSexualThreshold, textToxicThreshold, videoNudityThreshold) {
|
|
8
|
+
super();
|
|
9
|
+
this.logger = (0, community_server_1.getLoggerFor)(this);
|
|
10
|
+
this.client = client;
|
|
11
|
+
this.enabled = enabled;
|
|
12
|
+
this.imageNudityThreshold = imageNudityThreshold;
|
|
13
|
+
this.textSexualThreshold = textSexualThreshold;
|
|
14
|
+
this.textToxicThreshold = textToxicThreshold;
|
|
15
|
+
this.videoNudityThreshold = videoNudityThreshold;
|
|
16
|
+
this.logger.info('ModerationHandler initialized');
|
|
17
|
+
this.logger.info(` Enabled: ${enabled}`);
|
|
18
|
+
this.logger.info(` Thresholds: image=${imageNudityThreshold}, text=${textSexualThreshold}/${textToxicThreshold}, video=${videoNudityThreshold}`);
|
|
19
|
+
}
|
|
20
|
+
async handle({ operation }) {
|
|
21
|
+
this.logger.info(`[MODERATION] Handler called: ${operation.method} ${operation.target.path}`);
|
|
22
|
+
if (!this.enabled) {
|
|
23
|
+
this.logger.info('[MODERATION] Handler disabled, passing through');
|
|
24
|
+
throw new community_server_1.NotFoundHttpError();
|
|
25
|
+
}
|
|
26
|
+
if (!this.shouldModerate(operation)) {
|
|
27
|
+
this.logger.info(`[MODERATION] Operation not moderatable: ${operation.method}`);
|
|
28
|
+
throw new community_server_1.NotFoundHttpError();
|
|
29
|
+
}
|
|
30
|
+
const contentType = operation.body?.metadata.contentType;
|
|
31
|
+
if (!contentType || !this.isModeratable(contentType)) {
|
|
32
|
+
this.logger.info(`[MODERATION] Content type not moderatable: ${contentType}`);
|
|
33
|
+
throw new community_server_1.NotFoundHttpError();
|
|
34
|
+
}
|
|
35
|
+
this.logger.info(`[MODERATION] Starting moderation: ${operation.method} ${contentType} to ${operation.target.path}`);
|
|
36
|
+
try {
|
|
37
|
+
this.logger.info('[MODERATION] Reading request body...');
|
|
38
|
+
const chunks = [];
|
|
39
|
+
for await (const chunk of operation.body.data) {
|
|
40
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
41
|
+
}
|
|
42
|
+
const buffer = Buffer.concat(chunks);
|
|
43
|
+
this.logger.info(`[MODERATION] Body read: ${buffer.length} bytes`);
|
|
44
|
+
if (contentType.startsWith('image/')) {
|
|
45
|
+
this.logger.info('[MODERATION] Calling image moderation...');
|
|
46
|
+
await this.moderateImage(operation.target.path, buffer, contentType);
|
|
47
|
+
}
|
|
48
|
+
else if (contentType.startsWith('video/')) {
|
|
49
|
+
this.logger.info('[MODERATION] Calling video moderation...');
|
|
50
|
+
await this.moderateVideo(operation.target.path, buffer, contentType);
|
|
51
|
+
}
|
|
52
|
+
else if (contentType.startsWith('text/')) {
|
|
53
|
+
this.logger.info('[MODERATION] Calling text moderation...');
|
|
54
|
+
await this.moderateText(operation.target.path, buffer);
|
|
55
|
+
}
|
|
56
|
+
operation.body.data = (0, community_server_1.guardedStreamFrom)(buffer);
|
|
57
|
+
this.logger.info(`[MODERATION] ✅ Content APPROVED: ${operation.target.path}`);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof community_server_1.ForbiddenHttpError) {
|
|
61
|
+
this.logger.warn(`[MODERATION] ❌ Content BLOCKED: ${operation.target.path} - ${error.message}`);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
this.logger.error(`[MODERATION] ⚠️ Moderation failed: ${error.message}`);
|
|
65
|
+
this.logger.warn('[MODERATION] Allowing content through (fail-open policy)');
|
|
66
|
+
}
|
|
67
|
+
throw new community_server_1.NotFoundHttpError();
|
|
68
|
+
}
|
|
69
|
+
shouldModerate(operation) {
|
|
70
|
+
return (operation.method === 'POST' || operation.method === 'PUT' || operation.method === 'PATCH') &&
|
|
71
|
+
operation.body;
|
|
72
|
+
}
|
|
73
|
+
isModeratable(contentType) {
|
|
74
|
+
return contentType.startsWith('image/') ||
|
|
75
|
+
contentType.startsWith('video/') ||
|
|
76
|
+
contentType.startsWith('text/');
|
|
77
|
+
}
|
|
78
|
+
async moderateImage(path, buffer, contentType) {
|
|
79
|
+
const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
|
|
80
|
+
this.logger.info(`[MODERATION] Writing temp file: ${tempFile}`);
|
|
81
|
+
(0, fs_1.writeFileSync)(tempFile, buffer);
|
|
82
|
+
try {
|
|
83
|
+
this.logger.info('[MODERATION] Calling SightEngine API for image analysis...');
|
|
84
|
+
const result = await this.client.analyzeImage(tempFile);
|
|
85
|
+
this.logger.info(`[MODERATION] SightEngine response: ${JSON.stringify(result)}`);
|
|
86
|
+
if (result.nudity?.raw && result.nudity.raw > this.imageNudityThreshold) {
|
|
87
|
+
this.logger.warn(`[MODERATION] Image BLOCKED: ${path} - nudity ${result.nudity.raw.toFixed(2)} > ${this.imageNudityThreshold}`);
|
|
88
|
+
throw new community_server_1.ForbiddenHttpError(`Image contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
|
|
89
|
+
}
|
|
90
|
+
this.logger.info(`[MODERATION] Image passed: nudity ${result.nudity?.raw?.toFixed(2) || 'N/A'} <= ${this.imageNudityThreshold}`);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
try {
|
|
94
|
+
(0, fs_1.unlinkSync)(tempFile);
|
|
95
|
+
this.logger.info(`[MODERATION] Cleaned up temp file: ${tempFile}`);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
this.logger.warn(`[MODERATION] Failed to cleanup: ${tempFile}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async moderateVideo(path, buffer, contentType) {
|
|
103
|
+
const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
|
|
104
|
+
this.logger.info(`[MODERATION] Writing temp file: ${tempFile}`);
|
|
105
|
+
(0, fs_1.writeFileSync)(tempFile, buffer);
|
|
106
|
+
try {
|
|
107
|
+
this.logger.info('[MODERATION] Calling SightEngine API for video analysis...');
|
|
108
|
+
const result = await this.client.analyzeVideo(tempFile);
|
|
109
|
+
this.logger.info(`[MODERATION] SightEngine response: ${JSON.stringify(result)}`);
|
|
110
|
+
if (result.nudity?.raw && result.nudity.raw > this.videoNudityThreshold) {
|
|
111
|
+
this.logger.warn(`[MODERATION] Video BLOCKED: ${path} - nudity ${result.nudity.raw.toFixed(2)} > ${this.videoNudityThreshold}`);
|
|
112
|
+
throw new community_server_1.ForbiddenHttpError(`Video contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
|
|
113
|
+
}
|
|
114
|
+
this.logger.info(`[MODERATION] Video passed: nudity ${result.nudity?.raw?.toFixed(2) || 'N/A'} <= ${this.videoNudityThreshold}`);
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
try {
|
|
118
|
+
(0, fs_1.unlinkSync)(tempFile);
|
|
119
|
+
this.logger.info(`[MODERATION] Cleaned up temp file: ${tempFile}`);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
this.logger.warn(`[MODERATION] Failed to cleanup: ${tempFile}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async moderateText(path, buffer) {
|
|
127
|
+
const text = buffer.toString('utf-8');
|
|
128
|
+
this.logger.info(`[MODERATION] Text length: ${text.length} chars`);
|
|
129
|
+
this.logger.info('[MODERATION] Calling SightEngine API for text analysis...');
|
|
130
|
+
const result = await this.client.analyzeText(text);
|
|
131
|
+
this.logger.info(`[MODERATION] SightEngine response: ${JSON.stringify(result)}`);
|
|
132
|
+
const violations = [];
|
|
133
|
+
if (result.sexual > this.textSexualThreshold) {
|
|
134
|
+
violations.push(`sexual (${result.sexual.toFixed(2)} > ${this.textSexualThreshold})`);
|
|
135
|
+
}
|
|
136
|
+
if (result.toxic > this.textToxicThreshold) {
|
|
137
|
+
violations.push(`toxic (${result.toxic.toFixed(2)} > ${this.textToxicThreshold})`);
|
|
138
|
+
}
|
|
139
|
+
if (violations.length > 0) {
|
|
140
|
+
this.logger.warn(`[MODERATION] Text BLOCKED: ${path} - ${violations.join(', ')}`);
|
|
141
|
+
throw new community_server_1.ForbiddenHttpError(`Text contains inappropriate content: ${violations.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
this.logger.info(`[MODERATION] Text passed: sexual ${result.sexual.toFixed(2)}, toxic ${result.toxic.toFixed(2)}`);
|
|
144
|
+
}
|
|
145
|
+
getFileExtension(contentType) {
|
|
146
|
+
const map = {
|
|
147
|
+
'image/jpeg': 'jpg',
|
|
148
|
+
'image/png': 'png',
|
|
149
|
+
'image/gif': 'gif',
|
|
150
|
+
'image/webp': 'webp',
|
|
151
|
+
'video/mp4': 'mp4',
|
|
152
|
+
'video/webm': 'webm',
|
|
153
|
+
};
|
|
154
|
+
return map[contentType] || 'bin';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.ModerationHandler = ModerationHandler;
|
|
158
|
+
//# sourceMappingURL=ModerationHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModerationHandler.js","sourceRoot":"","sources":["../src/ModerationHandler.ts"],"names":[],"mappings":"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"}
|