@kkuffour/solid-moderation-plugin 0.2.2 → 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.
Files changed (74) hide show
  1. package/.data/.internal/idp/keys/cookie-secret$.json +1 -0
  2. package/.data/.internal/idp/keys/jwks$.json +1 -0
  3. package/.data/.internal/setup/current-base-url$.json +1 -0
  4. package/PLUGIN_DEVELOPER_GUIDE.md +213 -0
  5. package/README.md +25 -28
  6. package/components/context.jsonld +6 -245
  7. package/config/default.json +14 -28
  8. package/dist/ModerationHandler.d.ts +21 -0
  9. package/dist/ModerationHandler.d.ts.map +1 -0
  10. package/dist/ModerationHandler.js +158 -0
  11. package/dist/ModerationHandler.js.map +1 -0
  12. package/dist/ModerationHandler.jsonld +126 -0
  13. package/dist/components/components.jsonld +13 -0
  14. package/dist/components/context.jsonld +11 -0
  15. package/dist/index.d.ts +1 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -6
  18. package/dist/index.js.map +1 -1
  19. package/dist/providers/SightEngineProvider.jsonld +2 -2
  20. package/package.json +11 -11
  21. package/src/ModerationHandler.ts +189 -0
  22. package/src/index.ts +1 -6
  23. package/ARCHITECTURE.md +0 -52
  24. package/CONFIG-GUIDE.md +0 -49
  25. package/DEVELOPMENT.md +0 -129
  26. package/ENV-VARIABLES.md +0 -137
  27. package/INSTALLATION.md +0 -90
  28. package/MIGRATION.md +0 -81
  29. package/PRODUCTION.md +0 -186
  30. package/PUBLISHING.md +0 -104
  31. package/TESTING.md +0 -93
  32. package/components/components.jsonld +0 -18
  33. package/dist/ModerationConfig.d.ts +0 -16
  34. package/dist/ModerationConfig.d.ts.map +0 -1
  35. package/dist/ModerationConfig.js +0 -18
  36. package/dist/ModerationConfig.js.map +0 -1
  37. package/dist/ModerationConfig.jsonld +0 -66
  38. package/dist/ModerationMixin.d.ts +0 -13
  39. package/dist/ModerationMixin.d.ts.map +0 -1
  40. package/dist/ModerationMixin.js +0 -136
  41. package/dist/ModerationMixin.js.map +0 -1
  42. package/dist/ModerationMixin.jsonld +0 -180
  43. package/dist/ModerationOperationHandler.d.ts +0 -16
  44. package/dist/ModerationOperationHandler.d.ts.map +0 -1
  45. package/dist/ModerationOperationHandler.js +0 -45
  46. package/dist/ModerationOperationHandler.js.map +0 -1
  47. package/dist/ModerationOperationHandler.jsonld +0 -140
  48. package/dist/ModerationRecord.d.ts +0 -20
  49. package/dist/ModerationRecord.d.ts.map +0 -1
  50. package/dist/ModerationRecord.js +0 -3
  51. package/dist/ModerationRecord.js.map +0 -1
  52. package/dist/ModerationRecord.jsonld +0 -59
  53. package/dist/ModerationResourceStore.d.ts +0 -30
  54. package/dist/ModerationResourceStore.d.ts.map +0 -1
  55. package/dist/ModerationResourceStore.js +0 -167
  56. package/dist/ModerationResourceStore.js.map +0 -1
  57. package/dist/ModerationResourceStore.jsonld +0 -157
  58. package/dist/ModerationStore.d.ts +0 -12
  59. package/dist/ModerationStore.d.ts.map +0 -1
  60. package/dist/ModerationStore.js +0 -37
  61. package/dist/ModerationStore.js.map +0 -1
  62. package/dist/ModerationStore.jsonld +0 -59
  63. package/dist/util/GuardedStream.d.ts +0 -33
  64. package/dist/util/GuardedStream.d.ts.map +0 -1
  65. package/dist/util/GuardedStream.js +0 -89
  66. package/dist/util/GuardedStream.js.map +0 -1
  67. package/simple-test.json +0 -7
  68. package/src/ModerationConfig.ts +0 -29
  69. package/src/ModerationMixin.ts +0 -153
  70. package/src/ModerationOperationHandler.ts +0 -64
  71. package/src/ModerationRecord.ts +0 -19
  72. package/src/ModerationResourceStore.ts +0 -227
  73. package/src/ModerationStore.ts +0 -41
  74. package/src/util/GuardedStream.ts +0 -101
