@kkuffour/solid-moderation-plugin 0.1.2 → 0.2.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.
@@ -0,0 +1,52 @@
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/TESTING.md CHANGED
@@ -1,64 +1,93 @@
1
- # Local Testing Guide
1
+ # Testing ResourceStore Approach Locally
2
2
 
3
3
  ## Setup
4
4
 
5
- 1. **Set environment variables:**
6
- ```bash
7
- export SIGHTENGINE_API_USER=your_api_user
8
- export SIGHTENGINE_API_SECRET=your_api_secret
9
- ```
10
-
11
- 2. **Link the plugin locally (run in plugin directory):**
12
- ```bash
13
- cd /Users/opendata/solid-moderation
14
- npm link
15
- ```
16
-
17
- 3. **In your CSS project directory, link the plugin:**
18
- ```bash
19
- cd /path/to/your/communitysolidserver
20
- npm link @solid/moderation-plugin
21
- ```
22
-
23
- ## Run CSS with Moderation
5
+ 1. Build the plugin:
6
+ ```bash
7
+ npm run build
8
+ ```
24
9
 
10
+ 2. Link locally for testing:
25
11
  ```bash
26
- npx @solid/community-server -c /Users/opendata/solid-moderation/config-with-moderation.json
12
+ npm link
27
13
  ```
28
14
 
29
- ## Test the Moderation
15
+ 3. In your CSS directory:
16
+ ```bash
17
+ npm link @kkuffour/solid-moderation-plugin
18
+ ```
30
19
 
31
- ### Test Image Upload:
20
+ 4. Set environment variables:
32
21
  ```bash
33
- # Upload a safe image (should succeed)
34
- curl -X PUT http://localhost:3000/test.jpg \
35
- -H "Content-Type: image/jpeg" \
36
- --data-binary @safe-image.jpg
22
+ export SIGHTENGINE_API_USER=1060049443
23
+ export SIGHTENGINE_API_SECRET=QRQ8HUmh4hyvhZjksBJq5ZaNYPLPEKXu
24
+ ```
37
25
 
38
- # Upload inappropriate content (should be blocked)
39
- curl -X PUT http://localhost:3000/test2.jpg \
40
- -H "Content-Type: image/jpeg" \
41
- --data-binary @inappropriate-image.jpg
26
+ 5. Create test config in CSS directory (`config-moderation-test.json`):
27
+ ```json
28
+ {
29
+ "@context": [
30
+ "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld",
31
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld"
32
+ ],
33
+ "import": [
34
+ "css:config/file.json"
35
+ ],
36
+ "@graph": [
37
+ {
38
+ "@id": "urn:solid-moderation:SightEngineProvider",
39
+ "@type": "ksmp:dist/providers/SightEngineProvider.jsonld#SightEngineProvider",
40
+ "ksmp:dist/providers/SightEngineProvider.jsonld#SightEngineProvider_apiUser": "1060049443",
41
+ "ksmp:dist/providers/SightEngineProvider.jsonld#SightEngineProvider_apiSecret": "QRQ8HUmh4hyvhZjksBJq5ZaNYPLPEKXu"
42
+ },
43
+ {
44
+ "@id": "urn:solid-server:default:ResourceStore_Original",
45
+ "@type": "DataAccessorBasedStore",
46
+ "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
47
+ "auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" },
48
+ "accessor": { "@id": "urn:solid-server:default:DataAccessor" },
49
+ "metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" }
50
+ },
51
+ {
52
+ "@id": "urn:solid-server:default:ResourceStore",
53
+ "@type": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore",
54
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_source": {
55
+ "@id": "urn:solid-server:default:ResourceStore_Original"
56
+ },
57
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_client": {
58
+ "@id": "urn:solid-moderation:SightEngineProvider"
59
+ },
60
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_enabled": true,
61
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_imageNudityThreshold": 0.1,
62
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textSexualThreshold": 0.1,
63
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textToxicThreshold": 0.1,
64
+ "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_videoNudityThreshold": 0.1
65
+ }
66
+ ]
67
+ }
42
68
  ```
43
69
 
44
- ### Test Text Upload:
70
+ 6. Start CSS:
45
71
  ```bash
46
- # Upload safe text (should succeed)
47
- curl -X PUT http://localhost:3000/test.txt \
48
- -H "Content-Type: text/plain" \
49
- -d "Hello, this is safe content"
50
-
51
- # Upload inappropriate text (should be blocked)
52
- curl -X PUT http://localhost:3000/test2.txt \
53
- -H "Content-Type: text/plain" \
54
- -d "inappropriate content here"
72
+ npx @solid/community-server -c config-moderation-test.json
55
73
  ```
56
74
 
57
- ## Check Logs
75
+ ## Expected Behavior
58
76
 
