@nerviq/cli 1.10.0 → 1.11.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,218 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const PATH_ACTIONS = new Set(['read', 'write', 'edit', 'multiedit']);
5
+ const SECRET_PATH_RE = /(^|\/)(\.env(?:[^/]*)?|secrets?)(\/|$)/i;
6
+
7
+ function normalizeSlash(value) {
8
+ return String(value || '').replace(/\\/g, '/');
9
+ }
10
+
11
+ function stripWrappingQuotes(value) {
12
+ const trimmed = String(value || '').trim();
13
+ if (!trimmed) return '';
14
+ const first = trimmed[0];
15
+ const last = trimmed[trimmed.length - 1];
16
+ if ((first === '"' || first === "'") && first === last) {
17
+ return trimmed.slice(1, -1);
18
+ }
19
+ return trimmed;
20
+ }
21
+
22
+ function getProjectRoot(rootDir) {
23
+ try {
24
+ return fs.realpathSync.native(rootDir);
25
+ } catch {
26
+ return path.resolve(rootDir);
27
+ }
28
+ }
29
+
30
+ function splitPatternSegments(rawPattern, isAbsolute) {
31
+ const normalized = normalizeSlash(rawPattern);
32
+
33
+ if (/^[A-Za-z]:\//.test(normalized)) {
34
+ return normalized.slice(3).split('/').filter(Boolean);
35
+ }
36
+
37
+ if (isAbsolute && normalized.startsWith('/')) {
38
+ return normalized.slice(1).split('/').filter(Boolean);
39
+ }
40
+
41
+ return normalized.split('/').filter((segment) => segment && segment !== '.');
42
+ }
43
+
44
+ function hasGlob(segment) {
45
+ return /[*?[\]{}]/.test(segment);
46
+ }
47
+
48
+ function buildAbsolutePattern(rootDir, rawPattern) {
49
+ const normalized = stripWrappingQuotes(normalizeSlash(rawPattern).replace(/^file:\/\//i, ''));
50
+ if (!normalized) {
51
+ return {
52
+ absolutePattern: null,
53
+ normalizedInput: '',
54
+ isAbsolute: false,
55
+ traversalSegments: false,
56
+ };
57
+ }
58
+
59
+ const isAbsolute = /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('/');
60
+ const traversalSegments = normalized.split('/').some((segment) => segment === '..');
61
+ const segments = splitPatternSegments(normalized, isAbsolute);
62
+ let current = isAbsolute ? path.parse(path.resolve(normalized)).root : getProjectRoot(rootDir);
63
+
64
+ for (const segment of segments) {
65
+ const candidate = path.join(current, segment);
66
+ if (hasGlob(segment)) {
67
+ current = candidate;
68
+ continue;
69
+ }
70
+
71
+ try {
72
+ current = fs.realpathSync.native(candidate);
73
+ } catch {
74
+ current = candidate;
75
+ }
76
+ }
77
+
78
+ return {
79
+ absolutePattern: current,
80
+ normalizedInput: normalized,
81
+ isAbsolute,
82
+ traversalSegments,
83
+ };
84
+ }
85
+
86
+ function normalizePathPayload(rawPayload, rootDir) {
87
+ const {
88
+ absolutePattern,
89
+ normalizedInput,
90
+ isAbsolute,
91
+ traversalSegments,
92
+ } = buildAbsolutePattern(rootDir, rawPayload);
93
+
94
+ if (!absolutePattern) {
95
+ return {
96
+ normalizedPath: '',
97
+ repoRelativePath: '',
98
+ outsideRepo: false,
99
+ invalid: true,
100
+ isAbsolute,
101
+ traversalSegments,
102
+ };
103
+ }
104
+
105
+ const projectRoot = getProjectRoot(rootDir);
106
+ const relativePath = normalizeSlash(path.relative(projectRoot, absolutePattern));
107
+ const outsideRepo = relativePath === '..' || relativePath.startsWith('../') || /^[A-Za-z]:\//.test(relativePath);
108
+ const repoRelativePath = outsideRepo ? null : relativePath || '.';
109
+ const normalizedPath = outsideRepo
110
+ ? normalizeSlash(absolutePattern)
111
+ : `./${repoRelativePath}`;
112
+
113
+ return {
114
+ normalizedPath,
115
+ repoRelativePath,
116
+ outsideRepo,
117
+ invalid: traversalSegments && outsideRepo && !isAbsolute,
118
+ isAbsolute,
119
+ traversalSegments,
120
+ normalizedInput,
121
+ };
122
+ }
123
+
124
+ function normalizeCommandPayload(rawPayload) {
125
+ return stripWrappingQuotes(rawPayload).replace(/\s+/g, ' ').trim();
126
+ }
127
+
128
+ function normalizePermissionRule(rule, rootDir) {
129
+ if (typeof rule !== 'string' || !rule.trim()) return null;
130
+ const trimmed = rule.trim();
131
+ const match = trimmed.match(/^([A-Za-z]+)\((.*)\)$/);
132
+ if (!match) {
133
+ return {
134
+ raw: trimmed,
135
+ action: null,
136
+ payload: trimmed,
137
+ normalized: trimmed,
138
+ dedupeKey: trimmed.toLowerCase(),
139
+ kind: 'raw',
140
+ invalid: false,
141
+ outsideRepo: false,
142
+ protectsSecrets: false,
143
+ };
144
+ }
145
+
146
+ const action = match[1];
147
+ const payload = match[2].trim();
148
+ const actionKey = action.toLowerCase();
149
+
150
+ if (PATH_ACTIONS.has(actionKey)) {
151
+ const details = normalizePathPayload(payload, rootDir);
152
+ const dedupeKey = `${actionKey}:${details.normalizedPath.toLowerCase()}`;
153
+ return {
154
+ raw: trimmed,
155
+ action,
156
+ payload,
157
+ normalized: `${action}(${details.normalizedPath})`,
158
+ normalizedPath: details.normalizedPath,
159
+ repoRelativePath: details.repoRelativePath,
160
+ dedupeKey,
161
+ kind: 'path',
162
+ invalid: details.invalid,
163
+ outsideRepo: details.outsideRepo,
164
+ traversalSegments: details.traversalSegments,
165
+ isAbsolute: details.isAbsolute,
166
+ protectsSecrets: !details.outsideRepo && SECRET_PATH_RE.test(details.repoRelativePath || ''),
167
+ };
168
+ }
169
+
170
+ const normalizedPayload = normalizeCommandPayload(payload);
171
+ return {
172
+ raw: trimmed,
173
+ action,
174
+ payload,
175
+ normalized: `${action}(${normalizedPayload})`,
176
+ dedupeKey: `${actionKey}:${normalizedPayload.toLowerCase()}`,
177
+ kind: 'command',
178
+ invalid: false,
179
+ outsideRepo: false,
180
+ protectsSecrets: false,
181
+ };
182
+ }
183
+
184
+ function normalizePermissionRules(rules, rootDir) {
185
+ const seen = new Set();
186
+ const normalized = [];
187
+
188
+ for (const rule of Array.isArray(rules) ? rules : []) {
189
+ const entry = normalizePermissionRule(rule, rootDir);
190
+ if (!entry || entry.invalid) continue;
191
+ if (seen.has(entry.dedupeKey)) continue;
192
+ seen.add(entry.dedupeKey);
193
+ normalized.push(entry);
194
+ }
195
+
196
+ return normalized;
197
+ }
198
+
199
+ function collectClaudeDenyRules(ctx) {
200
+ const shared = ctx.jsonFile('.claude/settings.json');
201
+ const local = ctx.jsonFile('.claude/settings.local.json');
202
+ const denyRules = []
203
+ .concat(shared?.permissions?.deny || [])
204
+ .concat(local?.permissions?.deny || []);
205
+
206
+ return normalizePermissionRules(denyRules, ctx.dir);
207
+ }
208
+
209
+ function hasSecretDenyRule(rules) {
210
+ return (Array.isArray(rules) ? rules : []).some((rule) => rule && rule.protectsSecrets);
211
+ }
212
+
213
+ module.exports = {
214
+ collectClaudeDenyRules,
215
+ hasSecretDenyRule,
216
+ normalizePermissionRule,
217
+ normalizePermissionRules,
218
+ };
@@ -5,6 +5,15 @@ const EMBEDDED_SECRET_PATTERNS = [
5
5
  /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
6
6
  /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
7
7
  /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
8
+ /\bAZURE(?:_[A-Z0-9]+){0,4}_(?:API_)?KEY\s*[:=]\s*['"]?[A-Za-z0-9+/=_-]{20,}['"]?/g,
9
+ /\bDefaultEndpointsProtocol=https;AccountName=[^;\s]+;AccountKey=[A-Za-z0-9+/=]{20,};EndpointSuffix=core\.windows\.net\b/gi,
10
+ /\bEndpoint=sb:\/\/[^\s;]+;SharedAccessKeyName=[^;\s]+;SharedAccessKey=[A-Za-z0-9+/=]{20,}\b/gi,
11
+ /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis|rediss|amqp):\/\/[^:\s/]+:[^@\s]{4,}@[^/\s]+(?:\/[^\s'"]*)?/gi,
12
+ /\b(?:Server|Host|Data Source)\s*=\s*[^;\n]+;[^\n]*(?:Password|Pwd)\s*=\s*[^;\n]{4,}/gi,
13
+ /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
14
+ /-----BEGIN (?:OPENSSH|RSA|DSA|EC) PRIVATE KEY-----[\s\S]{40,}?-----END (?:OPENSSH|RSA|DSA|EC) PRIVATE KEY-----/g,
15
+ /-----BEGIN PRIVATE KEY-----[\s\S]{40,}?-----END PRIVATE KEY-----/g,
16
+ /"type"\s*:\s*"service_account"[\s\S]{0,1200}?"client_email"\s*:\s*"[^\"]+@[^"]*gserviceaccount\.com"[\s\S]{0,1200}?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[\s\S]{20,}?-----END PRIVATE KEY-----\\n?"/g,
8
17
  ];
9
18
 
10
19
  function containsEmbeddedSecret(text = '') {
package/src/server.js CHANGED
@@ -7,7 +7,7 @@ const { audit } = require('./audit');
7
7
  const { harmonyAudit } = require('./harmony/audit');
8
8
  const { getCatalog } = require('./public-api');
9
9
 
10
- const SUPPORTED_PLATFORMS = new Set([
10
+ const SUPPORTED_PLATFORMS = [
11
11
  'claude',
12
12
  'codex',
13
13
  'gemini',
@@ -16,7 +16,8 @@ const SUPPORTED_PLATFORMS = new Set([
16
16
  'windsurf',
17
17
  'aider',
18
18
  'opencode',
19
- ]);
19
+ ];
20
+ const SUPPORTED_PLATFORM_SET = new Set(SUPPORTED_PLATFORMS);
20
21
 
21
22
  function envelope(data) {
22
23
  return { data, meta: { version, timestamp: new Date().toISOString() } };
@@ -49,7 +50,7 @@ function resolveRequestDir(baseDir, rawDir) {
49
50
 
50
51
  function normalizePlatform(rawPlatform) {
51
52
  const platform = (rawPlatform || 'claude').toLowerCase();
52
- if (!SUPPORTED_PLATFORMS.has(platform)) {
53
+ if (!SUPPORTED_PLATFORM_SET.has(platform)) {
53
54
  const error = new Error(`Unsupported platform '${rawPlatform}'.`);
54
55
  error.statusCode = 400;
55
56
  throw error;
@@ -57,6 +58,392 @@ function normalizePlatform(rawPlatform) {
57
58
  return platform;
58
59
  }
59
60
 
61
+ function buildEnvelopeSchema(dataSchema) {
62
+ return {
63
+ type: 'object',
64
+ required: ['data', 'meta'],
65
+ properties: {
66
+ data: dataSchema,
67
+ meta: { $ref: '#/components/schemas/ResponseMeta' },
68
+ },
69
+ };
70
+ }
71
+
72
+ function buildServeOpenApiSpec(options = {}) {
73
+ const serverUrl = options.serverUrl || 'http://127.0.0.1:3000';
74
+ const catalogSize = options.catalogSize == null ? getCatalog().length : options.catalogSize;
75
+
76
+ return {
77
+ openapi: '3.1.0',
78
+ info: {
79
+ title: 'Nerviq Local API',
80
+ version,
81
+ summary: 'Zero-dependency local REST surface for audit, harmony, catalog, and health data.',
82
+ description: [
83
+ 'Nerviq exposes a local-first HTTP API through `nerviq serve`.',
84
+ 'Operational endpoints are GET-only, return JSON, and wrap successful payloads in `{ data, meta }` envelopes.',
85
+ 'The OpenAPI document itself is available at `/api/openapi.json`.',
86
+ ].join(' '),
87
+ },
88
+ servers: [
89
+ {
90
+ url: serverUrl,
91
+ description: 'Current Nerviq serve instance',
92
+ },
93
+ ],
94
+ tags: [
95
+ { name: 'system', description: 'Server health and contract discovery.' },
96
+ { name: 'audit', description: 'Repository audit and governance scoring.' },
97
+ { name: 'harmony', description: 'Cross-platform alignment and drift analysis.' },
98
+ { name: 'catalog', description: 'Unified public check catalog.' },
99
+ ],
100
+ paths: {
101
+ '/api/openapi.json': {
102
+ get: {
103
+ tags: ['system'],
104
+ operationId: 'getOpenApiSpec',
105
+ summary: 'Return the live OpenAPI contract for this Nerviq serve instance.',
106
+ responses: {
107
+ 200: {
108
+ description: 'OpenAPI 3.1 document for the active server surface.',
109
+ content: {
110
+ 'application/json': {
111
+ schema: {
112
+ type: 'object',
113
+ required: ['openapi', 'info', 'paths'],
114
+ properties: {
115
+ openapi: { type: 'string', example: '3.1.0' },
116
+ info: { type: 'object' },
117
+ servers: { type: 'array', items: { type: 'object' } },
118
+ paths: { type: 'object' },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ },
124
+ 405: {
125
+ $ref: '#/components/responses/MethodNotAllowed',
126
+ },
127
+ },
128
+ },
129
+ },
130
+ '/api/health': {
131
+ get: {
132
+ tags: ['system'],
133
+ operationId: 'getHealth',
134
+ summary: 'Check local server readiness and catalog size.',
135
+ responses: {
136
+ 200: {
137
+ description: 'Current server health envelope.',
138
+ content: {
139
+ 'application/json': {
140
+ schema: { $ref: '#/components/schemas/HealthEnvelope' },
141
+ },
142
+ },
143
+ },
144
+ 405: {
145
+ $ref: '#/components/responses/MethodNotAllowed',
146
+ },
147
+ 500: {
148
+ $ref: '#/components/responses/InternalError',
149
+ },
150
+ },
151
+ },
152
+ },
153
+ '/api/catalog': {
154
+ get: {
155
+ tags: ['catalog'],
156
+ operationId: 'getCatalog',
157
+ summary: 'Return the merged public check catalog.',
158
+ responses: {
159
+ 200: {
160
+ description: 'Full catalog envelope.',
161
+ content: {
162
+ 'application/json': {
163
+ schema: { $ref: '#/components/schemas/CatalogEnvelope' },
164
+ },
165
+ },
166
+ },
167
+ 405: {
168
+ $ref: '#/components/responses/MethodNotAllowed',
169
+ },
170
+ 500: {
171
+ $ref: '#/components/responses/InternalError',
172
+ },
173
+ },
174
+ },
175
+ },
176
+ '/api/audit': {
177
+ get: {
178
+ tags: ['audit'],
179
+ operationId: 'runAudit',
180
+ summary: 'Run a Nerviq audit for one directory and one platform.',
181
+ parameters: [
182
+ { $ref: '#/components/parameters/DirParam' },
183
+ { $ref: '#/components/parameters/PlatformParam' },
184
+ ],
185
+ responses: {
186
+ 200: {
187
+ description: 'Audit result envelope.',
188
+ content: {
189
+ 'application/json': {
190
+ schema: { $ref: '#/components/schemas/AuditEnvelope' },
191
+ },
192
+ },
193
+ },
194
+ 400: {
195
+ $ref: '#/components/responses/BadRequest',
196
+ },
197
+ 405: {
198
+ $ref: '#/components/responses/MethodNotAllowed',
199
+ },
200
+ 500: {
201
+ $ref: '#/components/responses/InternalError',
202
+ },
203
+ },
204
+ },
205
+ },
206
+ '/api/harmony': {
207
+ get: {
208
+ tags: ['harmony'],
209
+ operationId: 'runHarmonyAudit',
210
+ summary: 'Run cross-platform harmony audit and drift analysis.',
211
+ parameters: [
212
+ { $ref: '#/components/parameters/DirParam' },
213
+ ],
214
+ responses: {
215
+ 200: {
216
+ description: 'Harmony result envelope.',
217
+ content: {
218
+ 'application/json': {
219
+ schema: { $ref: '#/components/schemas/HarmonyEnvelope' },
220
+ },
221
+ },
222
+ },
223
+ 400: {
224
+ $ref: '#/components/responses/BadRequest',
225
+ },
226
+ 405: {
227
+ $ref: '#/components/responses/MethodNotAllowed',
228
+ },
229
+ 500: {
230
+ $ref: '#/components/responses/InternalError',
231
+ },
232
+ },
233
+ },
234
+ },
235
+ },
236
+ components: {
237
+ parameters: {
238
+ DirParam: {
239
+ name: 'dir',
240
+ in: 'query',
241
+ required: false,
242
+ description: 'Directory to audit. Relative paths resolve from the server base directory. Defaults to `.`.',
243
+ schema: {
244
+ type: 'string',
245
+ default: '.',
246
+ },
247
+ },
248
+ PlatformParam: {
249
+ name: 'platform',
250
+ in: 'query',
251
+ required: false,
252
+ description: 'Target platform to audit. Defaults to `claude` when omitted.',
253
+ schema: {
254
+ type: 'string',
255
+ enum: SUPPORTED_PLATFORMS,
256
+ default: 'claude',
257
+ },
258
+ },
259
+ },
260
+ responses: {
261
+ BadRequest: {
262
+ description: 'Validation error such as unsupported platform or missing directory.',
263
+ content: {
264
+ 'application/json': {
265
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
266
+ },
267
+ },
268
+ },
269
+ MethodNotAllowed: {
270
+ description: 'Only GET and OPTIONS are supported on this local API.',
271
+ content: {
272
+ 'application/json': {
273
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
274
+ },
275
+ },
276
+ },
277
+ InternalError: {
278
+ description: 'Unexpected server-side failure while building the response.',
279
+ content: {
280
+ 'application/json': {
281
+ schema: { $ref: '#/components/schemas/ErrorResponse' },
282
+ },
283
+ },
284
+ },
285
+ },
286
+ schemas: {
287
+ ErrorResponse: {
288
+ type: 'object',
289
+ required: ['error'],
290
+ properties: {
291
+ error: { type: 'string' },
292
+ },
293
+ },
294
+ ResponseMeta: {
295
+ type: 'object',
296
+ required: ['version', 'timestamp'],
297
+ properties: {
298
+ version: {
299
+ type: 'string',
300
+ example: version,
301
+ },
302
+ timestamp: {
303
+ type: 'string',
304
+ format: 'date-time',
305
+ },
306
+ },
307
+ },
308
+ HealthPayload: {
309
+ type: 'object',
310
+ required: ['status', 'version', 'checks'],
311
+ properties: {
312
+ status: { type: 'string', example: 'ok' },
313
+ version: { type: 'string', example: version },
314
+ checks: { type: 'integer', example: catalogSize },
315
+ },
316
+ },
317
+ CatalogEntry: {
318
+ type: 'object',
319
+ properties: {
320
+ platform: { type: 'string', example: 'claude' },
321
+ id: { type: 'string', example: 'CL-A01' },
322
+ key: { type: 'string', example: 'claudeMd' },
323
+ name: { type: 'string', example: 'CLAUDE.md project instructions' },
324
+ category: { type: 'string', example: 'memory' },
325
+ impact: { type: 'string', example: 'critical' },
326
+ fix: { type: 'string' },
327
+ sourceUrl: { type: 'string' },
328
+ confidence: { type: 'number', minimum: 0, maximum: 1 },
329
+ },
330
+ additionalProperties: true,
331
+ },
332
+ AuditStack: {
333
+ type: 'object',
334
+ properties: {
335
+ key: { type: 'string' },
336
+ label: { type: 'string' },
337
+ },
338
+ additionalProperties: true,
339
+ },
340
+ AuditAction: {
341
+ type: 'object',
342
+ properties: {
343
+ key: { type: 'string' },
344
+ name: { type: 'string' },
345
+ impact: { type: 'string' },
346
+ fix: { type: 'string' },
347
+ },
348
+ additionalProperties: true,
349
+ },
350
+ AuditCheck: {
351
+ type: 'object',
352
+ properties: {
353
+ key: { type: 'string' },
354
+ name: { type: 'string' },
355
+ impact: { type: 'string' },
356
+ category: { type: 'string' },
357
+ passed: {
358
+ oneOf: [
359
+ { type: 'boolean' },
360
+ { type: 'null' },
361
+ ],
362
+ },
363
+ },
364
+ additionalProperties: true,
365
+ },
366
+ AuditPayload: {
367
+ type: 'object',
368
+ properties: {
369
+ platform: { type: 'string', example: 'claude' },
370
+ platformLabel: { type: 'string', example: 'Claude Code' },
371
+ score: { type: 'integer', minimum: 0, maximum: 100 },
372
+ organicScore: { type: 'integer', minimum: 0, maximum: 100 },
373
+ earnedPoints: { type: 'integer' },
374
+ maxPoints: { type: 'integer' },
375
+ isScaffolded: { type: 'boolean' },
376
+ passed: { type: 'integer' },
377
+ failed: { type: 'integer' },
378
+ skipped: { type: 'integer' },
379
+ checkCount: { type: 'integer' },
380
+ stacks: {
381
+ type: 'array',
382
+ items: { $ref: '#/components/schemas/AuditStack' },
383
+ },
384
+ topNextActions: {
385
+ type: 'array',
386
+ items: { $ref: '#/components/schemas/AuditAction' },
387
+ },
388
+ results: {
389
+ type: 'array',
390
+ items: { $ref: '#/components/schemas/AuditCheck' },
391
+ },
392
+ },
393
+ additionalProperties: true,
394
+ },
395
+ HarmonyRecommendation: {
396
+ type: 'object',
397
+ properties: {
398
+ priority: { type: 'string' },
399
+ category: { type: 'string' },
400
+ message: { type: 'string' },
401
+ },
402
+ additionalProperties: true,
403
+ },
404
+ ActivePlatform: {
405
+ type: 'object',
406
+ properties: {
407
+ platform: { type: 'string' },
408
+ label: { type: 'string' },
409
+ },
410
+ additionalProperties: true,
411
+ },
412
+ HarmonyPayload: {
413
+ type: 'object',
414
+ properties: {
415
+ harmonyScore: { type: 'integer', minimum: 0, maximum: 100 },
416
+ platformScores: {
417
+ type: 'object',
418
+ additionalProperties: { type: 'integer' },
419
+ },
420
+ drift: {
421
+ type: 'object',
422
+ additionalProperties: true,
423
+ },
424
+ recommendations: {
425
+ type: 'array',
426
+ items: { $ref: '#/components/schemas/HarmonyRecommendation' },
427
+ },
428
+ activePlatforms: {
429
+ type: 'array',
430
+ items: { $ref: '#/components/schemas/ActivePlatform' },
431
+ },
432
+ },
433
+ additionalProperties: true,
434
+ },
435
+ HealthEnvelope: buildEnvelopeSchema({ $ref: '#/components/schemas/HealthPayload' }),
436
+ CatalogEnvelope: buildEnvelopeSchema({
437
+ type: 'array',
438
+ items: { $ref: '#/components/schemas/CatalogEntry' },
439
+ }),
440
+ AuditEnvelope: buildEnvelopeSchema({ $ref: '#/components/schemas/AuditPayload' }),
441
+ HarmonyEnvelope: buildEnvelopeSchema({ $ref: '#/components/schemas/HarmonyPayload' }),
442
+ },
443
+ },
444
+ };
445
+ }
446
+
60
447
  function createServer(options = {}) {
61
448
  const baseDir = path.resolve(options.baseDir || process.cwd());
62
449
 
@@ -74,6 +461,13 @@ function createServer(options = {}) {
74
461
  }
75
462
 
76
463
  try {
464
+ if (requestUrl.pathname === '/api/openapi.json') {
465
+ sendJson(res, 200, buildServeOpenApiSpec({
466
+ serverUrl: `http://${req.headers.host || '127.0.0.1:3000'}`,
467
+ }));
468
+ return;
469
+ }
470
+
77
471
  if (requestUrl.pathname === '/api/health') {
78
472
  sendJson(res, 200, envelope({
79
473
  status: 'ok',
@@ -127,6 +521,7 @@ function startServer(options = {}) {
127
521
  }
128
522
 
129
523
  module.exports = {
524
+ buildServeOpenApiSpec,
130
525
  createServer,
131
526
  startServer,
132
527
  };
package/src/setup.js CHANGED
@@ -809,8 +809,8 @@ process.stdin.on('end', () => {
809
809
  // Check command (for Bash)
810
810
  const cmd = (data.tool_input && data.tool_input.command) || '';
811
811
 
812
- const secretPattern = /\\.env($|\\.)|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i;
813
- const bashSecretPattern = /\\bcat\\s+\\.env|\\bless\\s+\\.env|\\bhead\\s+\\.env|\\btail\\s+\\.env|\\bgrep\\b.*\\.env|\\bcp\\s+\\.env|\\bmv\\s+\\.env|\\bbase64\\s+\\.env|\\bxxd\\s+\\.env|secrets\\/|credentials|\\.pem\\b|\\.key\\b/i;
812
+ const secretPattern = /\\.env($|\\.)|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$|\\.(?:p12|pfx)$|(?:^|[\\/\\\\])\\.ssh(?:[\\/\\\\]|$)|(?:^|[\\/\\\\])id_(?:rsa|dsa|ecdsa|ed25519)$|\\.tfvars(?:\\.json)?$|values[-_.]?secret\\.ya?ml$|service-?account[^\\/\\\\]*\\.json$|gcp[^\\/\\\\]*credentials?[^\\/\\\\]*\\.json$|sa-key[^\\/\\\\]*\\.json$/i;
813
+ const bashSecretPattern = /\\bcat\\s+\\.env|\\bless\\s+\\.env|\\bhead\\s+\\.env|\\btail\\s+\\.env|\\bgrep\\b.*\\.env|\\bcp\\s+\\.env|\\bmv\\s+\\.env|\\bbase64\\s+\\.env|\\bxxd\\s+\\.env|secrets[\\/\\\\]|credentials|\\.pem\\b|\\.key\\b|\\.(?:p12|pfx)\\b|\\.ssh[\\/\\\\]|id_(?:rsa|dsa|ecdsa|ed25519)\\b|\\.tfvars(?:\\.json)?\\b|values[-_.]?secret\\.ya?ml\\b|service-?account[^\\s]*\\.json\\b|gcp[^\\s]*credentials?[^\\s]*\\.json\\b|sa-key[^\\s]*\\.json\\b/i;
814
814
 
815
815
  if (secretPattern.test(fp) || bashSecretPattern.test(cmd)) {
816
816
  console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));