@@ -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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"}
@@ -0,0 +1,126 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.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/ModerationHandler.jsonld#ModerationHandler",
11
+ "@type": "Class",
12
+ "requireElement": "ModerationHandler",
13
+ "extends": [
14
+ "css:dist/http/ldp/OperationHandler.jsonld#OperationHandler"
15
+ ],
16
+ "parameters": [
17
+ {
18
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_enabled",
19
+ "range": "xsd:boolean"
20
+ },
21
+ {
22
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_imageNudityThreshold",
23
+ "range": "xsd:number"
24
+ },
25
+ {
26
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_textSexualThreshold",
27
+ "range": "xsd:number"
28
+ },
29
+ {
30
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_textToxicThreshold",
31
+ "range": "xsd:number"
32
+ },
33
+ {
34
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_videoNudityThreshold",
35
+ "range": "xsd:number"
36
+ },
37
+ {
38
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_client",
39
+ "range": "ksmp:dist/providers/SightEngineProvider.jsonld#SightEngineProvider"
40
+ }
41
+ ],
42
+ "memberFields": [
43
+ {
44
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_logger",
45
+ "memberFieldName": "logger"
46
+ },
47
+ {
48
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_client",
49
+ "memberFieldName": "client"
50
+ },
51
+ {
52
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_enabled",
53
+ "memberFieldName": "enabled"
54
+ },
55
+ {
56
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_imageNudityThreshold",
57
+ "memberFieldName": "imageNudityThreshold"
58
+ },
59
+ {
60
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_textSexualThreshold",
61
+ "memberFieldName": "textSexualThreshold"
62
+ },
63
+ {
64
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_textToxicThreshold",
65
+ "memberFieldName": "textToxicThreshold"
66
+ },
67
+ {
68
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_videoNudityThreshold",
69
+ "memberFieldName": "videoNudityThreshold"
70
+ },
71
+ {
72
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_constructor",
73
+ "memberFieldName": "constructor"
74
+ },
75
+ {
76
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_handle",
77
+ "memberFieldName": "handle"
78
+ },
79
+ {
80
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_shouldModerate",
81
+ "memberFieldName": "shouldModerate"
82
+ },
83
+ {
84
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_isModeratable",
85
+ "memberFieldName": "isModeratable"
86
+ },
87
+ {
88
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_moderateImage",
89
+ "memberFieldName": "moderateImage"
90
+ },
91
+ {
92
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_moderateVideo",
93
+ "memberFieldName": "moderateVideo"
94
+ },
95
+ {
96
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_moderateText",
97
+ "memberFieldName": "moderateText"
98
+ },
99
+ {
100
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler__member_getFileExtension",
101
+ "memberFieldName": "getFileExtension"
102
+ }
103
+ ],
104
+ "constructorArguments": [
105
+ {
106
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_client"
107
+ },
108
+ {
109
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_enabled"
110
+ },
111
+ {
112
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_imageNudityThreshold"
113
+ },
114
+ {
115
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_textSexualThreshold"
116
+ },
117
+ {
118
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_textToxicThreshold"
119
+ },
120
+ {
121
+ "@id": "ksmp:dist/ModerationHandler.jsonld#ModerationHandler_videoNudityThreshold"
122
+ }
123
+ ]
124
+ }
125
+ ]
126
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "@context": [
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/config/context.jsonld"
5
+ ],
6
+ "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
+ "@type": "Module",
8
+ "requireName": "@kkuffour/solid-moderation-plugin",
9
+ "import": [
10
+ "npmd:@kkuffour/solid-moderation-plugin/^0.3.0/dist/ModerationHandler.jsonld",
11
+ "npmd:@kkuffour/solid-moderation-plugin/^0.3.0/dist/providers/SightEngineProvider.jsonld"
12
+ ]
13
+ }
@@ -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/dist/index.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- export * from './ModerationOperationHandler';
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';
8
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,iCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -14,11 +14,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./ModerationOperationHandler"), exports);
18
- __exportStar(require("./ModerationResourceStore"), exports);
19
- __exportStar(require("./ModerationConfig"), exports);
20
- __exportStar(require("./ModerationStore"), exports);
21
- __exportStar(require("./ModerationRecord"), exports);
22
- __exportStar(require("./ModerationMixin"), exports);
17
+ __exportStar(require("./ModerationHandler"), exports);
23
18
  __exportStar(require("./providers/SightEngineProvider"), exports);
