@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,189 @@
|
|
|
1
|
+
import { OperationHandler, getLoggerFor, ForbiddenHttpError, NotFoundHttpError, guardedStreamFrom } from '@solid/community-server';
|
|
2
|
+
import type { OperationHandlerInput, ResponseDescription } from '@solid/community-server';
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
4
|
+
import type { SightEngineProvider } from './providers/SightEngineProvider';
|
|
5
|
+
|
|
6
|
+
export class ModerationHandler extends OperationHandler {
|
|
7
|
+
private readonly logger = getLoggerFor(this);
|
|
8
|
+
private readonly client: SightEngineProvider;
|
|
9
|
+
private readonly enabled: boolean;
|
|
10
|
+
private readonly imageNudityThreshold: number;
|
|
11
|
+
private readonly textSexualThreshold: number;
|
|
12
|
+
private readonly textToxicThreshold: number;
|
|
13
|
+
private readonly videoNudityThreshold: number;
|
|
14
|
+
|
|
15
|
+
public constructor(
|
|
16
|
+
client: SightEngineProvider,
|
|
17
|
+
enabled: boolean,
|
|
18
|
+
imageNudityThreshold: number,
|
|
19
|
+
textSexualThreshold: number,
|
|
20
|
+
textToxicThreshold: number,
|
|
21
|
+
videoNudityThreshold: number,
|
|
22
|
+
) {
|
|
23
|
+
super();
|
|
24
|
+
this.client = client;
|
|
25
|
+
this.enabled = enabled;
|
|
26
|
+
this.imageNudityThreshold = imageNudityThreshold;
|
|
27
|
+
this.textSexualThreshold = textSexualThreshold;
|
|
28
|
+
this.textToxicThreshold = textToxicThreshold;
|
|
29
|
+
this.videoNudityThreshold = videoNudityThreshold;
|
|
30
|
+
|
|
31
|
+
this.logger.info('ModerationHandler initialized');
|
|
32
|
+
this.logger.info(` Enabled: ${enabled}`);
|
|
33
|
+
this.logger.info(` Thresholds: image=${imageNudityThreshold}, text=${textSexualThreshold}/${textToxicThreshold}, video=${videoNudityThreshold}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async handle({ operation }: OperationHandlerInput): Promise<ResponseDescription> {
|
|
37
|
+
this.logger.info(`[MODERATION] Handler called: ${operation.method} ${operation.target.path}`);
|
|
38
|
+
|
|
39
|
+
if (!this.enabled) {
|
|
40
|
+
this.logger.info('[MODERATION] Handler disabled, passing through');
|
|
41
|
+
throw new NotFoundHttpError();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!this.shouldModerate(operation)) {
|
|
45
|
+
this.logger.info(`[MODERATION] Operation not moderatable: ${operation.method}`);
|
|
46
|
+
throw new NotFoundHttpError();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const contentType = operation.body?.metadata.contentType;
|
|
50
|
+
if (!contentType || !this.isModeratable(contentType)) {
|
|
51
|
+
this.logger.info(`[MODERATION] Content type not moderatable: ${contentType}`);
|
|
52
|
+
throw new NotFoundHttpError();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.logger.info(`[MODERATION] Starting moderation: ${operation.method} ${contentType} to ${operation.target.path}`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
this.logger.info('[MODERATION] Reading request body...');
|
|
59
|
+
const chunks: Buffer[] = [];
|
|
60
|
+
for await (const chunk of operation.body!.data) {
|
|
61
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
62
|
+
}
|
|
63
|
+
const buffer = Buffer.concat(chunks);
|
|
64
|
+
this.logger.info(`[MODERATION] Body read: ${buffer.length} bytes`);
|
|
65
|
+
|
|
66
|
+
if (contentType.startsWith('image/')) {
|
|
67
|
+
this.logger.info('[MODERATION] Calling image moderation...');
|
|
68
|
+
await this.moderateImage(operation.target.path, buffer, contentType);
|
|
69
|
+
} else if (contentType.startsWith('video/')) {
|
|
70
|
+
this.logger.info('[MODERATION] Calling video moderation...');
|
|
71
|
+
await this.moderateVideo(operation.target.path, buffer, contentType);
|
|
72
|
+
} else if (contentType.startsWith('text/')) {
|
|
73
|
+
this.logger.info('[MODERATION] Calling text moderation...');
|
|
74
|
+
await this.moderateText(operation.target.path, buffer);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
operation.body!.data = guardedStreamFrom(buffer);
|
|
78
|
+
this.logger.info(`[MODERATION] ✅ Content APPROVED: ${operation.target.path}`);
|
|
79
|
+
|
|
80
|
+
} catch (error: unknown) {
|
|
81
|
+
if (error instanceof ForbiddenHttpError) {
|
|
82
|
+
this.logger.warn(`[MODERATION] ❌ Content BLOCKED: ${operation.target.path} - ${(error as Error).message}`);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
this.logger.error(`[MODERATION] ⚠️ Moderation failed: ${(error as Error).message}`);
|
|
86
|
+
this.logger.warn('[MODERATION] Allowing content through (fail-open policy)');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw new NotFoundHttpError();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private shouldModerate(operation: any): boolean {
|
|
93
|
+
return (operation.method === 'POST' || operation.method === 'PUT' || operation.method === 'PATCH') &&
|
|
94
|
+
operation.body;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private isModeratable(contentType: string): boolean {
|
|
98
|
+
return contentType.startsWith('image/') ||
|
|
99
|
+
contentType.startsWith('video/') ||
|
|
100
|
+
contentType.startsWith('text/');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async moderateImage(path: string, buffer: Buffer, contentType: string): Promise<void> {
|
|
104
|
+
const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
|
|
105
|
+
this.logger.info(`[MODERATION] Writing temp file: ${tempFile}`);
|
|
106
|
+
writeFileSync(tempFile, buffer);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
this.logger.info('[MODERATION] Calling SightEngine API for image analysis...');
|
|
110
|
+
const result = await this.client.analyzeImage(tempFile);
|
|
111
|
+
this.logger.info(`[MODERATION] SightEngine response: ${JSON.stringify(result)}`);
|
|
112
|
+
|
|
113
|
+
if (result.nudity?.raw && result.nudity.raw > this.imageNudityThreshold) {
|
|
114
|
+
this.logger.warn(`[MODERATION] Image BLOCKED: ${path} - nudity ${result.nudity.raw.toFixed(2)} > ${this.imageNudityThreshold}`);
|
|
115
|
+
throw new ForbiddenHttpError(`Image contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
|
|
116
|
+
}
|
|
117
|
+
this.logger.info(`[MODERATION] Image passed: nudity ${result.nudity?.raw?.toFixed(2) || 'N/A'} <= ${this.imageNudityThreshold}`);
|
|
118
|
+
} finally {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(tempFile);
|
|
121
|
+
this.logger.info(`[MODERATION] Cleaned up temp file: ${tempFile}`);
|
|
122
|
+
} catch {
|
|
123
|
+
this.logger.warn(`[MODERATION] Failed to cleanup: ${tempFile}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async moderateVideo(path: string, buffer: Buffer, contentType: string): Promise<void> {
|
|
129
|
+
const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
|
|
130
|
+
this.logger.info(`[MODERATION] Writing temp file: ${tempFile}`);
|
|
131
|
+
writeFileSync(tempFile, buffer);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
this.logger.info('[MODERATION] Calling SightEngine API for video analysis...');
|
|
135
|
+
const result = await this.client.analyzeVideo(tempFile);
|
|
136
|
+
this.logger.info(`[MODERATION] SightEngine response: ${JSON.stringify(result)}`);
|
|
137
|
+
|
|
138
|
+
if (result.nudity?.raw && result.nudity.raw > this.videoNudityThreshold) {
|
|
139
|
+
this.logger.warn(`[MODERATION] Video BLOCKED: ${path} - nudity ${result.nudity.raw.toFixed(2)} > ${this.videoNudityThreshold}`);
|
|
140
|
+
throw new ForbiddenHttpError(`Video contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
|
|
141
|
+
}
|
|
142
|
+
this.logger.info(`[MODERATION] Video passed: nudity ${result.nudity?.raw?.toFixed(2) || 'N/A'} <= ${this.videoNudityThreshold}`);
|
|
143
|
+
} finally {
|
|
144
|
+
try {
|
|
145
|
+
unlinkSync(tempFile);
|
|
146
|
+
this.logger.info(`[MODERATION] Cleaned up temp file: ${tempFile}`);
|
|
147
|
+
} catch {
|
|
148
|
+
this.logger.warn(`[MODERATION] Failed to cleanup: ${tempFile}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async moderateText(path: string, buffer: Buffer): Promise<void> {
|
|
154
|
+
const text = buffer.toString('utf-8');
|
|
155
|
+
this.logger.info(`[MODERATION] Text length: ${text.length} chars`);
|
|
156
|
+
|
|
157
|
+
this.logger.info('[MODERATION] Calling SightEngine API for text analysis...');
|
|
158
|
+
const result = await this.client.analyzeText(text);
|
|
159
|
+
this.logger.info(`[MODERATION] SightEngine response: ${JSON.stringify(result)}`);
|
|
160
|
+
|
|
161
|
+
const violations: string[] = [];
|
|
162
|
+
|
|
163
|
+
if (result.sexual > this.textSexualThreshold) {
|
|
164
|
+
violations.push(`sexual (${result.sexual.toFixed(2)} > ${this.textSexualThreshold})`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (result.toxic > this.textToxicThreshold) {
|
|
168
|
+
violations.push(`toxic (${result.toxic.toFixed(2)} > ${this.textToxicThreshold})`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (violations.length > 0) {
|
|
172
|
+
this.logger.warn(`[MODERATION] Text BLOCKED: ${path} - ${violations.join(', ')}`);
|
|
173
|
+
throw new ForbiddenHttpError(`Text contains inappropriate content: ${violations.join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
this.logger.info(`[MODERATION] Text passed: sexual ${result.sexual.toFixed(2)}, toxic ${result.toxic.toFixed(2)}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private getFileExtension(contentType: string): string {
|
|
179
|
+
const map: Record<string, string> = {
|
|
180
|
+
'image/jpeg': 'jpg',
|
|
181
|
+
'image/png': 'png',
|
|
182
|
+
'image/gif': 'gif',
|
|
183
|
+
'image/webp': 'webp',
|
|
184
|
+
'video/mp4': 'mp4',
|
|
185
|
+
'video/webm': 'webm',
|
|
186
|
+
};
|
|
187
|
+
return map[contentType] || 'bin';
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
export * from './
|
|
2
|
-
export * from './ModerationResourceStore';
|
|
3
|
-
export * from './ModerationConfig';
|
|
4
|
-
export * from './ModerationStore';
|
|
5
|
-
export * from './ModerationRecord';
|
|
6
|
-
export * from './ModerationMixin';
|
|
1
|
+
export * from './ModerationHandler';
|
|
7
2
|
export * from './providers/SightEngineProvider';
|
package/ARCHITECTURE.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Architecture Decision: ResourceStore vs OperationHandler
|
|
2
|
-
|
|
3
|
-
## Branch History
|
|
4
|
-
|
|
5
|
-
### `main` branch (v0.1.0 - v0.1.2)
|
|
6
|
-
**Approach**: OperationHandler wrapping
|
|
7
|
-
**Status**: ❌ Does not work
|
|
8
|
-
**Reason**: Components.js cannot resolve cross-module component overrides. When trying to override `urn:solid-server:default:PutOperationHandler` with a type from an external npm package, Components.js looks for the type in the CSS module, not the plugin module.
|
|
9
|
-
|
|
10
|
-
**Key Learning**: Components.js uses module-scoped component resolution. External plugins cannot override core CSS handler IDs.
|
|
11
|
-
|
|
12
|
-
### `resourcestore-approach` branch (v0.2.0+)
|
|
13
|
-
**Approach**: ResourceStore wrapping
|
|
14
|
-
**Status**: 🚧 In development
|
|
15
|
-
**Strategy**: Wrap `urn:solid-server:default:ResourceStore` instead of individual operation handlers.
|
|
16
|
-
|
|
17
|
-
## Why ResourceStore Works
|
|
18
|
-
|
|
19
|
-
1. **Single interception point**: All write operations (PUT/POST/PATCH) call `ResourceStore.setRepresentation()`
|
|
20
|
-
2. **Designed for swapping**: CSS architecture expects ResourceStore to be configurable/replaceable
|
|
21
|
-
3. **Proper abstraction boundary**: ResourceStore is the storage layer interface
|
|
22
|
-
4. **Components.js compatible**: ResourceStore wrapping is a supported pattern
|
|
23
|
-
|
|
24
|
-
## Implementation Plan
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
Request Flow:
|
|
28
|
-
HTTP Request → OperationHandler → ResourceStore → Backend
|
|
29
|
-
↑
|
|
30
|
-
INTERCEPT HERE
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Core Changes
|
|
34
|
-
- Create `ModerationResourceStore` implementing `ResourceStore` interface
|
|
35
|
-
- Wrap original store and intercept `setRepresentation()`
|
|
36
|
-
- Moderate content before passing to wrapped store
|
|
37
|
-
- Delegate all read operations (getRepresentation, etc.)
|
|
38
|
-
|
|
39
|
-
### Config Pattern
|
|
40
|
-
```json
|
|
41
|
-
{
|
|
42
|
-
"@id": "urn:solid-server:default:ResourceStore",
|
|
43
|
-
"@type": "ModerationResourceStore",
|
|
44
|
-
"source": { "@id": "urn:solid-server:default:ResourceStore_Original" }
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Version Strategy
|
|
49
|
-
|
|
50
|
-
- **v0.1.x**: OperationHandler approach (archived, doesn't work)
|
|
51
|
-
- **v0.2.x**: ResourceStore approach (current development)
|
|
52
|
-
- Users can install specific versions if needed: `npm install @kkuffour/solid-moderation-plugin@0.1.2`
|
package/CONFIG-GUIDE.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# Configuration Guide
|
|
2
|
-
|
|
3
|
-
## Understanding the Configuration Files
|
|
4
|
-
|
|
5
|
-
### `config/default.json` (Production - Installed with npm)
|
|
6
|
-
- Located in `node_modules/@solid/moderation-plugin/config/default.json` after installation
|
|
7
|
-
- Contains the complete moderation setup with default thresholds
|
|
8
|
-
- Imported by users in their CSS config using: `"@solid/moderation-plugin:config/default.json"`
|
|
9
|
-
- **This is what production users import**
|
|
10
|
-
|
|
11
|
-
### `config-with-moderation.json` (Local Testing Only)
|
|
12
|
-
- Located in the plugin source directory
|
|
13
|
-
- Used for testing the plugin locally before publishing
|
|
14
|
-
- Points to local files, not npm package
|
|
15
|
-
- **Not included in npm package**
|
|
16
|
-
|
|
17
|
-
## For Production Users
|
|
18
|
-
|
|
19
|
-
When you install the plugin via npm, you create a config file in **your CSS project**:
|
|
20
|
-
|
|
21
|
-
```json
|
|
22
|
-
{
|
|
23
|
-
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
|
|
24
|
-
"import": [
|
|
25
|
-
"css:config/file.json",
|
|
26
|
-
"@solid/moderation-plugin:config/default.json"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Then run:
|
|
32
|
-
```bash
|
|
33
|
-
export SIGHTENGINE_API_USER=your_api_user
|
|
34
|
-
export SIGHTENGINE_API_SECRET=your_api_secret
|
|
35
|
-
npx @solid/community-server -c config-with-moderation.json
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## For Local Development/Testing
|
|
39
|
-
|
|
40
|
-
When testing the plugin locally (before publishing):
|
|
41
|
-
|
|
42
|
-
1. Use `npm link` to link the local plugin
|
|
43
|
-
2. Use the `config-with-moderation.json` file in the plugin directory
|
|
44
|
-
3. Run CSS pointing to that config file
|
|
45
|
-
|
|
46
|
-
## Key Difference
|
|
47
|
-
|
|
48
|
-
- **Production**: Import `@solid/moderation-plugin:config/default.json` (from npm package)
|
|
49
|
-
- **Local Testing**: Use full path to `config-with-moderation.json` (from source directory)
|
package/DEVELOPMENT.md
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
# Development Notes
|
|
2
|
-
|
|
3
|
-
## Components.js Module Loading Issue
|
|
4
|
-
|
|
5
|
-
### Problem
|
|
6
|
-
Components.js cannot load modules via `npm link` due to symlink resolution issues. When using `npm link`, the plugin appears in `node_modules/@solid/moderation-plugin` as a symlink, but Components.js's module loader cannot follow it to instantiate classes.
|
|
7
|
-
|
|
8
|
-
### Symptoms
|
|
9
|
-
- Server starts without errors
|
|
10
|
-
- Config file is loaded successfully
|
|
11
|
-
- No moderation logs appear when uploading files
|
|
12
|
-
- ModerationOperationHandler is never instantiated
|
|
13
|
-
- Uploads succeed without any moderation checks
|
|
14
|
-
|
|
15
|
-
### Root Cause
|
|
16
|
-
Components.js uses its own module resolution that doesn't properly handle symlinked packages. The `requireElement` in the generated `.jsonld` files points to the class name, but Components.js cannot resolve the actual module path through the symlink.
|
|
17
|
-
|
|
18
|
-
### Solutions
|
|
19
|
-
|
|
20
|
-
#### Option 1: File Path Dependency (Recommended for Development)
|
|
21
|
-
Add to CSS's `package.json`:
|
|
22
|
-
```json
|
|
23
|
-
{
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@solid/moderation-plugin": "file:../solid-moderation"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Then run:
|
|
31
|
-
```bash
|
|
32
|
-
cd /Users/opendata/CommunitySolidServer
|
|
33
|
-
npm install
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
This creates a proper copy (not symlink) in node_modules.
|
|
37
|
-
|
|
38
|
-
**After code changes:**
|
|
39
|
-
```bash
|
|
40
|
-
cd /Users/opendata/solid-moderation
|
|
41
|
-
npm run build
|
|
42
|
-
cd /Users/opendata/CommunitySolidServer
|
|
43
|
-
npm install # Re-copies the updated files
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
#### Option 2: Publish to npm (Recommended for Production)
|
|
47
|
-
```bash
|
|
48
|
-
cd /Users/opendata/solid-moderation
|
|
49
|
-
npm publish
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
Then in CSS:
|
|
53
|
-
```bash
|
|
54
|
-
npm install @solid/moderation-plugin@latest
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
#### Option 3: Manual Copy (Quick Testing Only)
|
|
58
|
-
```bash
|
|
59
|
-
rm -rf /Users/opendata/CommunitySolidServer/node_modules/@solid/moderation-plugin
|
|
60
|
-
cp -r /Users/opendata/solid-moderation /Users/opendata/CommunitySolidServer/node_modules/@solid/moderation-plugin
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**Note:** Must repeat after every code change.
|
|
64
|
-
|
|
65
|
-
## Current Configuration
|
|
66
|
-
|
|
67
|
-
### Environment Variables
|
|
68
|
-
```bash
|
|
69
|
-
export SIGHTENGINE_API_USER="1060049443"
|
|
70
|
-
export SIGHTENGINE_API_SECRET="QRQ8HUmh4hyvhZjksBJq5ZaNYPLPEKXu"
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Thresholds (Configurable in config-with-moderation.json)
|
|
74
|
-
- **Image Nudity**: 0.1 (10% - very strict)
|
|
75
|
-
- **Text Sexual**: 0.1 (10% - very strict)
|
|
76
|
-
- **Text Toxic**: 0.1 (10% - very strict)
|
|
77
|
-
- **Video Nudity**: 0.1 (10% - very strict)
|
|
78
|
-
|
|
79
|
-
Lower values = stricter moderation (blocks more content)
|
|
80
|
-
Higher values = lenient moderation (blocks less content)
|
|
81
|
-
Range: 0.0 (block everything) to 1.0 (block nothing)
|
|
82
|
-
|
|
83
|
-
### Starting the Server
|
|
84
|
-
```bash
|
|
85
|
-
cd /Users/opendata/CommunitySolidServer
|
|
86
|
-
export SIGHTENGINE_API_USER="1060049443"
|
|
87
|
-
export SIGHTENGINE_API_SECRET="QRQ8HUmh4hyvhZjksBJq5ZaNYPLPEKXu"
|
|
88
|
-
npm start -- -c /Users/opendata/solid-moderation/config-with-moderation.json -f data/ -p 3009 -l debug > logs/server.log 2>&1 &
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Checking Logs
|
|
92
|
-
```bash
|
|
93
|
-
# Watch for moderation activity
|
|
94
|
-
tail -f logs/server.log | grep -i moderation
|
|
95
|
-
|
|
96
|
-
# Check if handler is invoked
|
|
97
|
-
grep "MODERATION: Checking" logs/server.log
|
|
98
|
-
|
|
99
|
-
# Check for API failures
|
|
100
|
-
grep "MODERATION:.*failed" logs/server.log
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Features Implemented
|
|
104
|
-
|
|
105
|
-
### Code Structure
|
|
106
|
-
- **ModerationOperationHandler**: Wraps POST/PUT/PATCH handlers
|
|
107
|
-
- **ModerationMixin**: Performs actual content checks
|
|
108
|
-
- **ModerationStore**: Audit logging for violations
|
|
109
|
-
- **SightEngineProvider**: API client for SightEngine
|
|
110
|
-
- **Configurable thresholds**: All thresholds adjustable via config
|
|
111
|
-
|
|
112
|
-
### Moderation Checks
|
|
113
|
-
- **Images**: Nudity detection
|
|
114
|
-
- **Text**: Sexual content and toxic language
|
|
115
|
-
- **Video**: Nudity detection
|
|
116
|
-
- **Fail-open policy**: Allows content if API fails
|
|
117
|
-
|
|
118
|
-
### Debug Logging
|
|
119
|
-
- Handler invocation logs
|
|
120
|
-
- Content type detection logs
|
|
121
|
-
- API response with scores
|
|
122
|
-
- Threshold comparison logs
|
|
123
|
-
- Failure reason logs
|
|
124
|
-
|
|
125
|
-
## Known Issues
|
|
126
|
-
|
|
127
|
-
1. **npm link doesn't work** - Use file path dependency instead
|
|
128
|
-
2. **Environment variables** - Must be set before starting server
|
|
129
|
-
3. **Audit logs** - Only record violations, not all checks
|
package/ENV-VARIABLES.md
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
# Environment Variables Explained
|
|
2
|
-
|
|
3
|
-
## What `export` Does
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
export SIGHTENGINE_API_USER=your_api_user
|
|
7
|
-
export SIGHTENGINE_API_SECRET=your_api_secret
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
These commands set **environment variables** in your shell session:
|
|
11
|
-
|
|
12
|
-
- `export` makes the variable available to all programs you run in that terminal
|
|
13
|
-
- `SIGHTENGINE_API_USER` is the variable name
|
|
14
|
-
- `your_api_user` is the value (replace with your actual API credentials)
|
|
15
|
-
|
|
16
|
-
## How the Plugin Uses These Variables
|
|
17
|
-
|
|
18
|
-
In `config/default.json`, you'll see:
|
|
19
|
-
|
|
20
|
-
```json
|
|
21
|
-
"ModerationConfig:_sightEngine": {
|
|
22
|
-
"ModerationConfig:_sightEngine_apiUser": "${SIGHTENGINE_API_USER}",
|
|
23
|
-
"ModerationConfig:_sightEngine_apiSecret": "${SIGHTENGINE_API_SECRET}"
|
|
24
|
-
}
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
The `${SIGHTENGINE_API_USER}` syntax means:
|
|
28
|
-
- "Look for an environment variable named SIGHTENGINE_API_USER"
|
|
29
|
-
- "Replace this placeholder with its value"
|
|
30
|
-
|
|
31
|
-
## Example Flow
|
|
32
|
-
|
|
33
|
-
1. **You set the variables:**
|
|
34
|
-
```bash
|
|
35
|
-
export SIGHTENGINE_API_USER=abc123
|
|
36
|
-
export SIGHTENGINE_API_SECRET=xyz789
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
2. **You start CSS:**
|
|
40
|
-
```bash
|
|
41
|
-
npx @solid/community-server -c config.json
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
3. **The plugin reads the config and replaces placeholders:**
|
|
45
|
-
- `${SIGHTENGINE_API_USER}` becomes `abc123`
|
|
46
|
-
- `${SIGHTENGINE_API_SECRET}` becomes `xyz789`
|
|
47
|
-
|
|
48
|
-
4. **The plugin uses these credentials to call SightEngine API**
|
|
49
|
-
|
|
50
|
-
## Why Use Environment Variables?
|
|
51
|
-
|
|
52
|
-
✅ **Security**: Credentials aren't stored in config files (which might be committed to git)
|
|
53
|
-
✅ **Flexibility**: Different credentials for dev/staging/production without changing code
|
|
54
|
-
✅ **Standard Practice**: Common pattern for managing secrets
|
|
55
|
-
|
|
56
|
-
## Alternative Methods
|
|
57
|
-
|
|
58
|
-
### Option 1: Set per command (temporary)
|
|
59
|
-
```bash
|
|
60
|
-
SIGHTENGINE_API_USER=abc123 SIGHTENGINE_API_SECRET=xyz789 npx @solid/community-server -c config.json
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Option 2: Use a .env file (with dotenv)
|
|
64
|
-
Create `.env` file:
|
|
65
|
-
```
|
|
66
|
-
SIGHTENGINE_API_USER=abc123
|
|
67
|
-
SIGHTENGINE_API_SECRET=xyz789
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Option 3: Set in shell profile (permanent) ⭐ RECOMMENDED FOR PRODUCTION
|
|
71
|
-
Add to `~/.bashrc` or `~/.zshrc`:
|
|
72
|
-
```bash
|
|
73
|
-
export SIGHTENGINE_API_USER=abc123
|
|
74
|
-
export SIGHTENGINE_API_SECRET=xyz789
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Option 4: System service environment file (for systemd)
|
|
78
|
-
Create `/etc/systemd/system/solid-server.service.d/override.conf`:
|
|
79
|
-
```ini
|
|
80
|
-
[Service]
|
|
81
|
-
Environment="SIGHTENGINE_API_USER=abc123"
|
|
82
|
-
Environment="SIGHTENGINE_API_SECRET=xyz789"
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## What Happens on Server Restart?
|
|
86
|
-
|
|
87
|
-
### ❌ If you only used `export` in terminal:
|
|
88
|
-
- Variables are **LOST** when terminal closes or server restarts
|
|
89
|
-
- Server will fail to moderate content (falls back to fail-open policy)
|
|
90
|
-
- You must re-export before starting server again
|
|
91
|
-
|
|
92
|
-
### ✅ If you added to shell profile (`~/.bashrc` or `~/.zshrc`):
|
|
93
|
-
- Variables are **PRESERVED** across terminal sessions
|
|
94
|
-
- But still lost on full system reboot unless server auto-starts with those variables
|
|
95
|
-
|
|
96
|
-
### ✅ If you use systemd service file:
|
|
97
|
-
- Variables are **ALWAYS AVAILABLE** to the service
|
|
98
|
-
- Survives server restarts and system reboots
|
|
99
|
-
- **BEST for production servers**
|
|
100
|
-
|
|
101
|
-
### ✅ If you use Docker:
|
|
102
|
-
```yaml
|
|
103
|
-
services:
|
|
104
|
-
solid-server:
|
|
105
|
-
environment:
|
|
106
|
-
- SIGHTENGINE_API_USER=abc123
|
|
107
|
-
- SIGHTENGINE_API_SECRET=xyz789
|
|
108
|
-
```
|
|
109
|
-
Variables are preserved in container configuration.
|
|
110
|
-
|
|
111
|
-
## Production Recommendation
|
|
112
|
-
|
|
113
|
-
For a production server that needs to survive restarts:
|
|
114
|
-
|
|
115
|
-
1. **Use systemd service** with environment variables in service file
|
|
116
|
-
2. **Or use Docker** with environment variables in docker-compose.yml
|
|
117
|
-
3. **Or use a process manager** like PM2 with ecosystem.config.js:
|
|
118
|
-
```javascript
|
|
119
|
-
module.exports = {
|
|
120
|
-
apps: [{
|
|
121
|
-
name: 'solid-server',
|
|
122
|
-
script: 'npx',
|
|
123
|
-
args: '@solid/community-server -c config.json',
|
|
124
|
-
env: {
|
|
125
|
-
SIGHTENGINE_API_USER: 'abc123',
|
|
126
|
-
SIGHTENGINE_API_SECRET: 'xyz789'
|
|
127
|
-
}
|
|
128
|
-
}]
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Getting Your API Credentials
|
|
133
|
-
|
|
134
|
-
1. Sign up at https://sightengine.com/
|
|
135
|
-
2. Go to your dashboard
|
|
136
|
-
3. Find your API User and API Secret
|
|
137
|
-
4. Replace `your_api_user` and `your_api_secret` with those values
|
package/INSTALLATION.md
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# Files Generated in Community Solid Server
|
|
2
|
-
|
|
3
|
-
When you install `@solid/moderation-plugin` in your Community Solid Server, the following files will be generated in `node_modules/@solid/moderation-plugin/`:
|
|
4
|
-
|
|
5
|
-
## Directory Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
node_modules/@solid/moderation-plugin/
|
|
9
|
-
├── dist/ # Compiled JavaScript and metadata
|
|
10
|
-
│ ├── components/ # Components.js metadata
|
|
11
|
-
│ │ ├── components.jsonld # Component definitions
|
|
12
|
-
│ │ └── context.jsonld # JSON-LD context
|
|
13
|
-
│ │
|
|
14
|
-
│ ├── providers/ # API providers
|
|
15
|
-
│ │ ├── SightEngineProvider.js
|
|
16
|
-
│ │ ├── SightEngineProvider.d.ts
|
|
17
|
-
│ │ ├── SightEngineProvider.js.map
|
|
18
|
-
│ │ ├── SightEngineProvider.d.ts.map
|
|
19
|
-
│ │ └── SightEngineProvider.jsonld
|
|
20
|
-
│ │
|
|
21
|
-
│ ├── util/ # Utilities
|
|
22
|
-
│ │ ├── GuardedStream.js
|
|
23
|
-
│ │ ├── GuardedStream.d.ts
|
|
24
|
-
│ │ └── *.map files
|
|
25
|
-
│ │
|
|
26
|
-
│ ├── index.js # Main entry point
|
|
27
|
-
│ ├── index.d.ts # TypeScript definitions
|
|
28
|
-
│ ├── ModerationOperationHandler.js
|
|
29
|
-
│ ├── ModerationOperationHandler.d.ts
|
|
30
|
-
│ ├── ModerationOperationHandler.jsonld
|
|
31
|
-
│ ├── ModerationConfig.js
|
|
32
|
-
│ ├── ModerationConfig.d.ts
|
|
33
|
-
│ ├── ModerationConfig.jsonld
|
|
34
|
-
│ ├── ModerationStore.js
|
|
35
|
-
│ ├── ModerationStore.d.ts
|
|
36
|
-
│ ├── ModerationStore.jsonld
|
|
37
|
-
│ ├── ModerationMixin.js
|
|
38
|
-
│ ├── ModerationMixin.d.ts
|
|
39
|
-
│ ├── ModerationMixin.jsonld
|
|
40
|
-
│ ├── ModerationRecord.js
|
|
41
|
-
│ ├── ModerationRecord.d.ts
|
|
42
|
-
│ ├── ModerationRecord.jsonld
|
|
43
|
-
│ └── *.map files # Source maps
|
|
44
|
-
│
|
|
45
|
-
├── config/ # Configuration templates
|
|
46
|
-
│ └── default.json # Default moderation settings
|
|
47
|
-
│
|
|
48
|
-
├── package.json # Package metadata
|
|
49
|
-
├── README.md # Documentation
|
|
50
|
-
└── LICENSE # MIT License
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Runtime Files (Created During Operation)
|
|
54
|
-
|
|
55
|
-
When CSS runs with moderation enabled, these files/folders are created:
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
your-css-project/
|
|
59
|
-
├── data/
|
|
60
|
-
│ └── moderation-logs/ # Audit logs (if enabled)
|
|
61
|
-
│ ├── 2024-01-20.jsonl # Daily log files
|
|
62
|
-
│ ├── 2024-01-21.jsonl
|
|
63
|
-
│ └── ...
|
|
64
|
-
│
|
|
65
|
-
└── /tmp/ # Temporary files during analysis
|
|
66
|
-
└── moderation_*.jpg/mp4 # Auto-deleted after analysis
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Key Files Explained
|
|
70
|
-
|
|
71
|
-
### Components.js Files (*.jsonld)
|
|
72
|
-
- Used by CSS's dependency injection system
|
|
73
|
-
- Define how classes are instantiated and wired together
|
|
74
|
-
- Allow configuration through JSON-LD config files
|
|
75
|
-
|
|
76
|
-
### JavaScript Files (*.js)
|
|
77
|
-
- Compiled TypeScript code
|
|
78
|
-
- Main application logic
|
|
79
|
-
|
|
80
|
-
### TypeScript Definitions (*.d.ts)
|
|
81
|
-
- Type information for TypeScript users
|
|
82
|
-
- Enable IDE autocomplete and type checking
|
|
83
|
-
|
|
84
|
-
### Source Maps (*.map)
|
|
85
|
-
- Map compiled JS back to original TypeScript
|
|
86
|
-
- Used for debugging
|
|
87
|
-
|
|
88
|
-
### config/default.json
|
|
89
|
-
- Template configuration with default thresholds
|
|
90
|
-
- Can be imported in CSS config files
|