59
- Moderation logs will be stored in:
60
- ```
61
- ./data/moderation-logs/
77
+ - Server should start without errors
78
+ - Log should show: "ModerationResourceStore initialized"
79
+ - Uploading images/videos/text should trigger moderation
80
+ - Content exceeding thresholds should be blocked with 403 Forbidden
81
+
82
+ ## Testing
83
+
84
+ Upload a test image:
85
+ ```bash
86
+ curl -X PUT http://localhost:3000/test.jpg \
87
+ -H "Content-Type: image/jpeg" \
88
+ --data-binary @test-image.jpg
62
89
  ```
63
90
 
64
- Each violation is logged as a JSON line in daily files.
91
+ Check logs for:
92
+ - "Moderating image/jpeg upload to /test.jpg"
93
+ - "Image APPROVED" or "Image BLOCKED"
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "@type": "Module",
8
8
  "requireName": "@kkuffour/solid-moderation-plugin",
9
9
  "import": [
10
10
  "npmd:@solid/moderation-plugin/^1.0.0/components/ModerationOperationHandler.jsonld",
11
+ "npmd:@solid/moderation-plugin/^1.0.0/components/ModerationResourceStore.jsonld",
11
12
  "npmd:@solid/moderation-plugin/^1.0.0/components/ModerationConfig.jsonld",
12
13
  "npmd:@solid/moderation-plugin/^1.0.0/components/ModerationStore.jsonld",
13
14
  "npmd:@solid/moderation-plugin/^1.0.0/components/ModerationRecord.jsonld",
@@ -82,6 +82,45 @@
82
82
  }
83
83
  }
84
84
  },
85
+ "ModerationResourceStore": {
86
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore",
87
+ "@prefix": true,
88
+ "@context": {
89
+ "enabled": {
90
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_enabled"
91
+ },
92
+ "imageNudityThreshold": {
93
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_imageNudityThreshold"
94
+ },
95
+ "textSexualThreshold": {
96
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textSexualThreshold"
97
+ },
98
+ "textToxicThreshold": {
99
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textToxicThreshold"
100
+ },
101
+ "videoNudityThreshold": {
102
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_videoNudityThreshold"
103
+ },
104
+ "client": {
105
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_client"
106
+ },
107
+ "source": {
108
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_source"
109
+ },
110
+ "": {
111
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_enabled"
112
+ },
113
+ "dityThreshold": {
114
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_videoNudityThreshold"
115
+ },
116
+ "ualThreshold": {
117
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textSexualThreshold"
118
+ },
119
+ "icThreshold": {
120
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textToxicThreshold"
121
+ }
122
+ }
123
+ },
85
124
  "ModerationConfig": {
86
125
  "@id": "ksmp:dist/ModerationConfig.jsonld#ModerationConfig",
87
126
  "@prefix": true,
@@ -1,80 +1,39 @@
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/@solid/moderation-plugin/^1.0.0/components/context.jsonld"
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld"
5
+ ],
6
+ "import": [
7
+ "css:config/file.json"
5
8
  ],
6
9
  "@graph": [
7
10
  {
8
- "@id": "urn:solid-server:default:OperationHandler",
9
- "@type": "WaterfallHandler",
10
- "handlers": [
11
- { "@id": "urn:solid-moderation:wrapped:PostHandler" },
12
- { "@id": "urn:solid-moderation:wrapped:PutHandler" },
13
- { "@id": "urn:solid-moderation:wrapped:PatchHandler" },
14
- { "@type": "GetOperationHandler", "store": { "@id": "urn:solid-server:default:ResourceStore" } },
15
- { "@type": "HeadOperationHandler", "store": { "@id": "urn:solid-server:default:ResourceStore" } },
16
- { "@type": "DeleteOperationHandler", "store": { "@id": "urn:solid-server:default:ResourceStore" } }
17
- ]
18
- },
19
- {
20
- "@id": "urn:solid-moderation:wrapped:PostHandler",
21
- "@type": "ModerationOperationHandler",
22
- "ModerationOperationHandler:_source": {
23
- "@type": "PostOperationHandler",
24
- "store": { "@id": "urn:solid-server:default:ResourceStore" }
25
- },
26
- "ModerationOperationHandler:_enabled": true,
27
- "ModerationOperationHandler:_auditLoggingEnabled": true,
28
- "ModerationOperationHandler:_auditLoggingStorePath": "./data/moderation-logs",
29
- "ModerationOperationHandler:_sightEngineApiUser": "${SIGHTENGINE_API_USER}",
30
- "ModerationOperationHandler:_sightEngineApiSecret": "${SIGHTENGINE_API_SECRET}",
31
- "ModerationOperationHandler:_imagesEnabled": true,
32
- "ModerationOperationHandler:_textEnabled": true,
33
- "ModerationOperationHandler:_videoEnabled": true,
34
- "ModerationOperationHandler:_imageNudityThreshold": 0.5,
35
- "ModerationOperationHandler:_textSexualThreshold": 0.5,
36
- "ModerationOperationHandler:_textToxicThreshold": 0.5,
37
- "ModerationOperationHandler:_videoNudityThreshold": 0.5
11
+ "@comment": "SightEngine API client",
12
+ "@id": "urn:solid-moderation:SightEngineProvider",
13
+ "@type": "SightEngineProvider",
14
+ "SightEngineProvider:_apiUser": "${SIGHTENGINE_API_USER}",
15
+ "SightEngineProvider:_apiSecret": "${SIGHTENGINE_API_SECRET}"
38
16
  },
39
17
  {
40
- "@id": "urn:solid-moderation:wrapped:PutHandler",
41
- "@type": "ModerationOperationHandler",
42
- "ModerationOperationHandler:_source": {
43
- "@type": "PutOperationHandler",
44
- "store": { "@id": "urn:solid-server:default:ResourceStore" }
45
- },
46
- "ModerationOperationHandler:_enabled": true,
47
- "ModerationOperationHandler:_auditLoggingEnabled": true,
48
- "ModerationOperationHandler:_auditLoggingStorePath": "./data/moderation-logs",
49
- "ModerationOperationHandler:_sightEngineApiUser": "${SIGHTENGINE_API_USER}",
50
- "ModerationOperationHandler:_sightEngineApiSecret": "${SIGHTENGINE_API_SECRET}",
51
- "ModerationOperationHandler:_imagesEnabled": true,
52
- "ModerationOperationHandler:_textEnabled": true,
53
- "ModerationOperationHandler:_videoEnabled": true,
54
- "ModerationOperationHandler:_imageNudityThreshold": 0.5,
55
- "ModerationOperationHandler:_textSexualThreshold": 0.5,
56
- "ModerationOperationHandler:_textToxicThreshold": 0.5,
57
- "ModerationOperationHandler:_videoNudityThreshold": 0.5
18
+ "@comment": "Original ResourceStore - renamed to avoid conflict",
19
+ "@id": "urn:solid-server:default:ResourceStore_Original",
20
+ "@type": "DataAccessorBasedStore",
21
+ "identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" },
22
+ "auxiliaryStrategy": { "@id": "urn:solid-server:default:AuxiliaryStrategy" },
23
+ "accessor": { "@id": "urn:solid-server:default:DataAccessor" },
24
+ "metadataStrategy": { "@id": "urn:solid-server:default:MetadataStrategy" }
58
25
  },
59
26
  {
60
- "@id": "urn:solid-moderation:wrapped:PatchHandler",
61
- "@type": "ModerationOperationHandler",
62
- "ModerationOperationHandler:_source": {
63
- "@type": "PatchOperationHandler",
64
- "store": { "@id": "urn:solid-server:default:ResourceStore" }
65
- },
66
- "ModerationOperationHandler:_enabled": true,
67
- "ModerationOperationHandler:_auditLoggingEnabled": true,
68
- "ModerationOperationHandler:_auditLoggingStorePath": "./data/moderation-logs",
69
- "ModerationOperationHandler:_sightEngineApiUser": "${SIGHTENGINE_API_USER}",
70
- "ModerationOperationHandler:_sightEngineApiSecret": "${SIGHTENGINE_API_SECRET}",
71
- "ModerationOperationHandler:_imagesEnabled": true,
72
- "ModerationOperationHandler:_textEnabled": true,
73
- "ModerationOperationHandler:_videoEnabled": true,
74
- "ModerationOperationHandler:_imageNudityThreshold": 0.5,
75
- "ModerationOperationHandler:_textSexualThreshold": 0.5,
76
- "ModerationOperationHandler:_textToxicThreshold": 0.5,
77
- "ModerationOperationHandler:_videoNudityThreshold": 0.5
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
78
37
  }
79
38
  ]
80
39
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "components": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "components": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld",
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld",
5
5
  "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld"
6
6
  ],
7
7
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "components": [
@@ -0,0 +1,30 @@
1
+ import type { ResourceStore, ChangeMap, Representation, ResourceIdentifier, RepresentationPreferences, Conditions, Patch } from '@solid/community-server';
2
+ import type { SightEngineProvider } from './providers/SightEngineProvider';
3
+ /**
4
+ * @module
5
+ * Wraps a ResourceStore to add content moderation on write operations.
6
+ */
7
+ export declare class ModerationResourceStore implements ResourceStore {
8
+ private readonly logger;
9
+ private readonly source;
10
+ private readonly client;
11
+ private readonly enabled;
12
+ private readonly imageNudityThreshold;
13
+ private readonly textSexualThreshold;
14
+ private readonly textToxicThreshold;
15
+ private readonly videoNudityThreshold;
16
+ constructor(source: ResourceStore, client: SightEngineProvider, enabled: boolean, imageNudityThreshold: number, textSexualThreshold: number, textToxicThreshold: number, videoNudityThreshold: number);
17
+ hasResource(identifier: ResourceIdentifier): Promise<boolean>;
18
+ getRepresentation(identifier: ResourceIdentifier, preferences: RepresentationPreferences, conditions?: Conditions): Promise<Representation>;
19
+ setRepresentation(identifier: ResourceIdentifier, representation: Representation, conditions?: Conditions): Promise<ChangeMap>;
20
+ addResource(container: ResourceIdentifier, representation: Representation, conditions?: Conditions): Promise<ChangeMap>;
21
+ deleteResource(identifier: ResourceIdentifier, conditions?: Conditions): Promise<ChangeMap>;
22
+ modifyResource(identifier: ResourceIdentifier, patch: Patch, conditions?: Conditions): Promise<ChangeMap>;
23
+ private moderateRepresentation;
24
+ private shouldModerateContentType;
25
+ private moderateImage;
26
+ private moderateVideo;
27
+ private moderateText;
28
+ private getFileExtension;
29
+ }
30
+ //# sourceMappingURL=ModerationResourceStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModerationResourceStore.d.ts","sourceRoot":"","sources":["../src/ModerationResourceStore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAG1J,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAE3E;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,aAAa;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,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,aAAa,EACrB,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,OAAO,EAChB,oBAAoB,EAAE,MAAM,EAC5B,mBAAmB,EAAE,MAAM,EAC3B,kBAAkB,EAAE,MAAM,EAC1B,oBAAoB,EAAE,MAAM;IAejB,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,iBAAiB,CAC5B,UAAU,EAAE,kBAAkB,EAC9B,WAAW,EAAE,yBAAyB,EACtC,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,cAAc,CAAC;IAIb,iBAAiB,CAC5B,UAAU,EAAE,kBAAkB,EAC9B,cAAc,EAAE,cAAc,EAC9B,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,SAAS,CAAC;IAKR,WAAW,CACtB,SAAS,EAAE,kBAAkB,EAC7B,cAAc,EAAE,cAAc,EAC9B,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,SAAS,CAAC;IAKR,cAAc,CACzB,UAAU,EAAE,kBAAkB,EAC9B,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,SAAS,CAAC;IAIR,cAAc,CACzB,UAAU,EAAE,kBAAkB,EAC9B,KAAK,EAAE,KAAK,EACZ,UAAU,CAAC,EAAE,UAAU,GACtB,OAAO,CAAC,SAAS,CAAC;YAIP,sBAAsB;IAyDpC,OAAO,CAAC,yBAAyB;YAMnB,aAAa;YAsBb,aAAa;YAsBb,YAAY;IAsB1B,OAAO,CAAC,gBAAgB;CAWzB"}
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModerationResourceStore = void 0;
4
+ const community_server_1 = require("@solid/community-server");
5
+ const fs_1 = require("fs");
6
+ /**
7
+ * @module
8
+ * Wraps a ResourceStore to add content moderation on write operations.
9
+ */
10
+ class ModerationResourceStore {
11
+ constructor(source, client, enabled, imageNudityThreshold, textSexualThreshold, textToxicThreshold, videoNudityThreshold) {
12
+ this.logger = (0, community_server_1.getLoggerFor)(this);
13
+ this.source = source;
14
+ this.client = client;
15
+ this.enabled = enabled;
16
+ this.imageNudityThreshold = imageNudityThreshold;
17
+ this.textSexualThreshold = textSexualThreshold;
18
+ this.textToxicThreshold = textToxicThreshold;
19
+ this.videoNudityThreshold = videoNudityThreshold;
20
+ this.logger.info('ModerationResourceStore initialized');
21
+ this.logger.info(` Enabled: ${enabled}`);
22
+ this.logger.info(` Thresholds: image=${imageNudityThreshold}, text=${textSexualThreshold}/${textToxicThreshold}, video=${videoNudityThreshold}`);
23
+ }
24
+ async hasResource(identifier) {
25
+ return this.source.hasResource(identifier);
26
+ }
27
+ async getRepresentation(identifier, preferences, conditions) {
28
+ return this.source.getRepresentation(identifier, preferences, conditions);
29
+ }
30
+ async setRepresentation(identifier, representation, conditions) {
31
+ await this.moderateRepresentation(identifier, representation);
32
+ return this.source.setRepresentation(identifier, representation, conditions);
33
+ }
34
+ async addResource(container, representation, conditions) {
35
+ await this.moderateRepresentation(container, representation);
36
+ return this.source.addResource(container, representation, conditions);
37
+ }
38
+ async deleteResource(identifier, conditions) {
39
+ return this.source.deleteResource(identifier, conditions);
40
+ }
41
+ async modifyResource(identifier, patch, conditions) {
42
+ return this.source.modifyResource(identifier, patch, conditions);
43
+ }
44
+ async moderateRepresentation(identifier, representation) {
45
+ if (!this.enabled || !representation.data) {
46
+ return;
47
+ }
48
+ const contentType = representation.metadata.contentType;
49
+ if (!contentType) {
50
+ return;
51
+ }
52
+ const shouldModerate = this.shouldModerateContentType(contentType);
53
+ if (!shouldModerate) {
54
+ return;
55
+ }
56
+ this.logger.info(`Moderating ${contentType} upload to ${identifier.path}`);
57
+ try {
58
+ const chunks = [];
59
+ for await (const chunk of representation.data) {
60
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
61
+ }
62
+ const buffer = Buffer.concat(chunks);
63
+ if (contentType.startsWith('image/')) {
64
+ await this.moderateImage(identifier.path, buffer, contentType);
65
+ }
66
+ else if (contentType.startsWith('video/')) {
67
+ await this.moderateVideo(identifier.path, buffer, contentType);
68
+ }
69
+ else if (contentType.startsWith('text/')) {
70
+ await this.moderateText(identifier.path, buffer);
71
+ }
72
+ representation.data = (0, community_server_1.guardedStreamFrom)(buffer);
73
+ }
74
+ catch (error) {
75
+ if (error instanceof community_server_1.ForbiddenHttpError) {
76
+ throw error;
77
+ }
78
+ this.logger.error(`Moderation failed for ${identifier.path}: ${error.message}`);
79
+ this.logger.warn('Allowing content through due to moderation failure (fail-open policy)');
80
+ // Re-create stream from buffer if we have it
81
+ const chunks = [];
82
+ try {
83
+ for await (const chunk of representation.data) {
84
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
85
+ }
86
+ representation.data = (0, community_server_1.guardedStreamFrom)(Buffer.concat(chunks));
87
+ }
88
+ catch {
89
+ // Stream already consumed, can't recover
90
+ }
91
+ }
92
+ }
93
+ shouldModerateContentType(contentType) {
94
+ return contentType.startsWith('image/') ||
95
+ contentType.startsWith('video/') ||
96
+ contentType.startsWith('text/');
97
+ }
98
+ async moderateImage(path, buffer, contentType) {
99
+ const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
100
+ (0, fs_1.writeFileSync)(tempFile, buffer);
101
+ try {
102
+ const result = await this.client.analyzeImage(tempFile);
103
+ if (result.nudity?.raw && result.nudity.raw > this.imageNudityThreshold) {
104
+ this.logger.warn(`Image BLOCKED: ${path} - nudity score ${result.nudity.raw.toFixed(2)} > ${this.imageNudityThreshold}`);
105
+ throw new community_server_1.ForbiddenHttpError(`Image contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
106
+ }
107
+ this.logger.info(`Image APPROVED: ${path}`);
108
+ }
109
+ finally {
110
+ try {
111
+ (0, fs_1.unlinkSync)(tempFile);
112
+ }
113
+ catch {
114
+ this.logger.warn(`Failed to cleanup temp file: ${tempFile}`);
115
+ }
116
+ }
117
+ }
118
+ async moderateVideo(path, buffer, contentType) {
119
+ const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
120
+ (0, fs_1.writeFileSync)(tempFile, buffer);
121
+ try {
122
+ const result = await this.client.analyzeVideo(tempFile);
123
+ if (result.nudity?.raw && result.nudity.raw > this.videoNudityThreshold) {
124
+ this.logger.warn(`Video BLOCKED: ${path} - nudity score ${result.nudity.raw.toFixed(2)} > ${this.videoNudityThreshold}`);
125
+ throw new community_server_1.ForbiddenHttpError(`Video contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
126
+ }
127
+ this.logger.info(`Video APPROVED: ${path}`);
128
+ }
129
+ finally {
130
+ try {
131
+ (0, fs_1.unlinkSync)(tempFile);
132
+ }
133
+ catch {
134
+ this.logger.warn(`Failed to cleanup temp file: ${tempFile}`);
135
+ }
136
+ }
137
+ }
138
+ async moderateText(path, buffer) {
139
+ const text = buffer.toString('utf-8');
140
+ const result = await this.client.analyzeText(text);
141
+ const violations = [];
142
+ if (result.sexual > this.textSexualThreshold) {
143
+ violations.push(`sexual content (${result.sexual.toFixed(2)} > ${this.textSexualThreshold})`);
144
+ }
145
+ if (result.toxic > this.textToxicThreshold) {
146
+ violations.push(`toxic content (${result.toxic.toFixed(2)} > ${this.textToxicThreshold})`);
147
+ }
148
+ if (violations.length > 0) {
149
+ this.logger.warn(`Text BLOCKED: ${path} - ${violations.join(', ')}`);
150
+ throw new community_server_1.ForbiddenHttpError(`Text contains inappropriate content: ${violations.join(', ')}`);
151
+ }
152
+ this.logger.info(`Text APPROVED: ${path}`);
153
+ }
154
+ getFileExtension(contentType) {
155
+ const map = {
156
+ 'image/jpeg': 'jpg',
157
+ 'image/png': 'png',
158
+ 'image/gif': 'gif',
159
+ 'image/webp': 'webp',
160
+ 'video/mp4': 'mp4',
161
+ 'video/webm': 'webm',
162
+ };
163
+ return map[contentType] || 'bin';
164
+ }
165
+ }
166
+ exports.ModerationResourceStore = ModerationResourceStore;
167
+ //# sourceMappingURL=ModerationResourceStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModerationResourceStore.js","sourceRoot":"","sources":["../src/ModerationResourceStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,157 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld",
5
+ "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/components/context.jsonld"
6
+ ],
7
+ "@id": "npmd:@kkuffour/solid-moderation-plugin",
8
+ "components": [
9
+ {
10
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore",
11
+ "@type": "Class",
12
+ "requireElement": "ModerationResourceStore",
13
+ "extends": [
14
+ "css:dist/storage/ResourceStore.jsonld#ResourceStore"
15
+ ],
16
+ "parameters": [
17
+ {
18
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_enabled",
19
+ "range": "xsd:boolean"
20
+ },
21
+ {
22
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_imageNudityThreshold",
23
+ "range": "xsd:number"
24
+ },
25
+ {
26
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textSexualThreshold",
27
+ "range": "xsd:number"
28
+ },
29
+ {
30
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textToxicThreshold",
31
+ "range": "xsd:number"
32
+ },
33
+ {
34
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_videoNudityThreshold",
35
+ "range": "xsd:number"
36
+ },
37
+ {
38
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_client",
39
+ "range": "ksmp:dist/providers/SightEngineProvider.jsonld#SightEngineProvider"
40
+ },
41
+ {
42
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_source",
43
+ "range": "css:dist/storage/ResourceStore.jsonld#ResourceStore"
44
+ }
45
+ ],
46
+ "memberFields": [
47
+ {
48
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_logger",
49
+ "memberFieldName": "logger"
50
+ },
51
+ {
52
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_source",
53
+ "memberFieldName": "source"
54
+ },
55
+ {
56
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_client",
57
+ "memberFieldName": "client"
58
+ },
59
+ {
60
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_enabled",
61
+ "memberFieldName": "enabled"
62
+ },
63
+ {
64
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_imageNudityThreshold",
65
+ "memberFieldName": "imageNudityThreshold"
66
+ },
67
+ {
68
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_textSexualThreshold",
69
+ "memberFieldName": "textSexualThreshold"
70
+ },
71
+ {
72
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_textToxicThreshold",
73
+ "memberFieldName": "textToxicThreshold"
74
+ },
75
+ {
76
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_videoNudityThreshold",
77
+ "memberFieldName": "videoNudityThreshold"
78
+ },
79
+ {
80
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_constructor",
81
+ "memberFieldName": "constructor"
82
+ },
83
+ {
84
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_hasResource",
85
+ "memberFieldName": "hasResource"
86
+ },
87
+ {
88
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_getRepresentation",
89
+ "memberFieldName": "getRepresentation"
90
+ },
91
+ {
92
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_setRepresentation",
93
+ "memberFieldName": "setRepresentation"
94
+ },
95
+ {
96
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_addResource",
97
+ "memberFieldName": "addResource"
98
+ },
99
+ {
100
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_deleteResource",
101
+ "memberFieldName": "deleteResource"
102
+ },
103
+ {
104
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_modifyResource",
105
+ "memberFieldName": "modifyResource"
106
+ },
107
+ {
108
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_moderateRepresentation",
109
+ "memberFieldName": "moderateRepresentation"
110
+ },
111
+ {
112
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_shouldModerateContentType",
113
+ "memberFieldName": "shouldModerateContentType"
114
+ },
115
+ {
116
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_moderateImage",
117
+ "memberFieldName": "moderateImage"
118
+ },
119
+ {
120
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_moderateVideo",
121
+ "memberFieldName": "moderateVideo"
122
+ },
123
+ {
124
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_moderateText",
125
+ "memberFieldName": "moderateText"
126
+ },
127
+ {
128
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore__member_getFileExtension",
129
+ "memberFieldName": "getFileExtension"
130
+ }
131
+ ],
132
+ "constructorArguments": [
133
+ {
134
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_source"
135
+ },
136
+ {
137
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_client"
138
+ },
139
+ {
140
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_enabled"
141
+ },
142
+ {
143
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_imageNudityThreshold"
144
+ },
145
+ {
146
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textSexualThreshold"
147
+ },
148
+ {
149
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_textToxicThreshold"
150
+ },
151
+ {
152
+ "@id": "ksmp:dist/ModerationResourceStore.jsonld#ModerationResourceStore_videoNudityThreshold"
153
+ }
154
+ ]
155
+ }
156
+ ]
157
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "components": [
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './ModerationOperationHandler';
2
+ export * from './ModerationResourceStore';
2
3
  export * from './ModerationConfig';
3
4
  export * from './ModerationStore';
4
5
  export * from './ModerationRecord';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iCAAiC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./ModerationOperationHandler"), exports);
18
+ __exportStar(require("./ModerationResourceStore"), exports);
18
19
  __exportStar(require("./ModerationConfig"), exports);
19
20
  __exportStar(require("./ModerationStore"), exports);
20
21
  __exportStar(require("./ModerationRecord"), exports);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+DAA6C;AAC7C,qDAAmC;AACnC,oDAAkC;AAClC,qDAAmC;AACnC,oDAAkC;AAClC,kEAAgD"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+DAA6C;AAC7C,4DAA0C;AAC1C,qDAAmC;AACnC,oDAAkC;AAClC,qDAAmC;AACnC,oDAAkC;AAClC,kEAAgD"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "@context": [
3
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld",
4
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "components": [
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@kkuffour/solid-moderation-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Content moderation plugin for Community Solid Server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "lsd:module": "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin",
8
8
  "lsd:components": "components/components.jsonld",
9
9
  "lsd:contexts": {
10
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/components/context.jsonld": "components/context.jsonld",
11
- "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.1.0/config/context.jsonld": "config/context.jsonld"
10
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/components/context.jsonld": "components/context.jsonld",
11
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.2.0/config/context.jsonld": "config/context.jsonld"
12
12
  },
13
13
  "lsd:importPaths": {
14
14
  "https://linkedsoftwaredependencies.org/bundles/npm/@solid/moderation-plugin/^1.0.0/components/": "dist/",
@@ -0,0 +1,227 @@
1
+ import type { ResourceStore, ChangeMap, Representation, ResourceIdentifier, RepresentationPreferences, Conditions, Patch } from '@solid/community-server';
2
+ import { getLoggerFor, ForbiddenHttpError, guardedStreamFrom } from '@solid/community-server';
3
+ import { writeFileSync, unlinkSync } from 'fs';
4
+ import type { SightEngineProvider } from './providers/SightEngineProvider';
5
+
6
+ /**
7
+ * @module
8
+ * Wraps a ResourceStore to add content moderation on write operations.
9
+ */
10
+ export class ModerationResourceStore implements ResourceStore {
11
+ private readonly logger = getLoggerFor(this);
12
+ private readonly source: ResourceStore;
13
+ private readonly client: SightEngineProvider;
14
+ private readonly enabled: boolean;
15
+ private readonly imageNudityThreshold: number;
16
+ private readonly textSexualThreshold: number;
17
+ private readonly textToxicThreshold: number;
18
+ private readonly videoNudityThreshold: number;
19
+
20
+ public constructor(
21
+ source: ResourceStore,
22
+ client: SightEngineProvider,
23
+ enabled: boolean,
24
+ imageNudityThreshold: number,
25
+ textSexualThreshold: number,
26
+ textToxicThreshold: number,
27
+ videoNudityThreshold: number,
28
+ ) {
29
+ this.source = source;
30
+ this.client = client;
31
+ this.enabled = enabled;
32
+ this.imageNudityThreshold = imageNudityThreshold;
33
+ this.textSexualThreshold = textSexualThreshold;
34
+ this.textToxicThreshold = textToxicThreshold;
35
+ this.videoNudityThreshold = videoNudityThreshold;
36
+
37
+ this.logger.info('ModerationResourceStore initialized');
38
+ this.logger.info(` Enabled: ${enabled}`);
39
+ this.logger.info(` Thresholds: image=${imageNudityThreshold}, text=${textSexualThreshold}/${textToxicThreshold}, video=${videoNudityThreshold}`);
40
+ }
41
+
42
+ public async hasResource(identifier: ResourceIdentifier): Promise<boolean> {
43
+ return this.source.hasResource(identifier);
44
+ }
45
+
46
+ public async getRepresentation(
47
+ identifier: ResourceIdentifier,
48
+ preferences: RepresentationPreferences,
49
+ conditions?: Conditions,
50
+ ): Promise<Representation> {
51
+ return this.source.getRepresentation(identifier, preferences, conditions);
52
+ }
53
+
54
+ public async setRepresentation(
55
+ identifier: ResourceIdentifier,
56
+ representation: Representation,
57
+ conditions?: Conditions,
58
+ ): Promise<ChangeMap> {
59
+ await this.moderateRepresentation(identifier, representation);
60
+ return this.source.setRepresentation(identifier, representation, conditions);
61
+ }
62
+
63
+ public async addResource(
64
+ container: ResourceIdentifier,
65
+ representation: Representation,
66
+ conditions?: Conditions,
67
+ ): Promise<ChangeMap> {
68
+ await this.moderateRepresentation(container, representation);
69
+ return this.source.addResource(container, representation, conditions);
70
+ }
71
+
72
+ public async deleteResource(
73
+ identifier: ResourceIdentifier,
74
+ conditions?: Conditions,
75
+ ): Promise<ChangeMap> {
76
+ return this.source.deleteResource(identifier, conditions);
77
+ }
78
+
79
+ public async modifyResource(
80
+ identifier: ResourceIdentifier,
81
+ patch: Patch,
82
+ conditions?: Conditions,
83
+ ): Promise<ChangeMap> {
84
+ return this.source.modifyResource(identifier, patch, conditions);
85
+ }
86
+
87
+ private async moderateRepresentation(
88
+ identifier: ResourceIdentifier,
89
+ representation: Representation,
90
+ ): Promise<void> {
91
+ if (!this.enabled || !representation.data) {
92
+ return;
93
+ }
94
+
95
+ const contentType = representation.metadata.contentType;
96
+ if (!contentType) {
97
+ return;
98
+ }
99
+
100
+ const shouldModerate = this.shouldModerateContentType(contentType);
101
+ if (!shouldModerate) {
102
+ return;
103
+ }
104
+
105
+ this.logger.info(`Moderating ${contentType} upload to ${identifier.path}`);
106
+
107
+ try {
108
+ const chunks: Buffer[] = [];
109
+ for await (const chunk of representation.data) {
110
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
111
+ }
112
+ const buffer = Buffer.concat(chunks);
113
+
114
+ if (contentType.startsWith('image/')) {
115
+ await this.moderateImage(identifier.path, buffer, contentType);
116
+ } else if (contentType.startsWith('video/')) {
117
+ await this.moderateVideo(identifier.path, buffer, contentType);
118
+ } else if (contentType.startsWith('text/')) {
119
+ await this.moderateText(identifier.path, buffer);
120
+ }
121
+
122
+ representation.data = guardedStreamFrom(buffer);
123
+
124
+ } catch (error: unknown) {
125
+ if (error instanceof ForbiddenHttpError) {
126
+ throw error;
127
+ }
128
+ this.logger.error(`Moderation failed for ${identifier.path}: ${(error as Error).message}`);
129
+ this.logger.warn('Allowing content through due to moderation failure (fail-open policy)');
130
+
131
+ // Re-create stream from buffer if we have it
132
+ const chunks: Buffer[] = [];
133
+ try {
134
+ for await (const chunk of representation.data) {
135
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
136
+ }
137
+ representation.data = guardedStreamFrom(Buffer.concat(chunks));
138
+ } catch {
139
+ // Stream already consumed, can't recover
140
+ }
141
+ }
142
+ }
143
+
144
+ private shouldModerateContentType(contentType: string): boolean {
145
+ return contentType.startsWith('image/') ||
146
+ contentType.startsWith('video/') ||
147
+ contentType.startsWith('text/');
148
+ }
149
+
150
+ private async moderateImage(path: string, buffer: Buffer, contentType: string): Promise<void> {
151
+ const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
152
+ writeFileSync(tempFile, buffer);
153
+
154
+ try {
155
+ const result = await this.client.analyzeImage(tempFile);
156
+
157
+ if (result.nudity?.raw && result.nudity.raw > this.imageNudityThreshold) {
158
+ this.logger.warn(`Image BLOCKED: ${path} - nudity score ${result.nudity.raw.toFixed(2)} > ${this.imageNudityThreshold}`);
159
+ throw new ForbiddenHttpError(`Image contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
160
+ }
161
+
162
+ this.logger.info(`Image APPROVED: ${path}`);
163
+ } finally {
164
+ try {
165
+ unlinkSync(tempFile);
166
+ } catch {
167
+ this.logger.warn(`Failed to cleanup temp file: ${tempFile}`);
168
+ }
169
+ }
170
+ }
171
+
172
+ private async moderateVideo(path: string, buffer: Buffer, contentType: string): Promise<void> {
173
+ const tempFile = `/tmp/moderation_${Date.now()}.${this.getFileExtension(contentType)}`;
174
+ writeFileSync(tempFile, buffer);
175
+
176
+ try {
177
+ const result = await this.client.analyzeVideo(tempFile);
178
+
179
+ if (result.nudity?.raw && result.nudity.raw > this.videoNudityThreshold) {
180
+ this.logger.warn(`Video BLOCKED: ${path} - nudity score ${result.nudity.raw.toFixed(2)} > ${this.videoNudityThreshold}`);
181
+ throw new ForbiddenHttpError(`Video contains inappropriate content (nudity: ${result.nudity.raw.toFixed(2)})`);
182
+ }
183
+
184
+ this.logger.info(`Video APPROVED: ${path}`);
185
+ } finally {
186
+ try {
187
+ unlinkSync(tempFile);
188
+ } catch {
189
+ this.logger.warn(`Failed to cleanup temp file: ${tempFile}`);
190
+ }
191
+ }
192
+ }
193
+
194
+ private async moderateText(path: string, buffer: Buffer): Promise<void> {
195
+ const text = buffer.toString('utf-8');
196
+ const result = await this.client.analyzeText(text);
197
+
198
+ const violations: string[] = [];
199
+
200
+ if (result.sexual > this.textSexualThreshold) {
201
+ violations.push(`sexual content (${result.sexual.toFixed(2)} > ${this.textSexualThreshold})`);
202
+ }
203
+
204
+ if (result.toxic > this.textToxicThreshold) {
205
+ violations.push(`toxic content (${result.toxic.toFixed(2)} > ${this.textToxicThreshold})`);
206
+ }
207
+
208
+ if (violations.length > 0) {
209
+ this.logger.warn(`Text BLOCKED: ${path} - ${violations.join(', ')}`);
210
+ throw new ForbiddenHttpError(`Text contains inappropriate content: ${violations.join(', ')}`);
211
+ }
212
+
213
+ this.logger.info(`Text APPROVED: ${path}`);
214
+ }
215
+
216
+ private getFileExtension(contentType: string): string {
217
+ const map: Record<string, string> = {
218
+ 'image/jpeg': 'jpg',
219
+ 'image/png': 'png',
220
+ 'image/gif': 'gif',
221
+ 'image/webp': 'webp',
222
+ 'video/mp4': 'mp4',
223
+ 'video/webm': 'webm',
224
+ };
225
+ return map[contentType] || 'bin';
226
+ }
227
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './ModerationOperationHandler';
2
+ export * from './ModerationResourceStore';
2
3
  export * from './ModerationConfig';
3
4
  export * from './ModerationStore';
4
5
  export * from './ModerationRecord';