24
19
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAoC;AACpC,kEAAgD"}
@@ -1,7 +1,7 @@
1
1
  {
2
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"
3
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/components/context.jsonld",
4
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/config/context.jsonld"
5
5
  ],
6
6
  "@id": "npmd:@kkuffour/solid-moderation-plugin",
7
7
  "components": [
package/package.json CHANGED
@@ -1,25 +1,24 @@
1
1
  {
2
2
  "name": "@kkuffour/solid-moderation-plugin",
3
- "version": "0.2.2",
3
+ "version": "0.3.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
- "lsd:components": "components/components.jsonld",
8
+ "lsd:components": "dist/components/components.jsonld",
9
9
  "lsd:contexts": {
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"
10
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/components/context.jsonld": "dist/components/context.jsonld",
11
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/config/context.jsonld": "config/context.jsonld"
12
12
  },
13
13
  "lsd:importPaths": {
14
- "https://linkedsoftwaredependencies.org/bundles/npm/@solid/moderation-plugin/^1.0.0/components/": "dist/",
15
- "https://linkedsoftwaredependencies.org/bundles/npm/@solid/moderation-plugin/^1.0.0/config/": "config/",
16
- "https://linkedsoftwaredependencies.org/bundles/npm/@solid/moderation-plugin/^1.0.0/dist/": "dist/"
14
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/components/": "dist/components/",
15
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/config/": "config/",
16
+ "https://linkedsoftwaredependencies.org/bundles/npm/@kkuffour/solid-moderation-plugin/^0.3.0/dist/": "dist/"
17
17
  },
18
18
  "scripts": {
19
19
  "build": "npm run build:ts && npm run build:components",
20
20
  "build:ts": "tsc",
21
- "build:components": "componentsjs-generator",
22
- "build:copy": "mkdir -p dist/components && cp components/*.jsonld dist/components/",
21
+ "build:components": "componentsjs-generator -s src -c dist/components -r ksmp && cp components/context.jsonld dist/components/",
23
22
  "test": "jest"
24
23
  },
25
24
  "keywords": [
@@ -34,12 +33,13 @@
34
33
  "@solid/community-server": "^7.0.0"
35
34
  },
36
35
  "dependencies": {
36
+ "asynchronous-handlers": "^1.0.2",
37
37
  "form-data": "^4.0.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@solid/community-server": "^7.0.0",
41
+ "@types/node": "^20.0.0",
41
42
  "componentsjs-generator": "^3.1.0",
42
- "typescript": "^5.0.0",
43
- "@types/node": "^20.0.0"
43
+ "typescript": "^5.0.0"
44
44
  }
45
45
  }
@@ -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 './ModerationOperationHandler';
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';