@jagilber-org/index-server 1.28.10 → 1.28.19
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/CHANGELOG.md +109 -1
- package/CONTRIBUTING.md +13 -0
- package/README.md +10 -14
- package/dist/config/featureConfig.js +4 -1
- package/dist/dashboard/client/admin.html +69 -29
- package/dist/dashboard/client/js/admin.embeddings.js +97 -5
- package/dist/dashboard/client/js/admin.instructions.js +1 -1
- package/dist/dashboard/server/AdminPanel.js +38 -0
- package/dist/dashboard/server/ApiRoutes.js +14 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +76 -1
- package/dist/dashboard/server/routes/instructions.routes.js +4 -11
- package/dist/dashboard/server/routes/scripts.routes.js +35 -10
- package/dist/dashboard/server/routes/status.routes.js +77 -0
- package/dist/models/instruction.d.ts +2 -1
- package/dist/models/instruction.js +2 -0
- package/dist/schemas/index-server.code-schema.json +52478 -0
- package/dist/schemas/index.d.ts +7 -164
- package/dist/schemas/index.js +45 -63
- package/dist/schemas/instructionSchema.d.ts +46 -0
- package/dist/schemas/instructionSchema.js +159 -0
- package/{schemas → dist/schemas}/json-schema/instruction-content-type.schema.json +6 -4
- package/{schemas → dist/schemas}/json-schema/instruction-instruction-entry.schema.json +6 -4
- package/dist/schemas/manifest.json +78 -0
- package/dist/server/index-server.js +7 -1
- package/dist/services/bootstrapGating.js +2 -2
- package/dist/services/handlers/instructions.add.js +18 -0
- package/dist/services/handlers/instructions.groom.js +6 -1
- package/dist/services/handlers/instructions.import.js +42 -7
- package/dist/services/handlers.activation.js +3 -1
- package/dist/services/handlers.dashboardConfig.js +2 -1
- package/dist/services/handlers.feedback.d.ts +4 -4
- package/dist/services/handlers.feedback.js +390 -27
- package/dist/services/handlers.instructionSchema.js +73 -31
- package/dist/services/handlers.search.js +11 -6
- package/dist/services/indexLoader.js +7 -0
- package/dist/services/instructionRecordValidation.js +32 -84
- package/dist/services/mcpConfig/flagCatalog.d.ts +1 -1
- package/dist/services/mcpConfig/flagCatalog.js +2 -0
- package/dist/services/mcpConfig/formats.js +2 -6
- package/dist/services/seedBootstrap.contentModel.d.ts +13 -0
- package/dist/services/seedBootstrap.contentModel.js +166 -0
- package/dist/services/seedBootstrap.contentTypes.d.ts +5 -0
- package/dist/services/seedBootstrap.contentTypes.js +76 -0
- package/dist/services/seedBootstrap.d.ts +1 -0
- package/dist/services/seedBootstrap.js +87 -10
- package/dist/services/toolRegistry.js +52 -24
- package/dist/services/toolRegistry.zod.js +84 -37
- package/dist/versioning/schemaVersion.d.ts +1 -1
- package/dist/versioning/schemaVersion.js +1 -13
- package/package.json +17 -3
- package/schemas/index-server.code-schema.json +31019 -25047
- package/schemas/instruction.schema.json +16 -6
- package/schemas/manifest.json +3 -3
- package/scripts/README.md +20 -0
- package/scripts/build/README.md +41 -0
- package/scripts/build/setup-wizard-paths.mjs +27 -0
- package/scripts/build/setup-wizard.mjs +7 -21
- package/scripts/client/README.md +26 -0
- package/scripts/client/index-server-client.ps1 +203 -0
- package/scripts/client/index-server-client.sh +149 -0
- package/scripts/client/powershell-mcp-server.ps1 +83 -0
- package/scripts/client/powershell-mcp-template.ps1 +85 -0
- package/scripts/hooks/README.md +40 -0
- package/server.json +2 -2
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-persisted-admin-session.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-config.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-data.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-manifest.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/SessionPersistence-session-persistence-metadata.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/instruction-audience-scope.schema.json +0 -0
- /package/{schemas → dist/schemas}/json-schema/instruction-requirement-level.schema.json +0 -0
|
@@ -13,6 +13,7 @@ const schemaVersion_1 = require("../../versioning/schemaVersion");
|
|
|
13
13
|
const classificationService_1 = require("../classificationService");
|
|
14
14
|
const ownershipService_1 = require("../ownershipService");
|
|
15
15
|
const auditLog_1 = require("../auditLog");
|
|
16
|
+
const logger_1 = require("../logger");
|
|
16
17
|
const toolRegistry_1 = require("../toolRegistry");
|
|
17
18
|
const runtimeConfig_1 = require("../../config/runtimeConfig");
|
|
18
19
|
const canonical_1 = require("../canonical");
|
|
@@ -274,6 +275,12 @@ const instructions_shared_1 = require("./instructions.shared");
|
|
|
274
275
|
}
|
|
275
276
|
}
|
|
276
277
|
}
|
|
278
|
+
// Caller-required minimum for index_add. Other canonical-required fields
|
|
279
|
+
// (categories, contentType, etc.) are defaulted by normalization further
|
|
280
|
+
// down (categories → 'uncategorized'; classifier fills contentType /
|
|
281
|
+
// classification / status). Do not derive this from REQUIRED_INPUT_KEYS —
|
|
282
|
+
// canonical required[] reflects the on-disk record, not the smallest
|
|
283
|
+
// payload a caller may submit.
|
|
277
284
|
const requiredFieldErrors = [
|
|
278
285
|
!e.id ? 'id: missing required field' : undefined,
|
|
279
286
|
e.title === undefined ? 'title: missing required field' : undefined,
|
|
@@ -580,6 +587,17 @@ const instructions_shared_1 = require("./instructions.shared");
|
|
|
580
587
|
return { id: e.id, success: true, skipped: true, created: false, overwritten: false, hash: st0.hash, repaired: repaired ? true : undefined };
|
|
581
588
|
}
|
|
582
589
|
// Catch-all: never expose raw Node error text (ENOENT, null-byte path errors, stack frames) to MCP clients.
|
|
590
|
+
const writeError = err instanceof Error ? err : new Error(String(err));
|
|
591
|
+
(0, logger_1.logError)('[add] instruction write failed', {
|
|
592
|
+
id: e.id,
|
|
593
|
+
error: writeError.message,
|
|
594
|
+
stack: writeError.stack,
|
|
595
|
+
});
|
|
596
|
+
(0, auditLog_1.logAudit)('add_write_failed', e.id, {
|
|
597
|
+
error: writeError.message,
|
|
598
|
+
errorName: writeError.name,
|
|
599
|
+
overwrite: Boolean(overwrite),
|
|
600
|
+
});
|
|
583
601
|
return fail('write_failed', {
|
|
584
602
|
id: e.id,
|
|
585
603
|
message: 'Instruction write failed due to an internal error. The error details are not exposed to clients.',
|
|
@@ -159,7 +159,12 @@ const instructionRecordValidation_1 = require("../instructionRecordValidation");
|
|
|
159
159
|
const skippedErrors = [];
|
|
160
160
|
try {
|
|
161
161
|
const dir = (0, indexContext_1.getInstructionsDir)();
|
|
162
|
-
|
|
162
|
+
// Exclude internal manifest (_*) and bootstrap gating state files. These
|
|
163
|
+
// are runtime bookkeeping owned by bootstrapGating.ts, not instructions.
|
|
164
|
+
// Without this filter they surface as 'missing required fields' noise in
|
|
165
|
+
// every index_repair response. RCA 2026-05-07.
|
|
166
|
+
const STATE_FILES = new Set(['bootstrap.confirmed.json', 'bootstrap.pending.json']);
|
|
167
|
+
const diskFiles = fs_1.default.readdirSync(dir).filter(f => f.endsWith('.json') && !f.startsWith('_') && !STATE_FILES.has(f));
|
|
163
168
|
// Use the compiled-in schema property set so disk-resident vs static-import
|
|
164
169
|
// schemas can never diverge (review #211 finding 9).
|
|
165
170
|
const schemaProps = (0, loaderSchemaValidator_1.getSchemaPropertyNames)();
|
|
@@ -14,6 +14,7 @@ const classificationService_1 = require("../classificationService");
|
|
|
14
14
|
const ownershipService_1 = require("../ownershipService");
|
|
15
15
|
const instructionRecordValidation_1 = require("../instructionRecordValidation");
|
|
16
16
|
const instructionRecordValidation_2 = require("../instructionRecordValidation");
|
|
17
|
+
const instructionSchema_1 = require("../../schemas/instructionSchema");
|
|
17
18
|
const auditLog_1 = require("../auditLog");
|
|
18
19
|
const logger_1 = require("../logger");
|
|
19
20
|
// Structured WARN without auto-attached call stack: the log-hygiene gate
|
|
@@ -149,10 +150,36 @@ function parseInlineEntries(rawEntries) {
|
|
|
149
150
|
let imported = 0, skipped = 0, overwritten = 0;
|
|
150
151
|
const errors = [];
|
|
151
152
|
const classifier = new classificationService_1.ClassificationService();
|
|
153
|
+
// Aggregate counts of server-managed fields we silently dropped from caller-supplied
|
|
154
|
+
// entries. Reported back so callers can see the implicit normalisation. The split is
|
|
155
|
+
// driven by SERVER_MANAGED_KEYS (annotated x-fieldClass on the canonical schema), so
|
|
156
|
+
// export→import is naturally round-trippable: server-owned timestamps/hashes are
|
|
157
|
+
// partitioned out of the input contract before validation.
|
|
158
|
+
const stripped = {};
|
|
152
159
|
const skippedIds = new Set();
|
|
153
160
|
const formatImportValidationError = (validationErrors) => `invalid_instruction: ${validationErrors.join('; ')}`;
|
|
154
|
-
for (const
|
|
155
|
-
const id =
|
|
161
|
+
for (const rawEntry of entries) {
|
|
162
|
+
const id = rawEntry?.id || 'unknown';
|
|
163
|
+
// Partition server-managed fields out of the input. Preserves usageCount /
|
|
164
|
+
// firstSeenTs / lastUsedAt for restore scenarios; discards the rest (server
|
|
165
|
+
// recomputes sourceHash, schemaVersion, createdAt, updatedAt on write).
|
|
166
|
+
const partition = (0, instructionSchema_1.splitEntry)(rawEntry);
|
|
167
|
+
const carryForward = {};
|
|
168
|
+
for (const key of Object.keys(partition.serverManaged)) {
|
|
169
|
+
stripped[key] = (stripped[key] || 0) + 1;
|
|
170
|
+
// Carry-forward fields that represent observed usage history rather than
|
|
171
|
+
// server-derived integrity. The server will re-derive sourceHash,
|
|
172
|
+
// schemaVersion, createdAt, updatedAt regardless of input.
|
|
173
|
+
if (key === 'usageCount' || key === 'firstSeenTs' || key === 'lastUsedAt' || key === 'archivedAt') {
|
|
174
|
+
carryForward[key] = partition.serverManaged[key];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const e = { ...partition.input, ...partition.unknown };
|
|
178
|
+
// Caller-required minimum for index_import. Other canonical-required
|
|
179
|
+
// fields (categories → 'uncategorized'; classifier-filled contentType)
|
|
180
|
+
// are defaulted by normalization further down. Do not derive this from
|
|
181
|
+
// REQUIRED_INPUT_KEYS — canonical required[] reflects the on-disk
|
|
182
|
+
// record, not the smallest payload a caller may submit.
|
|
156
183
|
const requiredFieldErrors = [
|
|
157
184
|
!e?.id ? 'id: missing required field' : undefined,
|
|
158
185
|
e?.title === undefined ? 'title: missing required field' : undefined,
|
|
@@ -224,7 +251,14 @@ function parseInlineEntries(rawEntries) {
|
|
|
224
251
|
skippedIds.add(e.id);
|
|
225
252
|
continue;
|
|
226
253
|
}
|
|
227
|
-
|
|
254
|
+
// For overwrites we still apply carryForward AFTER spreading `existing` so a
|
|
255
|
+
// caller-supplied usageCount / firstSeenTs / lastUsedAt / archivedAt (e.g. a
|
|
256
|
+
// restored export) wins over what's currently in the store. For new entries
|
|
257
|
+
// the same spread provides defaults from the export. carryForward is empty
|
|
258
|
+
// when the caller did not supply any of those keys.
|
|
259
|
+
const base = existing
|
|
260
|
+
? { ...existing, title: e.title, body: bodyTrimmed, rationale: e.rationale, priority: e.priority, audience: e.audience, requirement: e.requirement, categories, primaryCategory: effectivePrimary, updatedAt: now, ...carryForward }
|
|
261
|
+
: { id: e.id, title: e.title, body: bodyTrimmed, rationale: e.rationale, priority: e.priority, audience: e.audience, requirement: e.requirement, categories, primaryCategory: effectivePrimary, sourceHash: newBodyHash, schemaVersion: schemaVersion_1.SCHEMA_VERSION, deprecatedBy: e.deprecatedBy, createdAt: now, updatedAt: now, riskScore: e.riskScore, createdByAgent: instructionsCfg.agentId, sourceWorkspace: instructionsCfg.workspaceId, extensions: e.extensions, ...carryForward };
|
|
228
262
|
(0, instructions_shared_1.applyGovernanceKeys)(base, e, instructions_shared_1.IMPORT_GOVERNANCE_KEYS);
|
|
229
263
|
if (!base.sourceWorkspace)
|
|
230
264
|
base.sourceWorkspace = instructionsCfg.workspaceId;
|
|
@@ -254,9 +288,10 @@ function parseInlineEntries(rawEntries) {
|
|
|
254
288
|
(0, logger_1.logError)('[import] entry write rejected', { id: e.id, reason: 'validation-failed-at-write', error: errMsg });
|
|
255
289
|
continue;
|
|
256
290
|
}
|
|
257
|
-
const
|
|
258
|
-
errors.push({ id: e.id, error:
|
|
259
|
-
(0, logger_1.logError)('[import] entry write failed', { id: e.id, error:
|
|
291
|
+
const writeError = err instanceof Error ? err : new Error(String(err));
|
|
292
|
+
errors.push({ id: e.id, error: 'write_failed: Instruction write failed due to an internal error. The error details are not exposed to clients.' });
|
|
293
|
+
(0, logger_1.logError)('[import] entry write failed', { id: e.id, error: writeError.message, stack: writeError.stack });
|
|
294
|
+
(0, auditLog_1.logAudit)('import_write_failed', e.id, { error: writeError.message, errorName: writeError.name, mode });
|
|
260
295
|
continue;
|
|
261
296
|
}
|
|
262
297
|
if (fileExists && mode === 'overwrite')
|
|
@@ -283,7 +318,7 @@ function parseInlineEntries(rawEntries) {
|
|
|
283
318
|
(0, logger_1.logError)('[import] verification failed', { missingAfterReload: verificationErrors.length, ids: verificationErrors.map(v => v.id) });
|
|
284
319
|
}
|
|
285
320
|
const verifiedCount = writtenIds.length - verificationErrors.length;
|
|
286
|
-
const summary = { hash: st.hash, imported, skipped, overwritten, total: entries.length, errors, verified: verificationErrors.length === 0, verifiedCount, verificationErrorCount: verificationErrors.length };
|
|
321
|
+
const summary = { hash: st.hash, imported, skipped, overwritten, total: entries.length, errors, verified: verificationErrors.length === 0, verifiedCount, verificationErrorCount: verificationErrors.length, stripped };
|
|
287
322
|
(0, auditLog_1.logAudit)('import', entries.map(e => e.id), { imported, skipped, overwritten, errors: errors.length, verified: verificationErrors.length === 0 });
|
|
288
323
|
if (errors.length) {
|
|
289
324
|
warnStruct('[import] complete with errors', { imported, skipped, overwritten, total: entries.length, errorCount: errors.length, verifiedCount, verificationErrorCount: verificationErrors.length, errorIds: errors.map(e => e.id) });
|
|
@@ -73,10 +73,11 @@ const ACTIVATION_GUIDE_VERSION = '1.0.0';
|
|
|
73
73
|
function: 'activate_feedback_and_health_monitoring_tools()',
|
|
74
74
|
description: 'Feedback submission and system health monitoring',
|
|
75
75
|
tools: [
|
|
76
|
+
'feedback_manage',
|
|
76
77
|
'feedback_submit',
|
|
77
78
|
'health_check'
|
|
78
79
|
],
|
|
79
|
-
toolCount:
|
|
80
|
+
toolCount: 3
|
|
80
81
|
}
|
|
81
82
|
};
|
|
82
83
|
if (diagnosticsEnabled) {
|
|
@@ -171,6 +172,7 @@ const ACTIVATION_GUIDE_VERSION = '1.0.0';
|
|
|
171
172
|
'bootstrap_request': { category: 'bootstrap', function: 'activate_bootstrap_management_tools' },
|
|
172
173
|
'bootstrap_confirmFinalize': { category: 'bootstrap', function: 'activate_bootstrap_management_tools' },
|
|
173
174
|
'bootstrap_status': { category: 'bootstrap', function: 'activate_bootstrap_management_tools' },
|
|
175
|
+
'feedback_manage': { category: 'health', function: 'activate_feedback_and_health_monitoring_tools' },
|
|
174
176
|
'feedback_submit': { category: 'health', function: 'activate_feedback_and_health_monitoring_tools' },
|
|
175
177
|
'health_check': { category: 'health', function: 'activate_feedback_and_health_monitoring_tools' }
|
|
176
178
|
};
|
|
@@ -56,6 +56,7 @@ exports.FLAG_REGISTRY = [
|
|
|
56
56
|
{ name: 'INDEX_SERVER_DISABLE_EARLY_STDIN_BUFFER', category: 'diagnostics', description: 'Disable early stdin buffering (compare fragmentation behavior).', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
57
57
|
{ name: 'INDEX_SERVER_FATAL_EXIT_DELAY_MS', category: 'diagnostics', description: 'Delay before forced fatal exit (ms).', stability: 'diagnostic', default: '15', type: 'number', since: '1.1.1' },
|
|
58
58
|
{ name: 'INDEX_SERVER_IDLE_KEEPALIVE_MS', category: 'diagnostics', description: 'Keepalive interval for idle transports.', stability: 'stable', default: '30000', type: 'number', since: '1.0.0' },
|
|
59
|
+
{ name: 'INDEX_SERVER_DISABLE_PPID_WATCHDOG', category: 'diagnostics', description: 'Disable parent-process watchdog. Required for dev sandbox launchers that spawn through a transient shell (e.g. cmd /c start /B node).', stability: 'experimental', default: 'off', type: 'boolean', since: '1.28.14' },
|
|
59
60
|
{ name: 'INDEX_SERVER_ADD_TIMING', category: 'diagnostics', description: 'Embed per-tool timing phase marks in response envelope.', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
60
61
|
{ name: 'INDEX_SERVER_TRACE_DISPATCH_DIAG', category: 'diagnostics', description: 'Extra dispatcher timing/phase logs (use INDEX_SERVER_TRACE=dispatchDiag).', stability: 'diagnostic', default: 'off', type: 'boolean', since: '1.1.1' },
|
|
61
62
|
// Stress / adversarial
|
|
@@ -90,7 +91,7 @@ exports.FLAG_REGISTRY = [
|
|
|
90
91
|
{ name: 'INDEX_SERVER_SQLITE_PATH', category: 'storage', description: 'SQLite database file path.', stability: 'experimental', default: './data/index.db', type: 'string', since: '1.25.0' },
|
|
91
92
|
{ name: 'INDEX_SERVER_SQLITE_WAL', category: 'storage', description: 'Enable SQLite WAL journaling.', stability: 'experimental', default: 'on', type: 'boolean', since: '1.25.0' },
|
|
92
93
|
{ name: 'INDEX_SERVER_SQLITE_MIGRATE_ON_START', category: 'storage', description: 'Run schema migrations at startup.', stability: 'experimental', default: 'on', type: 'boolean', since: '1.25.0' },
|
|
93
|
-
{ name: 'INDEX_SERVER_SQLITE_VEC_ENABLED', category: 'storage', description: 'Enable sqlite-vec extension for embeddings.', stability: 'experimental', default: 'off', type: 'boolean', since: '1.25.0' },
|
|
94
|
+
{ name: 'INDEX_SERVER_SQLITE_VEC_ENABLED', category: 'storage', description: 'Enable sqlite-vec extension for embeddings. Auto-enabled when INDEX_SERVER_STORAGE_BACKEND=sqlite; set 0 to opt out. Falls back to JSON if native extension fails.', stability: 'experimental', default: 'on (when backend=sqlite), off otherwise', type: 'boolean', since: '1.25.0' },
|
|
94
95
|
{ name: 'INDEX_SERVER_SQLITE_VEC_PATH', category: 'storage', description: 'Path to sqlite-vec loadable extension.', stability: 'experimental', default: '(unset)', type: 'string', since: '1.25.0' },
|
|
95
96
|
// Feedback & messaging
|
|
96
97
|
{ name: 'INDEX_SERVER_FEEDBACK_DIR', category: 'feedback', description: 'Feedback storage directory.', stability: 'stable', default: './feedback', type: 'string', since: '1.10.0' },
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP feedback
|
|
2
|
+
* MCP feedback handlers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* feedback_submit remains a standalone alias for quick agent reporting.
|
|
5
|
+
* feedback_manage is the single action-dispatch management surface for
|
|
6
|
+
* submit/list/get/update/delete/stats.
|
|
7
7
|
*/
|
|
8
8
|
export {};
|
|
@@ -1,67 +1,430 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* MCP feedback
|
|
3
|
+
* MCP feedback handlers.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* feedback_submit remains a standalone alias for quick agent reporting.
|
|
6
|
+
* feedback_manage is the single action-dispatch management surface for
|
|
7
|
+
* submit/list/get/update/delete/stats.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
const registry_1 = require("../server/registry");
|
|
11
11
|
const logger_1 = require("./logger");
|
|
12
12
|
const auditLog_1 = require("./auditLog");
|
|
13
13
|
const feedbackStorage_1 = require("./feedbackStorage");
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const VALID_TYPES = ['issue', 'status', 'security', 'feature-request', 'bug-report', 'performance', 'usability', 'other'];
|
|
15
|
+
const VALID_SEVERITIES = ['low', 'medium', 'high', 'critical'];
|
|
16
|
+
const VALID_STATUSES = ['new', 'acknowledged', 'in-progress', 'resolved', 'closed'];
|
|
17
|
+
const MAX_TITLE_LENGTH = 200;
|
|
18
|
+
const MAX_DESCRIPTION_LENGTH = 10_000;
|
|
19
|
+
const MAX_TAGS = 10;
|
|
20
|
+
const MAX_LIST_LIMIT = 200;
|
|
21
|
+
function errorEnvelope(action, error, message, opts = {}) {
|
|
22
|
+
return {
|
|
23
|
+
action: action || 'unknown',
|
|
24
|
+
success: false,
|
|
25
|
+
error,
|
|
26
|
+
message,
|
|
27
|
+
...opts,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function isFeedbackType(value) {
|
|
31
|
+
return VALID_TYPES.includes(value);
|
|
32
|
+
}
|
|
33
|
+
function isFeedbackSeverity(value) {
|
|
34
|
+
return VALID_SEVERITIES.includes(value);
|
|
35
|
+
}
|
|
36
|
+
function isFeedbackStatus(value) {
|
|
37
|
+
return VALID_STATUSES.includes(value);
|
|
38
|
+
}
|
|
39
|
+
function sanitizeTags(value) {
|
|
40
|
+
if (!Array.isArray(value))
|
|
41
|
+
return undefined;
|
|
42
|
+
const tags = value
|
|
43
|
+
.filter((tag) => typeof tag === 'string')
|
|
44
|
+
.map(tag => tag.trim())
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.slice(0, MAX_TAGS);
|
|
47
|
+
return tags.length > 0 ? tags : undefined;
|
|
48
|
+
}
|
|
49
|
+
function validateSubmitParams(params) {
|
|
50
|
+
const missing = ['type', 'severity', 'title', 'description'].filter((field) => {
|
|
51
|
+
const value = params[field];
|
|
52
|
+
return typeof value !== 'string' || !value.trim();
|
|
53
|
+
});
|
|
54
|
+
if (missing.length) {
|
|
55
|
+
return errorEnvelope('submit', 'missing_required', `Missing required parameter(s): ${missing.join(', ')}`, {
|
|
56
|
+
hint: 'Submit requires type, severity, title, and description.',
|
|
57
|
+
});
|
|
17
58
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!validTypes.includes(params.type)) {
|
|
21
|
-
throw new Error(`Invalid type. Must be one of: ${validTypes.join(', ')}`);
|
|
59
|
+
if (!isFeedbackType(params.type)) {
|
|
60
|
+
return errorEnvelope('submit', 'invalid_param', `Invalid type. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
22
61
|
}
|
|
23
|
-
if (!
|
|
24
|
-
|
|
62
|
+
if (!isFeedbackSeverity(params.severity)) {
|
|
63
|
+
return errorEnvelope('submit', 'invalid_param', `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}`);
|
|
25
64
|
}
|
|
65
|
+
if (params.tags !== undefined && !Array.isArray(params.tags)) {
|
|
66
|
+
return errorEnvelope('submit', 'invalid_param', 'Invalid tags. Must be an array of strings.');
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function createFeedbackEntry(params) {
|
|
26
71
|
const timestamp = new Date().toISOString();
|
|
27
72
|
const entry = {
|
|
28
73
|
id: (0, feedbackStorage_1.generateFeedbackId)(params.type, timestamp),
|
|
29
74
|
timestamp,
|
|
30
75
|
type: params.type,
|
|
31
76
|
severity: params.severity,
|
|
32
|
-
title: params.title.
|
|
33
|
-
description: params.description.
|
|
77
|
+
title: params.title.trim().slice(0, MAX_TITLE_LENGTH),
|
|
78
|
+
description: params.description.slice(0, MAX_DESCRIPTION_LENGTH),
|
|
34
79
|
context: params.context,
|
|
35
80
|
metadata: params.metadata,
|
|
36
|
-
tags: params.tags
|
|
37
|
-
status: 'new'
|
|
81
|
+
tags: sanitizeTags(params.tags),
|
|
82
|
+
status: 'new',
|
|
38
83
|
};
|
|
84
|
+
return entry;
|
|
85
|
+
}
|
|
86
|
+
function emitCriticalFeedbackNotice(entry) {
|
|
87
|
+
if (entry.type !== 'security' && entry.severity !== 'critical')
|
|
88
|
+
return;
|
|
89
|
+
try {
|
|
90
|
+
process.stderr.write(`[SECURITY/CRITICAL] Feedback ID: ${entry.id}, Type: ${entry.type}, Title: ${entry.title}\n`);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Ignore stderr write failures.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function persistSubmittedFeedback(params) {
|
|
97
|
+
const entry = createFeedbackEntry(params);
|
|
39
98
|
const storage = (0, feedbackStorage_1.loadFeedbackStorage)();
|
|
40
99
|
storage.entries.push(entry);
|
|
41
100
|
(0, feedbackStorage_1.saveFeedbackStorage)(storage);
|
|
42
101
|
(0, auditLog_1.logAudit)('feedback_submit', [entry.id], {
|
|
43
102
|
type: entry.type,
|
|
44
103
|
severity: entry.severity,
|
|
45
|
-
title: entry.title
|
|
104
|
+
title: entry.title,
|
|
46
105
|
}, 'feedback');
|
|
47
106
|
(0, logger_1.logInfo)('[feedback] Feedback submitted', {
|
|
48
107
|
id: entry.id,
|
|
49
108
|
type: entry.type,
|
|
50
109
|
severity: entry.severity,
|
|
51
|
-
title: entry.title
|
|
110
|
+
title: entry.title,
|
|
52
111
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
112
|
+
emitCriticalFeedbackNotice(entry);
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
function submitFeedbackOrThrow(params) {
|
|
116
|
+
const validationError = validateSubmitParams(params);
|
|
117
|
+
if (validationError)
|
|
118
|
+
throw new Error(validationError.message);
|
|
119
|
+
const entry = persistSubmittedFeedback(params);
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
feedbackId: entry.id,
|
|
123
|
+
timestamp: entry.timestamp,
|
|
124
|
+
message: 'Feedback submitted successfully',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function submitFeedbackManaged(params) {
|
|
128
|
+
const validationError = validateSubmitParams(params);
|
|
129
|
+
if (validationError)
|
|
130
|
+
return validationError;
|
|
131
|
+
try {
|
|
132
|
+
const entry = persistSubmittedFeedback(params);
|
|
133
|
+
return {
|
|
134
|
+
action: 'submit',
|
|
135
|
+
success: true,
|
|
136
|
+
feedbackId: entry.id,
|
|
137
|
+
entry,
|
|
138
|
+
timestamp: entry.timestamp,
|
|
139
|
+
message: 'Feedback submitted successfully',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
(0, logger_1.logError)('[feedback] feedback_manage submit storage failure', error instanceof Error ? error : { error: String(error) });
|
|
144
|
+
(0, auditLog_1.logAudit)('feedback_manage_storage_error', undefined, { action: 'submit' }, 'feedback');
|
|
145
|
+
return errorEnvelope('submit', 'storage_error', 'Feedback submit failed due to a storage error. The error details are not exposed to clients.');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function loadStorageManaged(action) {
|
|
149
|
+
try {
|
|
150
|
+
return (0, feedbackStorage_1.loadFeedbackStorage)();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
(0, logger_1.logError)('[feedback] feedback_manage storage load failure', error instanceof Error ? error : { error: String(error), action });
|
|
154
|
+
(0, auditLog_1.logAudit)('feedback_manage_storage_error', undefined, { action }, 'feedback');
|
|
155
|
+
return errorEnvelope(action, 'storage_error', 'Feedback storage could not be loaded. The error details are not exposed to clients.');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function saveStorageManaged(action, storage, id) {
|
|
159
|
+
try {
|
|
160
|
+
(0, feedbackStorage_1.saveFeedbackStorage)(storage);
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
(0, logger_1.logError)('[feedback] feedback_manage storage save failure', error instanceof Error ? error : { error: String(error), action, id });
|
|
165
|
+
(0, auditLog_1.logAudit)('feedback_manage_storage_error', id ? [id] : undefined, { action }, 'feedback');
|
|
166
|
+
return errorEnvelope(action, 'storage_error', 'Feedback update failed due to a storage error. The error details are not exposed to clients.', { id });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function requireId(action, id) {
|
|
170
|
+
if (typeof id !== 'string' || !id.trim()) {
|
|
171
|
+
return errorEnvelope(action, 'missing_required', 'Missing required parameter: id', {
|
|
172
|
+
hint: `${action} requires a feedback entry id.`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return id;
|
|
176
|
+
}
|
|
177
|
+
function listFeedback(params) {
|
|
178
|
+
const storage = loadStorageManaged('list');
|
|
179
|
+
if ('success' in storage)
|
|
180
|
+
return storage;
|
|
181
|
+
let entries = [...storage.entries];
|
|
182
|
+
if (params.type !== undefined) {
|
|
183
|
+
if (!isFeedbackType(String(params.type)))
|
|
184
|
+
return errorEnvelope('list', 'invalid_param', `Invalid type. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
185
|
+
entries = entries.filter(entry => entry.type === params.type);
|
|
186
|
+
}
|
|
187
|
+
if (params.severity !== undefined) {
|
|
188
|
+
if (!isFeedbackSeverity(String(params.severity)))
|
|
189
|
+
return errorEnvelope('list', 'invalid_param', `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}`);
|
|
190
|
+
entries = entries.filter(entry => entry.severity === params.severity);
|
|
191
|
+
}
|
|
192
|
+
if (params.status !== undefined) {
|
|
193
|
+
if (!isFeedbackStatus(String(params.status)))
|
|
194
|
+
return errorEnvelope('list', 'invalid_param', `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}`);
|
|
195
|
+
entries = entries.filter(entry => entry.status === params.status);
|
|
196
|
+
}
|
|
197
|
+
if (params.since !== undefined) {
|
|
198
|
+
if (Number.isNaN(Date.parse(params.since)))
|
|
199
|
+
return errorEnvelope('list', 'invalid_param', 'Invalid since. Must be an ISO-compatible date string.');
|
|
200
|
+
entries = entries.filter(entry => entry.timestamp >= params.since);
|
|
201
|
+
}
|
|
202
|
+
if (params.tags !== undefined) {
|
|
203
|
+
if (!Array.isArray(params.tags))
|
|
204
|
+
return errorEnvelope('list', 'invalid_param', 'Invalid tags. Must be an array of strings.');
|
|
205
|
+
const tags = sanitizeTags(params.tags) ?? [];
|
|
206
|
+
if (tags.length) {
|
|
207
|
+
entries = entries.filter(entry => entry.tags?.some(tag => tags.includes(tag)));
|
|
56
208
|
}
|
|
57
|
-
|
|
58
|
-
|
|
209
|
+
}
|
|
210
|
+
entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
211
|
+
const limit = params.limit === undefined ? 50 : params.limit;
|
|
212
|
+
const offset = params.offset === undefined ? 0 : params.offset;
|
|
213
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
|
|
214
|
+
return errorEnvelope('list', 'invalid_param', `Invalid limit. Must be an integer from 1 to ${MAX_LIST_LIMIT}.`);
|
|
215
|
+
}
|
|
216
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
217
|
+
return errorEnvelope('list', 'invalid_param', 'Invalid offset. Must be a non-negative integer.');
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
action: 'list',
|
|
221
|
+
success: true,
|
|
222
|
+
entries: entries.slice(offset, offset + limit),
|
|
223
|
+
total: entries.length,
|
|
224
|
+
limit,
|
|
225
|
+
offset,
|
|
226
|
+
hasMore: offset + limit < entries.length,
|
|
227
|
+
lastUpdated: storage.lastUpdated,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function getFeedback(params) {
|
|
231
|
+
const id = requireId('get', params.id);
|
|
232
|
+
if (typeof id !== 'string')
|
|
233
|
+
return id;
|
|
234
|
+
const storage = loadStorageManaged('get');
|
|
235
|
+
if ('success' in storage)
|
|
236
|
+
return storage;
|
|
237
|
+
const entry = storage.entries.find(item => item.id === id);
|
|
238
|
+
if (!entry) {
|
|
239
|
+
return errorEnvelope('get', 'not_found', `Feedback entry not found: ${id}`, {
|
|
240
|
+
id,
|
|
241
|
+
hint: 'Use feedback_manage with action=list to inspect available entries.',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return { action: 'get', success: true, entry };
|
|
245
|
+
}
|
|
246
|
+
function updateFeedback(params) {
|
|
247
|
+
const id = requireId('update', params.id);
|
|
248
|
+
if (typeof id !== 'string')
|
|
249
|
+
return id;
|
|
250
|
+
const storage = loadStorageManaged('update');
|
|
251
|
+
if ('success' in storage)
|
|
252
|
+
return storage;
|
|
253
|
+
const index = storage.entries.findIndex(item => item.id === id);
|
|
254
|
+
if (index === -1) {
|
|
255
|
+
return errorEnvelope('update', 'not_found', `Feedback entry not found: ${id}`, {
|
|
256
|
+
id,
|
|
257
|
+
hint: 'Use feedback_manage with action=list to inspect available entries.',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
const entry = { ...storage.entries[index] };
|
|
261
|
+
const updatedFields = [];
|
|
262
|
+
if (params.status !== undefined) {
|
|
263
|
+
if (!isFeedbackStatus(String(params.status)))
|
|
264
|
+
return errorEnvelope('update', 'invalid_param', `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}`, { id });
|
|
265
|
+
entry.status = params.status;
|
|
266
|
+
updatedFields.push('status');
|
|
267
|
+
}
|
|
268
|
+
if (params.title !== undefined) {
|
|
269
|
+
if (typeof params.title !== 'string' || !params.title.trim())
|
|
270
|
+
return errorEnvelope('update', 'invalid_param', 'Invalid title. Must be a non-empty string.', { id });
|
|
271
|
+
entry.title = params.title.trim().slice(0, MAX_TITLE_LENGTH);
|
|
272
|
+
updatedFields.push('title');
|
|
273
|
+
}
|
|
274
|
+
if (params.description !== undefined) {
|
|
275
|
+
if (typeof params.description !== 'string')
|
|
276
|
+
return errorEnvelope('update', 'invalid_param', 'Invalid description. Must be a string.', { id });
|
|
277
|
+
entry.description = params.description.slice(0, MAX_DESCRIPTION_LENGTH);
|
|
278
|
+
updatedFields.push('description');
|
|
279
|
+
}
|
|
280
|
+
if (params.severity !== undefined) {
|
|
281
|
+
if (!isFeedbackSeverity(String(params.severity)))
|
|
282
|
+
return errorEnvelope('update', 'invalid_param', `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}`, { id });
|
|
283
|
+
entry.severity = params.severity;
|
|
284
|
+
updatedFields.push('severity');
|
|
285
|
+
}
|
|
286
|
+
if (params.tags !== undefined) {
|
|
287
|
+
if (!Array.isArray(params.tags))
|
|
288
|
+
return errorEnvelope('update', 'invalid_param', 'Invalid tags. Must be an array of strings.', { id });
|
|
289
|
+
entry.tags = sanitizeTags(params.tags);
|
|
290
|
+
updatedFields.push('tags');
|
|
291
|
+
}
|
|
292
|
+
if (params.metadata !== undefined) {
|
|
293
|
+
if (!params.metadata || typeof params.metadata !== 'object' || Array.isArray(params.metadata)) {
|
|
294
|
+
return errorEnvelope('update', 'invalid_param', 'Invalid metadata. Must be an object.', { id });
|
|
59
295
|
}
|
|
296
|
+
entry.metadata = { ...entry.metadata, ...params.metadata };
|
|
297
|
+
updatedFields.push('metadata');
|
|
60
298
|
}
|
|
299
|
+
if (!updatedFields.length) {
|
|
300
|
+
return errorEnvelope('update', 'missing_required', 'No update fields provided.', {
|
|
301
|
+
id,
|
|
302
|
+
hint: 'Provide one or more of status, title, description, severity, tags, or metadata.',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
storage.entries[index] = entry;
|
|
306
|
+
const saveError = saveStorageManaged('update', storage, id);
|
|
307
|
+
if (saveError)
|
|
308
|
+
return saveError;
|
|
309
|
+
(0, auditLog_1.logAudit)('feedback_manage_update', [id], { fields: updatedFields }, 'feedback');
|
|
310
|
+
(0, logger_1.logInfo)('[feedback] Feedback entry updated', { id, fields: updatedFields });
|
|
61
311
|
return {
|
|
312
|
+
action: 'update',
|
|
62
313
|
success: true,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
314
|
+
entry,
|
|
315
|
+
message: 'Feedback entry updated successfully',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function deleteFeedback(params) {
|
|
319
|
+
const id = requireId('delete', params.id);
|
|
320
|
+
if (typeof id !== 'string')
|
|
321
|
+
return id;
|
|
322
|
+
const storage = loadStorageManaged('delete');
|
|
323
|
+
if ('success' in storage)
|
|
324
|
+
return storage;
|
|
325
|
+
const index = storage.entries.findIndex(item => item.id === id);
|
|
326
|
+
if (index === -1) {
|
|
327
|
+
return errorEnvelope('delete', 'not_found', `Feedback entry not found: ${id}`, {
|
|
328
|
+
id,
|
|
329
|
+
hint: 'Use feedback_manage with action=list to inspect available entries.',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
storage.entries.splice(index, 1);
|
|
333
|
+
const saveError = saveStorageManaged('delete', storage, id);
|
|
334
|
+
if (saveError)
|
|
335
|
+
return saveError;
|
|
336
|
+
(0, auditLog_1.logAudit)('feedback_manage_delete', [id], undefined, 'feedback');
|
|
337
|
+
(0, logger_1.logInfo)('[feedback] Feedback entry deleted', { id });
|
|
338
|
+
return {
|
|
339
|
+
action: 'delete',
|
|
340
|
+
success: true,
|
|
341
|
+
deleted: true,
|
|
342
|
+
id,
|
|
343
|
+
message: 'Feedback entry deleted successfully',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function feedbackStats(params) {
|
|
347
|
+
const storage = loadStorageManaged('stats');
|
|
348
|
+
if ('success' in storage)
|
|
349
|
+
return storage;
|
|
350
|
+
if (params.since !== undefined && Number.isNaN(Date.parse(params.since))) {
|
|
351
|
+
return errorEnvelope('stats', 'invalid_param', 'Invalid since. Must be an ISO-compatible date string.');
|
|
352
|
+
}
|
|
353
|
+
const entries = params.since ? storage.entries.filter(entry => entry.timestamp >= params.since) : storage.entries;
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
const day = 24 * 60 * 60 * 1000;
|
|
356
|
+
const stats = {
|
|
357
|
+
total: entries.length,
|
|
358
|
+
byType: {},
|
|
359
|
+
bySeverity: {},
|
|
360
|
+
byStatus: {},
|
|
361
|
+
recentActivity: {
|
|
362
|
+
last24h: 0,
|
|
363
|
+
last7d: 0,
|
|
364
|
+
last30d: 0,
|
|
365
|
+
},
|
|
66
366
|
};
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
stats.byType[entry.type] = (stats.byType[entry.type] || 0) + 1;
|
|
369
|
+
stats.bySeverity[entry.severity] = (stats.bySeverity[entry.severity] || 0) + 1;
|
|
370
|
+
stats.byStatus[entry.status] = (stats.byStatus[entry.status] || 0) + 1;
|
|
371
|
+
const age = now - new Date(entry.timestamp).getTime();
|
|
372
|
+
if (age <= day)
|
|
373
|
+
stats.recentActivity.last24h += 1;
|
|
374
|
+
if (age <= 7 * day)
|
|
375
|
+
stats.recentActivity.last7d += 1;
|
|
376
|
+
if (age <= 30 * day)
|
|
377
|
+
stats.recentActivity.last30d += 1;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
action: 'stats',
|
|
381
|
+
success: true,
|
|
382
|
+
stats,
|
|
383
|
+
storageInfo: {
|
|
384
|
+
lastUpdated: storage.lastUpdated,
|
|
385
|
+
version: storage.version,
|
|
386
|
+
maxEntries: (0, feedbackStorage_1.getMaxEntries)(),
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
(0, registry_1.registerHandler)('feedback_submit', (params) => {
|
|
391
|
+
try {
|
|
392
|
+
return submitFeedbackOrThrow(params);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
396
|
+
if (!message.startsWith('Missing required parameter') && !message.startsWith('Invalid ')) {
|
|
397
|
+
(0, logger_1.logError)('[feedback] feedback_submit storage failure', error instanceof Error ? error : { error: message });
|
|
398
|
+
(0, auditLog_1.logAudit)('feedback_submit_storage_error', undefined, {}, 'feedback');
|
|
399
|
+
throw new Error('Feedback submit failed due to a storage error. The error details are not exposed to clients.');
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
(0, registry_1.registerHandler)('feedback_manage', (params) => {
|
|
405
|
+
try {
|
|
406
|
+
const action = params?.action;
|
|
407
|
+
if (!action) {
|
|
408
|
+
return errorEnvelope('unknown', 'missing_required', 'Missing required parameter: action', {
|
|
409
|
+
hint: 'Use one of: submit, list, get, update, delete, stats.',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (!['submit', 'list', 'get', 'update', 'delete', 'stats'].includes(action)) {
|
|
413
|
+
return errorEnvelope(action, 'invalid_param', 'Invalid action. Must be one of: submit, list, get, update, delete, stats.');
|
|
414
|
+
}
|
|
415
|
+
switch (action) {
|
|
416
|
+
case 'submit': return submitFeedbackManaged(params);
|
|
417
|
+
case 'list': return listFeedback(params);
|
|
418
|
+
case 'get': return getFeedback(params);
|
|
419
|
+
case 'update': return updateFeedback(params);
|
|
420
|
+
case 'delete': return deleteFeedback(params);
|
|
421
|
+
case 'stats': return feedbackStats(params);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
const action = params?.action || 'unknown';
|
|
426
|
+
(0, logger_1.logError)('[feedback] feedback_manage unexpected failure', error instanceof Error ? error : { error: String(error), action });
|
|
427
|
+
(0, auditLog_1.logAudit)('feedback_manage_storage_error', undefined, { action }, 'feedback');
|
|
428
|
+
return errorEnvelope(action, 'storage_error', 'Feedback management failed due to a storage error. The error details are not exposed to clients.');
|
|
429
|
+
}
|
|
67
430
|
});
|