@thotischner/observability-mcp 1.7.0 → 1.8.1
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/config/products.yaml.example +48 -0
- package/dist/audit/log.d.ts +99 -0
- package/dist/audit/log.js +180 -0
- package/dist/audit/log.test.d.ts +1 -0
- package/dist/audit/log.test.js +147 -0
- package/dist/audit/middleware.d.ts +20 -0
- package/dist/audit/middleware.js +50 -0
- package/dist/auth/credentials.d.ts +18 -0
- package/dist/auth/credentials.js +26 -1
- package/dist/auth/credentials.test.js +26 -1
- package/dist/auth/local-users.d.ts +62 -0
- package/dist/auth/local-users.js +143 -0
- package/dist/auth/local-users.test.d.ts +1 -0
- package/dist/auth/local-users.test.js +80 -0
- package/dist/auth/middleware.d.ts +48 -0
- package/dist/auth/middleware.js +65 -0
- package/dist/auth/middleware.test.d.ts +1 -0
- package/dist/auth/middleware.test.js +90 -0
- package/dist/auth/oidc/client.d.ts +73 -0
- package/dist/auth/oidc/client.js +104 -0
- package/dist/auth/oidc/client.test.d.ts +1 -0
- package/dist/auth/oidc/client.test.js +121 -0
- package/dist/auth/oidc/discovery.d.ts +38 -0
- package/dist/auth/oidc/discovery.js +48 -0
- package/dist/auth/oidc/discovery.test.d.ts +1 -0
- package/dist/auth/oidc/discovery.test.js +68 -0
- package/dist/auth/oidc/endpoints.d.ts +20 -0
- package/dist/auth/oidc/endpoints.js +124 -0
- package/dist/auth/oidc/endpoints.test.d.ts +7 -0
- package/dist/auth/oidc/endpoints.test.js +304 -0
- package/dist/auth/oidc/flow-cookie.d.ts +57 -0
- package/dist/auth/oidc/flow-cookie.js +142 -0
- package/dist/auth/oidc/flow-cookie.test.d.ts +1 -0
- package/dist/auth/oidc/flow-cookie.test.js +0 -0
- package/dist/auth/oidc/index.d.ts +7 -0
- package/dist/auth/oidc/index.js +6 -0
- package/dist/auth/oidc/jwks.d.ts +36 -0
- package/dist/auth/oidc/jwks.js +69 -0
- package/dist/auth/oidc/jwks.test.d.ts +1 -0
- package/dist/auth/oidc/jwks.test.js +65 -0
- package/dist/auth/oidc/jwt.d.ts +62 -0
- package/dist/auth/oidc/jwt.js +113 -0
- package/dist/auth/oidc/jwt.test.d.ts +1 -0
- package/dist/auth/oidc/jwt.test.js +141 -0
- package/dist/auth/oidc/pkce.d.ts +19 -0
- package/dist/auth/oidc/pkce.js +43 -0
- package/dist/auth/oidc/pkce.test.d.ts +1 -0
- package/dist/auth/oidc/pkce.test.js +55 -0
- package/dist/auth/oidc/runtime.d.ts +63 -0
- package/dist/auth/oidc/runtime.js +129 -0
- package/dist/auth/oidc/runtime.test.d.ts +1 -0
- package/dist/auth/oidc/runtime.test.js +180 -0
- package/dist/auth/policy/engine.d.ts +48 -0
- package/dist/auth/policy/engine.js +73 -0
- package/dist/auth/policy/engine.test.d.ts +1 -0
- package/dist/auth/policy/engine.test.js +98 -0
- package/dist/auth/policy/loader.d.ts +35 -0
- package/dist/auth/policy/loader.js +100 -0
- package/dist/auth/policy/opa.d.ts +69 -0
- package/dist/auth/policy/opa.js +162 -0
- package/dist/auth/policy/opa.test.d.ts +1 -0
- package/dist/auth/policy/opa.test.js +158 -0
- package/dist/auth/rbac.d.ts +40 -0
- package/dist/auth/rbac.js +120 -0
- package/dist/auth/rbac.test.d.ts +1 -0
- package/dist/auth/rbac.test.js +121 -0
- package/dist/auth/session.d.ts +66 -0
- package/dist/auth/session.js +146 -0
- package/dist/auth/session.test.d.ts +1 -0
- package/dist/auth/session.test.js +90 -0
- package/dist/catalog/loader.d.ts +67 -0
- package/dist/catalog/loader.js +122 -0
- package/dist/catalog/loader.test.d.ts +1 -0
- package/dist/catalog/loader.test.js +108 -0
- package/dist/connectors/kubernetes.d.ts +1 -0
- package/dist/connectors/kubernetes.js +12 -2
- package/dist/connectors/topology-vocabulary.d.ts +41 -0
- package/dist/connectors/topology-vocabulary.js +120 -0
- package/dist/connectors/topology-vocabulary.test.d.ts +1 -0
- package/dist/connectors/topology-vocabulary.test.js +63 -0
- package/dist/context.d.ts +13 -1
- package/dist/context.js +5 -1
- package/dist/index.js +1012 -29
- package/dist/net/egress-policy.js +2 -0
- package/dist/openapi.js +440 -0
- package/dist/openapi.test.d.ts +1 -0
- package/dist/openapi.test.js +64 -0
- package/dist/policy/redact.d.ts +44 -0
- package/dist/policy/redact.js +144 -0
- package/dist/policy/redact.test.d.ts +1 -0
- package/dist/policy/redact.test.js +172 -0
- package/dist/products/loader.d.ts +84 -0
- package/dist/products/loader.js +216 -0
- package/dist/products/loader.test.d.ts +1 -0
- package/dist/products/loader.test.js +168 -0
- package/dist/quota/limiter.d.ts +72 -0
- package/dist/quota/limiter.js +105 -0
- package/dist/quota/limiter.test.d.ts +1 -0
- package/dist/quota/limiter.test.js +119 -0
- package/dist/quota/token-budget.d.ts +119 -0
- package/dist/quota/token-budget.js +297 -0
- package/dist/quota/token-budget.test.d.ts +1 -0
- package/dist/quota/token-budget.test.js +215 -0
- package/dist/tenancy/context.d.ts +45 -0
- package/dist/tenancy/context.js +97 -0
- package/dist/tenancy/context.test.d.ts +1 -0
- package/dist/tenancy/context.test.js +72 -0
- package/dist/tenancy/migration.test.d.ts +7 -0
- package/dist/tenancy/migration.test.js +75 -0
- package/dist/ui/index.html +1454 -88
- package/package.json +20 -3
|
@@ -25,6 +25,8 @@ export const EGRESS_ALLOWLIST = [
|
|
|
25
25
|
{ prefix: "connectors/", reason: "connectors query operator-configured source backends" },
|
|
26
26
|
{ prefix: "cli/index.ts", reason: "CLI fetches a source location the operator passed explicitly" },
|
|
27
27
|
{ prefix: "index.ts", reason: "connector-hub plugin install of an operator/registry-requested tarball URL" },
|
|
28
|
+
{ prefix: "auth/oidc/", reason: "OIDC client calls the operator-configured OMCP_OIDC_ISSUER for discovery, JWKS, and code-exchange" },
|
|
29
|
+
{ prefix: "auth/policy/", reason: "OpaPolicyEngine queries the operator-configured OMCP_OPA_URL on every RBAC decision" },
|
|
28
30
|
];
|
|
29
31
|
/**
|
|
30
32
|
* Hard-blocked analytics/telemetry SDKs — matches an *import/require of the
|
package/dist/openapi.js
CHANGED
|
@@ -51,6 +51,10 @@ export function buildOpenApiSpec(version) {
|
|
|
51
51
|
{ name: "settings", description: "Runtime server configuration." },
|
|
52
52
|
{ name: "metrics-config", description: "Per-source metric definitions." },
|
|
53
53
|
{ name: "self", description: "Server liveness and Prometheus metrics." },
|
|
54
|
+
{ name: "auth", description: "Management-plane session login / logout / identity." },
|
|
55
|
+
{ name: "audit", description: "Tamper-evident audit log of /api/* mutations." },
|
|
56
|
+
{ name: "usage", description: "Per-identity rate-limit snapshot for /mcp callers." },
|
|
57
|
+
{ name: "catalog", description: "Operator-curated service catalog." },
|
|
54
58
|
],
|
|
55
59
|
paths: {
|
|
56
60
|
"/api/sources": {
|
|
@@ -180,6 +184,442 @@ export function buildOpenApiSpec(version) {
|
|
|
180
184
|
responses: { "200": { description: "OpenAPI 3.1 document." } },
|
|
181
185
|
},
|
|
182
186
|
},
|
|
187
|
+
"/api/info": {
|
|
188
|
+
get: {
|
|
189
|
+
tags: ["self"],
|
|
190
|
+
summary: "Server identity, build info, plugin list and governance posture.",
|
|
191
|
+
description: "Anonymous-readable snapshot for external dashboards and discovery probes. " +
|
|
192
|
+
"The `governance` block surfaces the active management-plane configuration " +
|
|
193
|
+
"as booleans / rate-limit number only — no file paths, no session secret, " +
|
|
194
|
+
"no user counts. Useful for alerting on \"this deployment silently reverted " +
|
|
195
|
+
"to anonymous mode\" or \"redaction is off in prod\".",
|
|
196
|
+
responses: {
|
|
197
|
+
"200": {
|
|
198
|
+
description: "Server info + governance posture.",
|
|
199
|
+
content: {
|
|
200
|
+
"application/json": {
|
|
201
|
+
schema: {
|
|
202
|
+
type: "object",
|
|
203
|
+
properties: {
|
|
204
|
+
name: { type: "string" },
|
|
205
|
+
version: { type: "string" },
|
|
206
|
+
mcpProtocolVersion: { type: "string" },
|
|
207
|
+
build: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
commit: { type: ["string", "null"] },
|
|
211
|
+
date: { type: ["string", "null"] },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
runtime: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
node: { type: "string" },
|
|
218
|
+
platform: { type: "string" },
|
|
219
|
+
arch: { type: "string" },
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
governance: {
|
|
223
|
+
type: "object",
|
|
224
|
+
description: "Active management-plane posture; booleans + rate-limit number only.",
|
|
225
|
+
properties: {
|
|
226
|
+
authMode: { type: "string", enum: ["anonymous", "basic", "oidc"] },
|
|
227
|
+
authSecretEphemeral: {
|
|
228
|
+
type: "boolean",
|
|
229
|
+
description: "True when OMCP_SESSION_SECRET is unset and the server minted an in-memory secret at boot. Sessions don't survive a restart.",
|
|
230
|
+
},
|
|
231
|
+
oidcIssuer: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Active OIDC issuer URL. Empty string when authMode is not 'oidc'. Never includes the client_secret.",
|
|
234
|
+
},
|
|
235
|
+
auditPersisted: {
|
|
236
|
+
type: "boolean",
|
|
237
|
+
description: "True when OMCP_MGMT_AUDIT_FILE is set; false means the audit log is the in-memory 500-entry ring.",
|
|
238
|
+
},
|
|
239
|
+
catalogConfigured: { type: "boolean" },
|
|
240
|
+
redaction: { type: "boolean" },
|
|
241
|
+
trustProxy: { type: "boolean" },
|
|
242
|
+
toolRatePerMin: { type: "integer" },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
plugins: {
|
|
246
|
+
type: "array",
|
|
247
|
+
items: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
name: { type: "string" },
|
|
251
|
+
source: { type: "string" },
|
|
252
|
+
version: { type: ["string", "null"] },
|
|
253
|
+
signalTypes: { type: ["array", "null"], items: { type: "string" } },
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
"/api/me": {
|
|
266
|
+
get: {
|
|
267
|
+
tags: ["auth"],
|
|
268
|
+
summary: "Current identity, mode and granted permissions.",
|
|
269
|
+
responses: {
|
|
270
|
+
"200": {
|
|
271
|
+
description: "Identity snapshot. `authenticated: false` in anonymous mode or when the session cookie is missing/invalid.",
|
|
272
|
+
content: {
|
|
273
|
+
"application/json": {
|
|
274
|
+
schema: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
authenticated: { type: "boolean" },
|
|
278
|
+
mode: { type: "string", enum: ["anonymous", "basic", "oidc"] },
|
|
279
|
+
user: {
|
|
280
|
+
type: "object",
|
|
281
|
+
properties: {
|
|
282
|
+
sub: { type: "string" },
|
|
283
|
+
name: { type: "string" },
|
|
284
|
+
email: { type: "string", description: "Present when the IdP supplied a verified email claim (OIDC mode)." },
|
|
285
|
+
roles: { type: "array", items: { type: "string" } },
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
permissions: {
|
|
289
|
+
type: "array",
|
|
290
|
+
items: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
293
|
+
resource: { type: "string" },
|
|
294
|
+
action: { type: "string", enum: ["read", "write", "delete", "bypass"] },
|
|
295
|
+
// Resource enum is unconstrained at the OpenAPI level so
|
|
296
|
+
// custom policies loaded via OMCP_RBAC_POLICY_FILE that
|
|
297
|
+
// (correctly) include all built-in resources still validate.
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
exp: { type: "integer", description: "Cookie expiry (seconds since epoch)." },
|
|
302
|
+
idpIssuer: {
|
|
303
|
+
type: "string",
|
|
304
|
+
description: "Active OIDC issuer URL. Present only when mode === \"oidc\". Useful for UI badges or IdP-side profile links.",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
"/api/auth/login": {
|
|
315
|
+
post: {
|
|
316
|
+
tags: ["auth"],
|
|
317
|
+
summary: "Sign in (basic mode only).",
|
|
318
|
+
requestBody: {
|
|
319
|
+
required: true,
|
|
320
|
+
content: {
|
|
321
|
+
"application/json": {
|
|
322
|
+
schema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
required: ["username", "password"],
|
|
325
|
+
properties: {
|
|
326
|
+
username: { type: "string" },
|
|
327
|
+
password: { type: "string", format: "password" },
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
responses: {
|
|
334
|
+
"200": { description: "Set-Cookie carries the signed session." },
|
|
335
|
+
"400": { description: "Missing username or password." },
|
|
336
|
+
"401": { description: "Invalid credentials." },
|
|
337
|
+
"429": { description: "Too many login attempts." },
|
|
338
|
+
"503": { description: "Server is in anonymous mode and does not accept logins." },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
"/api/auth/logout": {
|
|
343
|
+
post: {
|
|
344
|
+
tags: ["auth"],
|
|
345
|
+
summary: "Sign out — clears the session cookie.",
|
|
346
|
+
responses: { "204": { description: "Cookie cleared." } },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
"/api/auth/oidc/login": {
|
|
350
|
+
get: {
|
|
351
|
+
tags: ["auth"],
|
|
352
|
+
summary: "Redirect to the configured OIDC identity provider's authorization endpoint.",
|
|
353
|
+
description: "Mounted only when OMCP_AUTH=oidc. Mints a short-lived flow cookie carrying state + nonce + PKCE code-verifier + return_to, then 302s the browser to the IdP.",
|
|
354
|
+
parameters: [
|
|
355
|
+
{ name: "return_to", in: "query", required: false, schema: { type: "string" }, description: "Same-origin path to redirect to after a successful callback. Absolute URLs or scheme-relative paths are rejected." },
|
|
356
|
+
],
|
|
357
|
+
responses: {
|
|
358
|
+
"302": { description: "Redirect to the IdP authorize_endpoint." },
|
|
359
|
+
"502": { description: "OIDC discovery failed (IdP unreachable / misconfigured)." },
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
"/api/auth/oidc/callback": {
|
|
364
|
+
get: {
|
|
365
|
+
tags: ["auth"],
|
|
366
|
+
summary: "OIDC code-flow callback — exchanges code for an id_token and mints an OMCP session cookie.",
|
|
367
|
+
description: "Verifies the state cookie, the IdP-returned state, the id_token signature (RS256/ES256), iss/aud/exp/nbf/nonce claims, then resolves OMCP roles from OMCP_OIDC_ROLES_CLAIM via OMCP_OIDC_ROLE_MAP and 302s to the cookie's return_to.",
|
|
368
|
+
parameters: [
|
|
369
|
+
{ name: "code", in: "query", schema: { type: "string" } },
|
|
370
|
+
{ name: "state", in: "query", schema: { type: "string" } },
|
|
371
|
+
{ name: "error", in: "query", required: false, schema: { type: "string" } },
|
|
372
|
+
],
|
|
373
|
+
responses: {
|
|
374
|
+
"302": { description: "Authentication succeeded; session cookie set and redirected to return_to." },
|
|
375
|
+
"400": { description: "Bad / expired / missing flow cookie, IdP error parameter present, or token exchange / verification failed." },
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
"/api/auth/oidc/logout": {
|
|
380
|
+
post: {
|
|
381
|
+
tags: ["auth"],
|
|
382
|
+
summary: "Sign out of the OMCP session. Does not perform RP-initiated logout against the IdP.",
|
|
383
|
+
description: "Clears the OMCP session cookie. To force an IdP-side sign-out, the UI should subsequently navigate to OMCP_OIDC_LOGOUT_REDIRECT (typically the IdP's end_session_endpoint).",
|
|
384
|
+
responses: { "204": { description: "Cookie cleared." } },
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
"/api/audit": {
|
|
388
|
+
get: {
|
|
389
|
+
tags: ["audit"],
|
|
390
|
+
summary: "Recent management-plane audit entries (most recent first).",
|
|
391
|
+
parameters: [
|
|
392
|
+
{ name: "from", in: "query", schema: { type: "string", format: "date-time" } },
|
|
393
|
+
{ name: "to", in: "query", schema: { type: "string", format: "date-time" } },
|
|
394
|
+
{ name: "actor", in: "query", schema: { type: "string" } },
|
|
395
|
+
{ name: "action", in: "query", schema: { type: "string" } },
|
|
396
|
+
{ name: "tenant", in: "query", schema: { type: "string" }, description: "Tenant scope. Non-admins are silently scoped to their own tenant; admins can pass any value (omit → all tenants)." },
|
|
397
|
+
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1, maximum: 500, default: 100 } },
|
|
398
|
+
],
|
|
399
|
+
responses: {
|
|
400
|
+
"200": {
|
|
401
|
+
description: "Audit feed plus the chain's tip hash.",
|
|
402
|
+
content: {
|
|
403
|
+
"application/json": {
|
|
404
|
+
schema: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
entries: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
408
|
+
tipHash: { type: "string" },
|
|
409
|
+
persisted: { type: "boolean" },
|
|
410
|
+
scopedTo: { type: ["string", "null"], description: "Tenant name this view is scoped to; null = all tenants (admin)." },
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
"401": { description: "Unauthenticated (basic mode)." },
|
|
417
|
+
"403": { description: "Missing audit:read permission." },
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
"/api/usage": {
|
|
422
|
+
get: {
|
|
423
|
+
tags: ["usage"],
|
|
424
|
+
summary: "Per-identity windowed call count for /mcp callers.",
|
|
425
|
+
parameters: [
|
|
426
|
+
{ name: "actor", in: "query", schema: { type: "string" }, description: "Narrow to a single identity." },
|
|
427
|
+
{ name: "tenant", in: "query", schema: { type: "string" }, description: "Tenant scope. Non-admins silently scoped to their own; admins can pick any (omit → all)." },
|
|
428
|
+
],
|
|
429
|
+
responses: {
|
|
430
|
+
"200": {
|
|
431
|
+
description: "Usage snapshot. Anonymous /mcp traffic does not appear here.",
|
|
432
|
+
content: {
|
|
433
|
+
"application/json": {
|
|
434
|
+
schema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
identities: {
|
|
438
|
+
type: "array",
|
|
439
|
+
items: {
|
|
440
|
+
type: "object",
|
|
441
|
+
properties: {
|
|
442
|
+
actor: { type: "string" },
|
|
443
|
+
tenant: { type: "string", description: "Tenant the identity belongs to. 'default' when single-tenant." },
|
|
444
|
+
count: { type: "integer" },
|
|
445
|
+
limit: { type: "integer" },
|
|
446
|
+
windowMs: { type: "integer" },
|
|
447
|
+
tokens: {
|
|
448
|
+
type: "object",
|
|
449
|
+
description: "Per-identity 24h-rolling token usage. `limit: 0` means uncapped (OMCP_TOOL_DAILY_TOKENS unset).",
|
|
450
|
+
properties: {
|
|
451
|
+
used: { type: "integer" },
|
|
452
|
+
limit: { type: "integer" },
|
|
453
|
+
windowMs: { type: "integer" },
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
defaultLimit: { type: "integer" },
|
|
460
|
+
windowMs: { type: "integer" },
|
|
461
|
+
tokens: {
|
|
462
|
+
type: "object",
|
|
463
|
+
description: "Process-wide defaults for the token-budget tracker.",
|
|
464
|
+
properties: {
|
|
465
|
+
defaultLimit: { type: "integer" },
|
|
466
|
+
windowMs: { type: "integer" },
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
"403": { description: "Missing audit:read permission." },
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
"/api/policy": {
|
|
479
|
+
get: {
|
|
480
|
+
tags: ["auth"],
|
|
481
|
+
summary: "Read-only view of the active RBAC policy (admin-only). Dry-run probe with ?resource=&action=&roles=.",
|
|
482
|
+
parameters: [
|
|
483
|
+
{ name: "roles", in: "query", required: false, schema: { type: "string" }, description: "Comma-separated role names to probe. Defaults to none (treated as anonymous → always denied)." },
|
|
484
|
+
{ name: "resource", in: "query", required: false, schema: { type: "string" }, description: "Resource to probe. Pair with `action` to enter dry-run mode." },
|
|
485
|
+
{ name: "action", in: "query", required: false, schema: { type: "string" }, description: "Action to probe. Pair with `resource` to enter dry-run mode." },
|
|
486
|
+
],
|
|
487
|
+
responses: {
|
|
488
|
+
"200": {
|
|
489
|
+
description: "Either the full policy map (no probe params) or a dry-run decision (with `resource` + `action`).",
|
|
490
|
+
content: {
|
|
491
|
+
"application/json": {
|
|
492
|
+
schema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
engine: { type: "string", description: "Identifier of the active engine: 'builtin', 'file:<path>', 'opa:<url>'." },
|
|
496
|
+
policy: { type: "object", additionalProperties: true },
|
|
497
|
+
roles: { type: "array", items: { type: "string" } },
|
|
498
|
+
note: { type: "string" },
|
|
499
|
+
dryRun: {
|
|
500
|
+
type: "object",
|
|
501
|
+
properties: {
|
|
502
|
+
roles: { type: "array", items: { type: "string" } },
|
|
503
|
+
resource: { type: "string" },
|
|
504
|
+
action: { type: "string" },
|
|
505
|
+
allowed: { type: "boolean" },
|
|
506
|
+
reason: { type: "string" },
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
"403": { description: "Missing users:delete permission (admin-only)." },
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
"/api/products": {
|
|
519
|
+
get: {
|
|
520
|
+
tags: ["products"],
|
|
521
|
+
summary: "Loaded MCP Products catalogue (curated tool bundles for agents).",
|
|
522
|
+
parameters: [
|
|
523
|
+
{ name: "tenant", in: "query", schema: { type: "string" }, description: "Tenant scope. Non-admins silently scoped to their own; admins can pick any (omit → all)." },
|
|
524
|
+
],
|
|
525
|
+
responses: {
|
|
526
|
+
"200": {
|
|
527
|
+
description: "Products list scoped to caller's tenant; admins see staging entries too.",
|
|
528
|
+
content: {
|
|
529
|
+
"application/json": {
|
|
530
|
+
schema: {
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {
|
|
533
|
+
products: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
534
|
+
configured: { type: "boolean" },
|
|
535
|
+
scopedTo: { type: ["string", "null"] },
|
|
536
|
+
includesStaging: { type: "boolean" },
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
"403": { description: "Missing products:read permission." },
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
"/api/products/{id}": {
|
|
547
|
+
get: {
|
|
548
|
+
tags: ["products"],
|
|
549
|
+
summary: "Single product by id (404 on cross-tenant or staging probe by non-admin).",
|
|
550
|
+
parameters: [
|
|
551
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
552
|
+
],
|
|
553
|
+
responses: {
|
|
554
|
+
"200": { description: "The product entry.", content: { "application/json": { schema: { type: "object", additionalProperties: true } } } },
|
|
555
|
+
"404": { description: "Not found (or hidden by tenant / staging scope)." },
|
|
556
|
+
"403": { description: "Missing products:read permission." },
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
put: {
|
|
560
|
+
tags: ["products"],
|
|
561
|
+
summary: "Upsert a product (admin + operator). Body must match the OMCP_PRODUCTS_FILE entry shape.",
|
|
562
|
+
parameters: [
|
|
563
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
564
|
+
],
|
|
565
|
+
requestBody: {
|
|
566
|
+
required: true,
|
|
567
|
+
content: { "application/json": { schema: { type: "object", additionalProperties: true } } },
|
|
568
|
+
},
|
|
569
|
+
responses: {
|
|
570
|
+
"200": {
|
|
571
|
+
description: "Upsert succeeded; returns the validated product + a persisted flag.",
|
|
572
|
+
content: { "application/json": { schema: { type: "object", properties: {
|
|
573
|
+
product: { type: "object", additionalProperties: true },
|
|
574
|
+
persisted: { type: "boolean", description: "True when OMCP_PRODUCTS_FILE was set and the file was rewritten." },
|
|
575
|
+
} } } },
|
|
576
|
+
},
|
|
577
|
+
"400": { description: "Body shape invalid (validateProduct rejected — typo, unknown key, wrong type, ...)." },
|
|
578
|
+
"403": { description: "Missing products:write permission, or non-admin attempting to write into another tenant." },
|
|
579
|
+
"404": { description: "Existing product belongs to a different tenant (non-admin)." },
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
delete: {
|
|
583
|
+
tags: ["products"],
|
|
584
|
+
summary: "Delete a product by id (admin only).",
|
|
585
|
+
parameters: [
|
|
586
|
+
{ name: "id", in: "path", required: true, schema: { type: "string" } },
|
|
587
|
+
],
|
|
588
|
+
responses: {
|
|
589
|
+
"204": { description: "Deleted." },
|
|
590
|
+
"403": { description: "Missing products:delete permission." },
|
|
591
|
+
"404": { description: "Not found (or hidden by tenant scope)." },
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
"/api/catalog": {
|
|
596
|
+
get: {
|
|
597
|
+
tags: ["catalog"],
|
|
598
|
+
summary: "Loaded service catalog (owner / tier / on-call / SLO).",
|
|
599
|
+
parameters: [
|
|
600
|
+
{ name: "tenant", in: "query", schema: { type: "string" }, description: "Tenant scope. Non-admins silently scoped to their own; admins can pick any (omit → all)." },
|
|
601
|
+
],
|
|
602
|
+
responses: {
|
|
603
|
+
"200": {
|
|
604
|
+
description: "Catalog map keyed by service name.",
|
|
605
|
+
content: {
|
|
606
|
+
"application/json": {
|
|
607
|
+
schema: {
|
|
608
|
+
type: "object",
|
|
609
|
+
properties: {
|
|
610
|
+
services: { type: "object", additionalProperties: true },
|
|
611
|
+
count: { type: "integer" },
|
|
612
|
+
configured: { type: "boolean", description: "true when OMCP_SERVICE_CATALOG_FILE is set." },
|
|
613
|
+
scopedTo: { type: ["string", "null"], description: "Tenant name this view is scoped to; null = all tenants (admin)." },
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
"403": { description: "Missing catalog:read permission." },
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
183
623
|
},
|
|
184
624
|
};
|
|
185
625
|
return doc;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildOpenApiSpec } from "./openapi.js";
|
|
4
|
+
test("openapi — every user-visible /api path is documented", () => {
|
|
5
|
+
// If a future PR adds an endpoint here, also document it in
|
|
6
|
+
// openapi.ts. The list intentionally excludes admin-only routes the
|
|
7
|
+
// spec deliberately keeps internal — add to that allow-list rather
|
|
8
|
+
// than mass-adding routes to the spec.
|
|
9
|
+
const documentedRoutes = [
|
|
10
|
+
"/api/health",
|
|
11
|
+
"/api/services",
|
|
12
|
+
"/api/sources",
|
|
13
|
+
"/api/sources/{name}",
|
|
14
|
+
"/api/sources/{name}/metrics",
|
|
15
|
+
"/api/source-types",
|
|
16
|
+
"/api/settings",
|
|
17
|
+
"/api/health-thresholds",
|
|
18
|
+
"/api/me",
|
|
19
|
+
"/api/auth/login",
|
|
20
|
+
"/api/auth/logout",
|
|
21
|
+
"/api/auth/oidc/login",
|
|
22
|
+
"/api/auth/oidc/callback",
|
|
23
|
+
"/api/auth/oidc/logout",
|
|
24
|
+
"/api/audit",
|
|
25
|
+
"/api/usage",
|
|
26
|
+
"/api/policy",
|
|
27
|
+
"/api/catalog",
|
|
28
|
+
"/api/products",
|
|
29
|
+
"/api/products/{id}",
|
|
30
|
+
"/api/info",
|
|
31
|
+
"/api/openapi.json",
|
|
32
|
+
];
|
|
33
|
+
const spec = buildOpenApiSpec("test-1.0.0");
|
|
34
|
+
const paths = Object.keys(spec.paths || {});
|
|
35
|
+
for (const route of documentedRoutes) {
|
|
36
|
+
assert.ok(paths.includes(route), `expected ${route} to be in the OpenAPI spec, paths=${paths.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
test("openapi — /api/info governance block schema documents every field the handler returns", () => {
|
|
40
|
+
const spec = buildOpenApiSpec("test-1.0.0");
|
|
41
|
+
const info = spec.paths?.["/api/info"]?.get;
|
|
42
|
+
assert.ok(info, "/api/info should be documented");
|
|
43
|
+
// Walk down to the governance properties; the schema is inlined so
|
|
44
|
+
// we don't have to chase $refs.
|
|
45
|
+
const schema = info.responses["200"].content["application/json"].schema;
|
|
46
|
+
const gov = schema.properties?.governance?.properties;
|
|
47
|
+
assert.ok(gov, "governance block should be a documented object schema");
|
|
48
|
+
for (const field of [
|
|
49
|
+
"authMode",
|
|
50
|
+
"authSecretEphemeral",
|
|
51
|
+
"oidcIssuer",
|
|
52
|
+
"auditPersisted",
|
|
53
|
+
"catalogConfigured",
|
|
54
|
+
"redaction",
|
|
55
|
+
"trustProxy",
|
|
56
|
+
"toolRatePerMin",
|
|
57
|
+
]) {
|
|
58
|
+
assert.ok(field in gov, `governance.${field} should be in the schema (got: ${Object.keys(gov).join(", ")})`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
test("openapi — info.version is the version string the caller passed in", () => {
|
|
62
|
+
const spec = buildOpenApiSpec("9.9.9-test");
|
|
63
|
+
assert.equal(spec.info?.version, "9.9.9-test");
|
|
64
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conservative PII / secret redaction for tool outputs that may contain
|
|
3
|
+
* arbitrary log payloads.
|
|
4
|
+
*
|
|
5
|
+
* Scope of this module: pure string redaction with deterministic
|
|
6
|
+
* patterns. Returns the rewritten string plus a per-category count so
|
|
7
|
+
* callers can surface a "redacted N matches" hint to the user / agent
|
|
8
|
+
* without leaking what was matched. Designed to be safe-by-default
|
|
9
|
+
* (over-redact rather than under-redact) and explicit (each category
|
|
10
|
+
* tagged in the replacement marker, e.g. `[redacted-email]`).
|
|
11
|
+
*
|
|
12
|
+
* Bypass today is process-wide only: set `OMCP_REDACTION=off` if the
|
|
13
|
+
* upstream is already PII-clean. A per-request `redaction:bypass` RBAC
|
|
14
|
+
* permission for interactive admin sessions is on the roadmap — see
|
|
15
|
+
* docs/access-control.md "Why are my logs returning [redacted-email]?"
|
|
16
|
+
* and docs/redaction.md for the current state.
|
|
17
|
+
*/
|
|
18
|
+
export type RedactionCategory = "email" | "ipv4" | "ipv6" | "bearer" | "jwt" | "api-key" | "aws-key" | "slack-token" | "private-key" | "gh-pat" | "credit-card";
|
|
19
|
+
export interface RedactionResult {
|
|
20
|
+
text: string;
|
|
21
|
+
matches: Record<RedactionCategory, number>;
|
|
22
|
+
totalMatches: number;
|
|
23
|
+
}
|
|
24
|
+
/** Run all patterns in a deterministic order; later patterns won't
|
|
25
|
+
* re-match content already replaced by an earlier one (the marker
|
|
26
|
+
* starts with `[redacted-` which none of the patterns match). */
|
|
27
|
+
export declare function redactText(input: string): RedactionResult;
|
|
28
|
+
/** Maximum nesting depth the walker will descend into. Operational
|
|
29
|
+
* log payloads are essentially flat (objects of strings + a few
|
|
30
|
+
* nested arrays); a pathologically deep structure is almost certainly
|
|
31
|
+
* a bug or an attack, and stack-overflowing the auth path is worse
|
|
32
|
+
* than truncating. The cap is generous — well above anything a
|
|
33
|
+
* Prometheus / Loki record would ever produce. */
|
|
34
|
+
export declare const MAX_REDACT_DEPTH = 64;
|
|
35
|
+
/** Walk an arbitrary parsed-JSON value and redact every string leaf,
|
|
36
|
+
* accumulating match counts. Non-string leaves and structural keys are
|
|
37
|
+
* left untouched. Returns a new value (does not mutate input). Bails
|
|
38
|
+
* out below `MAX_REDACT_DEPTH` levels of nesting and returns the raw
|
|
39
|
+
* sub-tree untouched at that point. */
|
|
40
|
+
export declare function redactValue(input: unknown): {
|
|
41
|
+
value: unknown;
|
|
42
|
+
matches: Record<RedactionCategory, number>;
|
|
43
|
+
totalMatches: number;
|
|
44
|
+
};
|