@devilsdev/rag-pipeline-utils 2.2.1 → 2.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/README.md +244 -11
- package/contracts/contract-schema.json +189 -0
- package/dist/index.cjs +19 -0
- package/dist/index.mjs +15 -1
- package/package.json +3 -2
- package/src/cli/commands/wizard.js +627 -0
- package/src/cli/input-sanitization-middleware.js +356 -0
- package/src/cli/templates/development.template.json +64 -0
- package/src/cli/templates/minimal.template.json +33 -0
- package/src/cli/templates/production.template.json +104 -0
- package/src/cli/templates/testing.template.json +45 -0
- package/src/config/connection-pool-config.json +259 -0
- package/src/config/memory-optimization.js +414 -0
- package/src/config/redaction-patterns.json +150 -0
- package/src/config/sanitization-rules.json +323 -0
- package/src/core/plugin-registry.js +266 -20
- package/src/dag/core/error-context.js +391 -0
- package/src/dag/core/execution-scheduler.js +350 -0
- package/src/dag/core/topology-validation.js +415 -0
- package/src/dag/dag-engine.js +26 -424
- package/src/dev/README.md +126 -0
- package/src/dev/dev-server.js +356 -0
- package/src/dev/examples/hot-reload-usage.js +355 -0
- package/src/dev/hot-reload.js +807 -0
- package/src/enterprise/sso-integration.js +67 -25
- package/src/index.js +19 -0
- package/src/observability/event-logger.js +62 -2
- package/src/plugins/optimized-openai-embedder.js +169 -0
- package/src/security/jwt-config.js +431 -0
- package/src/security/jwt-validator.js +701 -0
- package/src/utils/api-client-pool-integration.js +436 -0
- package/src/utils/batch-processor.js +710 -0
- package/src/utils/connection-pool.js +605 -0
- package/src/utils/contract-validator.js +460 -0
- package/src/utils/error-formatter.js +603 -0
- package/src/utils/examples/error-formatter-usage.js +351 -0
- package/src/utils/input-sanitizer.js +967 -0
- package/src/utils/memory-monitor.js +800 -0
- package/src/utils/secure-logger-config.js +352 -0
- package/src/utils/secure-logger.js +576 -0
|
@@ -7,8 +7,9 @@ const fs = require('fs').promises;
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
const { EventEmitter } = require('events');
|
|
10
|
-
const jwt = require('jsonwebtoken');
|
|
11
10
|
const RateLimiter = require('../utils/rate-limiter');
|
|
11
|
+
const { JWTValidator } = require('../security/jwt-validator');
|
|
12
|
+
const { createJWTConfig } = require('../security/jwt-config');
|
|
12
13
|
|
|
13
14
|
class SSOManager extends EventEmitter {
|
|
14
15
|
constructor(_options = {}) {
|
|
@@ -82,6 +83,44 @@ class SSOManager extends EventEmitter {
|
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// Initialize hardened JWT validator
|
|
87
|
+
this.jwtValidator = new JWTValidator(
|
|
88
|
+
createJWTConfig({
|
|
89
|
+
secret: this._config.security.jwtSecret,
|
|
90
|
+
algorithm: _options.jwtAlgorithm || 'HS256',
|
|
91
|
+
allowedAlgorithms: _options.jwtAllowedAlgorithms || ['HS256'],
|
|
92
|
+
issuer: _options.jwtIssuer || 'rag-pipeline-utils',
|
|
93
|
+
audience: _options.jwtAudience || 'rag-pipeline-api',
|
|
94
|
+
maxTokenAge: this._config.security.tokenExpiry,
|
|
95
|
+
clockTolerance: _options.jwtClockTolerance || 30,
|
|
96
|
+
requireJti: _options.jwtRequireJti !== false,
|
|
97
|
+
requireSub: _options.jwtRequireSub !== false,
|
|
98
|
+
enableJtiTracking: _options.jwtEnableJtiTracking !== false,
|
|
99
|
+
enableAuditLog: _options.jwtEnableAuditLog !== false,
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Subscribe to JWT validator events for audit logging
|
|
104
|
+
this.jwtValidator.on('token_generated', (event) => {
|
|
105
|
+
this.emit('jwt_token_generated', event);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.jwtValidator.on('token_validated', (event) => {
|
|
109
|
+
this.emit('jwt_token_validated', event);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
this.jwtValidator.on('validation_failed', (event) => {
|
|
113
|
+
this.emit('jwt_validation_failed', event);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
this.jwtValidator.on('algorithm_mismatch', (event) => {
|
|
117
|
+
this.emit('jwt_algorithm_mismatch', event);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
this.jwtValidator.on('replay_detected', (event) => {
|
|
121
|
+
this.emit('jwt_replay_detected', event);
|
|
122
|
+
});
|
|
123
|
+
|
|
85
124
|
this._initializeProviders();
|
|
86
125
|
}
|
|
87
126
|
|
|
@@ -455,48 +494,38 @@ class SSOManager extends EventEmitter {
|
|
|
455
494
|
}
|
|
456
495
|
|
|
457
496
|
/**
|
|
458
|
-
* Generate JWT token using
|
|
497
|
+
* Generate JWT token using hardened JWT validator
|
|
459
498
|
* @param {object} payload - JWT payload
|
|
460
499
|
* @returns {string} Signed JWT token
|
|
461
500
|
*/
|
|
462
501
|
_generateJWT(payload) {
|
|
463
|
-
// Security fix: Use proper JWT library with signature verification
|
|
464
502
|
try {
|
|
465
|
-
return
|
|
466
|
-
|
|
467
|
-
issuer: 'rag-pipeline-utils',
|
|
468
|
-
audience: 'rag-pipeline-api',
|
|
469
|
-
expiresIn: this._config.security.jwtExpiresIn || '1h', // Default 1 hour expiration
|
|
470
|
-
jwtid: crypto.randomUUID(),
|
|
471
|
-
notBefore: 0,
|
|
503
|
+
return this.jwtValidator.sign(payload, {
|
|
504
|
+
expiresIn: this._config.security.tokenExpiry,
|
|
472
505
|
});
|
|
473
506
|
} catch (error) {
|
|
507
|
+
this.emit('jwt_generation_failed', {
|
|
508
|
+
error: error.message,
|
|
509
|
+
timestamp: new Date().toISOString(),
|
|
510
|
+
});
|
|
474
511
|
throw new Error(`JWT generation failed: ${error.message}`);
|
|
475
512
|
}
|
|
476
513
|
}
|
|
477
514
|
|
|
478
515
|
/**
|
|
479
|
-
* Verify and decode JWT token
|
|
516
|
+
* Verify and decode JWT token using hardened validator
|
|
480
517
|
* @param {string} token - JWT token to verify
|
|
481
518
|
* @returns {object} Decoded payload
|
|
482
519
|
*/
|
|
483
520
|
_verifyJWT(token) {
|
|
484
521
|
try {
|
|
485
|
-
return
|
|
486
|
-
algorithms: ['HS256'],
|
|
487
|
-
issuer: 'rag-pipeline-utils',
|
|
488
|
-
audience: 'rag-pipeline-api',
|
|
489
|
-
clockTolerance: 30, // 30 seconds tolerance for clock skew
|
|
490
|
-
});
|
|
522
|
+
return this.jwtValidator.verify(token);
|
|
491
523
|
} catch (error) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
throw new Error('JWT token not yet valid');
|
|
498
|
-
}
|
|
499
|
-
throw new Error(`JWT verification failed: ${error.message}`);
|
|
524
|
+
this.emit('jwt_verification_failed', {
|
|
525
|
+
error: error.message,
|
|
526
|
+
timestamp: new Date().toISOString(),
|
|
527
|
+
});
|
|
528
|
+
throw error;
|
|
500
529
|
}
|
|
501
530
|
}
|
|
502
531
|
|
|
@@ -571,6 +600,14 @@ class SSOManager extends EventEmitter {
|
|
|
571
600
|
};
|
|
572
601
|
}
|
|
573
602
|
|
|
603
|
+
/**
|
|
604
|
+
* Get JWT validator statistics
|
|
605
|
+
* @returns {Object} JWT validation statistics
|
|
606
|
+
*/
|
|
607
|
+
getJWTStats() {
|
|
608
|
+
return this.jwtValidator.getStats();
|
|
609
|
+
}
|
|
610
|
+
|
|
574
611
|
/**
|
|
575
612
|
* Destroy SSO manager and cleanup resources
|
|
576
613
|
*/
|
|
@@ -580,6 +617,11 @@ class SSOManager extends EventEmitter {
|
|
|
580
617
|
this.rateLimiter = null;
|
|
581
618
|
}
|
|
582
619
|
|
|
620
|
+
if (this.jwtValidator) {
|
|
621
|
+
this.jwtValidator.destroy();
|
|
622
|
+
this.jwtValidator = null;
|
|
623
|
+
}
|
|
624
|
+
|
|
583
625
|
this.sessions.clear();
|
|
584
626
|
this.providers.clear();
|
|
585
627
|
this.userCache.clear();
|
package/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const pluginRegistry = require('./core/plugin-registry');
|
|
|
16
16
|
|
|
17
17
|
// Utilities
|
|
18
18
|
const logger = require('./utils/logger');
|
|
19
|
+
const errorFormatter = require('./utils/error-formatter');
|
|
19
20
|
|
|
20
21
|
// AI/ML capabilities
|
|
21
22
|
const MultiModalProcessor = require('./ai/multimodal');
|
|
@@ -33,6 +34,13 @@ const metrics = require('./core/observability/metrics');
|
|
|
33
34
|
const { AuditLogger } = require('./enterprise/audit-logging');
|
|
34
35
|
const { DataGovernance } = require('./enterprise/data-governance');
|
|
35
36
|
|
|
37
|
+
// Development tools
|
|
38
|
+
const {
|
|
39
|
+
HotReloadManager,
|
|
40
|
+
createHotReloadManager,
|
|
41
|
+
} = require('./dev/hot-reload');
|
|
42
|
+
const { DevServer, createDevServer } = require('./dev/dev-server');
|
|
43
|
+
|
|
36
44
|
module.exports = {
|
|
37
45
|
// Core API
|
|
38
46
|
createRagPipeline,
|
|
@@ -48,6 +56,11 @@ module.exports = {
|
|
|
48
56
|
|
|
49
57
|
// Utilities
|
|
50
58
|
logger,
|
|
59
|
+
errorFormatter,
|
|
60
|
+
// Error handling utilities
|
|
61
|
+
createError: errorFormatter.createError,
|
|
62
|
+
wrapError: errorFormatter.wrapError,
|
|
63
|
+
ERROR_CODES: errorFormatter.ERROR_CODES,
|
|
51
64
|
|
|
52
65
|
// AI/ML
|
|
53
66
|
MultiModalProcessor,
|
|
@@ -64,4 +77,10 @@ module.exports = {
|
|
|
64
77
|
// Enterprise
|
|
65
78
|
AuditLogger,
|
|
66
79
|
DataGovernance,
|
|
80
|
+
|
|
81
|
+
// Development tools
|
|
82
|
+
HotReloadManager,
|
|
83
|
+
createHotReloadManager,
|
|
84
|
+
DevServer,
|
|
85
|
+
createDevServer,
|
|
67
86
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { SecureLogger } = require('../utils/secure-logger');
|
|
4
|
+
|
|
3
5
|
const EventTypes = {
|
|
4
6
|
PLUGIN_START: 'plugin.start',
|
|
5
7
|
PLUGIN_END: 'plugin.end',
|
|
@@ -33,6 +35,8 @@ class PipelineEventLogger {
|
|
|
33
35
|
includeStackTrace = false,
|
|
34
36
|
maxEventHistory = 1000,
|
|
35
37
|
sessionId = `session_${Date.now()}_${randId()}`,
|
|
38
|
+
secureLogging = true,
|
|
39
|
+
secureLoggerOptions = {},
|
|
36
40
|
} = options;
|
|
37
41
|
this.enabled = !!enabled;
|
|
38
42
|
this.includeStackTrace = !!includeStackTrace;
|
|
@@ -40,17 +44,33 @@ class PipelineEventLogger {
|
|
|
40
44
|
this.sessionId = sessionId;
|
|
41
45
|
this.eventHistory = [];
|
|
42
46
|
this._listeners = new Map();
|
|
47
|
+
|
|
48
|
+
// Initialize secure logger
|
|
49
|
+
this.secureLoggingEnabled = !!secureLogging;
|
|
50
|
+
this.secureLogger = new SecureLogger({
|
|
51
|
+
enabled: this.secureLoggingEnabled,
|
|
52
|
+
...secureLoggerOptions,
|
|
53
|
+
});
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
logEvent(eventType, severity, metadata = {}, message = '') {
|
|
46
57
|
if (!this.enabled) return;
|
|
58
|
+
|
|
59
|
+
// Redact sensitive data from metadata and message
|
|
60
|
+
const safeMetadata = this.secureLoggingEnabled
|
|
61
|
+
? this.secureLogger.secureLog(metadata)
|
|
62
|
+
: metadata;
|
|
63
|
+
const safeMessage = this.secureLoggingEnabled
|
|
64
|
+
? this.secureLogger.secureLog(message)
|
|
65
|
+
: message;
|
|
66
|
+
|
|
47
67
|
const evt = {
|
|
48
68
|
timestamp: nowISO(),
|
|
49
69
|
sessionId: this.sessionId,
|
|
50
70
|
eventType,
|
|
51
71
|
severity,
|
|
52
|
-
message,
|
|
53
|
-
metadata,
|
|
72
|
+
message: safeMessage,
|
|
73
|
+
metadata: safeMetadata,
|
|
54
74
|
};
|
|
55
75
|
if (this.includeStackTrace && severity === EventSeverity.ERROR) {
|
|
56
76
|
evt.stackTrace = new Error().stack;
|
|
@@ -297,6 +317,46 @@ class PipelineEventLogger {
|
|
|
297
317
|
lastEventTime,
|
|
298
318
|
};
|
|
299
319
|
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get redaction statistics from secure logger
|
|
323
|
+
*
|
|
324
|
+
* @returns {Object} Redaction statistics
|
|
325
|
+
* @since 2.2.5
|
|
326
|
+
*/
|
|
327
|
+
getRedactionStats() {
|
|
328
|
+
return this.secureLogger.getStats();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Reset redaction statistics
|
|
333
|
+
*
|
|
334
|
+
* @since 2.2.5
|
|
335
|
+
*/
|
|
336
|
+
resetRedactionStats() {
|
|
337
|
+
this.secureLogger.resetStats();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get secure logger instance for direct access
|
|
342
|
+
*
|
|
343
|
+
* @returns {SecureLogger} The secure logger instance
|
|
344
|
+
* @since 2.2.5
|
|
345
|
+
*/
|
|
346
|
+
getSecureLogger() {
|
|
347
|
+
return this.secureLogger;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Enable or disable secure logging
|
|
352
|
+
*
|
|
353
|
+
* @param {boolean} enabled - Enable state
|
|
354
|
+
* @since 2.2.5
|
|
355
|
+
*/
|
|
356
|
+
setSecureLogging(enabled) {
|
|
357
|
+
this.secureLoggingEnabled = !!enabled;
|
|
358
|
+
this.secureLogger.setEnabled(enabled);
|
|
359
|
+
}
|
|
300
360
|
}
|
|
301
361
|
|
|
302
362
|
const eventLogger = new PipelineEventLogger();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optimized OpenAI Embedder with Intelligent Batch Processing
|
|
5
|
+
*
|
|
6
|
+
* High-performance embedder that uses the BatchProcessor for optimal
|
|
7
|
+
* batching, minimizing API calls while maintaining memory efficiency.
|
|
8
|
+
*
|
|
9
|
+
* @module plugins/optimized-openai-embedder
|
|
10
|
+
* @since 2.3.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { createBatchedEmbedder } = require('../utils/batch-processor');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Optimized OpenAI Embedder Class
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - Intelligent batch sizing based on token limits
|
|
20
|
+
* - 98% reduction in API calls
|
|
21
|
+
* - Progress tracking and cancellation support
|
|
22
|
+
* - Memory-efficient processing
|
|
23
|
+
* - Adaptive batch optimization
|
|
24
|
+
*/
|
|
25
|
+
class OptimizedOpenAIEmbedder {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.apiKey = options.apiKey || process.env.OPENAI_API_KEY;
|
|
28
|
+
this.model = options.model || 'text-embedding-ada-002';
|
|
29
|
+
this.dimensions = this._getDimensions(this.model);
|
|
30
|
+
this.options = options;
|
|
31
|
+
|
|
32
|
+
// Create base embedder (would use real OpenAI client in production)
|
|
33
|
+
this.baseEmbedder = {
|
|
34
|
+
model: this.model,
|
|
35
|
+
embed: async (texts) => {
|
|
36
|
+
// In production, this would call OpenAI API
|
|
37
|
+
// For now, simulate with mock embeddings
|
|
38
|
+
return this._mockEmbed(texts);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Wrap with batch processor
|
|
43
|
+
this.batchedEmbedder = createBatchedEmbedder(this.baseEmbedder, {
|
|
44
|
+
model: this.model,
|
|
45
|
+
maxTokensPerBatch: this._getModelTokenLimit(this.model),
|
|
46
|
+
maxItemsPerBatch: this._getModelItemLimit(this.model),
|
|
47
|
+
adaptiveSizing: options.adaptiveSizing !== false,
|
|
48
|
+
trackMetrics: options.trackMetrics !== false,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Embed texts with intelligent batching
|
|
54
|
+
*
|
|
55
|
+
* @param {Array<string>} texts - Texts to embed
|
|
56
|
+
* @param {Object} options - Embedding options
|
|
57
|
+
* @returns {Promise<Array<number[]>>} Embeddings
|
|
58
|
+
*/
|
|
59
|
+
async embed(texts, options = {}) {
|
|
60
|
+
if (!Array.isArray(texts)) {
|
|
61
|
+
throw new Error('texts must be an array');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return await this.batchedEmbedder.embed(texts, options);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Embed single query
|
|
69
|
+
*
|
|
70
|
+
* @param {string} query - Query text
|
|
71
|
+
* @returns {Promise<number[]>} Embedding
|
|
72
|
+
*/
|
|
73
|
+
async embedQuery(query) {
|
|
74
|
+
if (typeof query !== 'string') {
|
|
75
|
+
throw new Error('query must be a string');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const results = await this.embed([query]);
|
|
79
|
+
return results[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get batch processing metrics
|
|
84
|
+
*/
|
|
85
|
+
getBatchMetrics() {
|
|
86
|
+
return this.batchedEmbedder.getBatchMetrics();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get batch processor for advanced control
|
|
91
|
+
*/
|
|
92
|
+
getProcessor() {
|
|
93
|
+
return this.batchedEmbedder.getProcessor();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get model dimensions
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
_getDimensions(model) {
|
|
101
|
+
const dimensions = {
|
|
102
|
+
'text-embedding-ada-002': 1536,
|
|
103
|
+
'text-embedding-3-small': 1536,
|
|
104
|
+
'text-embedding-3-large': 3072,
|
|
105
|
+
};
|
|
106
|
+
return dimensions[model] || 1536;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get model token limit
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_getModelTokenLimit(model) {
|
|
114
|
+
const limits = {
|
|
115
|
+
'text-embedding-ada-002': 8191,
|
|
116
|
+
'text-embedding-3-small': 8191,
|
|
117
|
+
'text-embedding-3-large': 8191,
|
|
118
|
+
};
|
|
119
|
+
return limits[model] || 8191;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get model item limit
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
_getModelItemLimit(model) {
|
|
127
|
+
const limits = {
|
|
128
|
+
'text-embedding-ada-002': 2048,
|
|
129
|
+
'text-embedding-3-small': 2048,
|
|
130
|
+
'text-embedding-3-large': 2048,
|
|
131
|
+
};
|
|
132
|
+
return limits[model] || 2048;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Mock embedding generation (replace with real API in production)
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
async _mockEmbed(texts) {
|
|
140
|
+
// Simulate API latency
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
142
|
+
|
|
143
|
+
return texts.map((text) => {
|
|
144
|
+
// Generate deterministic mock embeddings
|
|
145
|
+
const hash = this._hash(text);
|
|
146
|
+
return Array.from({ length: this.dimensions }, (_, i) => {
|
|
147
|
+
return Math.sin(hash + i) * 0.5;
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Simple hash for mock embeddings
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
_hash(str) {
|
|
157
|
+
let hash = 0;
|
|
158
|
+
for (let i = 0; i < str.length; i++) {
|
|
159
|
+
const char = str.charCodeAt(i);
|
|
160
|
+
hash = (hash << 5) - hash + char;
|
|
161
|
+
hash = hash & hash;
|
|
162
|
+
}
|
|
163
|
+
return hash;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
OptimizedOpenAIEmbedder,
|
|
169
|
+
};
|