@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { DEFAULT_TENANT, MAX_TENANT_LENGTH, normaliseTenant, tenantFromClaim, parseKeyTenants, } from "./context.js";
|
|
4
|
+
test("normaliseTenant — happy paths", () => {
|
|
5
|
+
assert.equal(normaliseTenant("acme"), "acme");
|
|
6
|
+
assert.equal(normaliseTenant("ACME"), "acme", "lowercases");
|
|
7
|
+
assert.equal(normaliseTenant(" acme-corp "), "acme-corp", "trims");
|
|
8
|
+
assert.equal(normaliseTenant("team_a.us-east"), "team_a.us-east");
|
|
9
|
+
});
|
|
10
|
+
test("normaliseTenant — invalid / empty / null falls back to default", () => {
|
|
11
|
+
assert.equal(normaliseTenant(undefined), DEFAULT_TENANT);
|
|
12
|
+
assert.equal(normaliseTenant(null), DEFAULT_TENANT);
|
|
13
|
+
assert.equal(normaliseTenant(""), DEFAULT_TENANT);
|
|
14
|
+
assert.equal(normaliseTenant(" "), DEFAULT_TENANT);
|
|
15
|
+
assert.equal(normaliseTenant(123), DEFAULT_TENANT);
|
|
16
|
+
// Disallowed shapes
|
|
17
|
+
assert.equal(normaliseTenant("acme/corp"), DEFAULT_TENANT, "slash rejected");
|
|
18
|
+
assert.equal(normaliseTenant("acme corp"), DEFAULT_TENANT, "space rejected");
|
|
19
|
+
assert.equal(normaliseTenant("..hidden"), DEFAULT_TENANT, "leading dot rejected");
|
|
20
|
+
assert.equal(normaliseTenant("-leading"), DEFAULT_TENANT, "leading dash rejected");
|
|
21
|
+
// Too long
|
|
22
|
+
assert.equal(normaliseTenant("x".repeat(MAX_TENANT_LENGTH + 1)), DEFAULT_TENANT);
|
|
23
|
+
});
|
|
24
|
+
test("normaliseTenant — exactly MAX_TENANT_LENGTH passes", () => {
|
|
25
|
+
const t = "a" + "x".repeat(MAX_TENANT_LENGTH - 1);
|
|
26
|
+
assert.equal(t.length, MAX_TENANT_LENGTH);
|
|
27
|
+
assert.equal(normaliseTenant(t), t);
|
|
28
|
+
});
|
|
29
|
+
test("tenantFromClaim — flat claim", () => {
|
|
30
|
+
assert.equal(tenantFromClaim({ tenant: "acme" }, "tenant"), "acme");
|
|
31
|
+
assert.equal(tenantFromClaim({ tenant: "ACME" }, "tenant"), "acme");
|
|
32
|
+
assert.equal(tenantFromClaim({}, "tenant"), DEFAULT_TENANT);
|
|
33
|
+
});
|
|
34
|
+
test("tenantFromClaim — dotted claim path", () => {
|
|
35
|
+
assert.equal(tenantFromClaim({ app: { tenant_id: "acme" } }, "app.tenant_id"), "acme");
|
|
36
|
+
assert.equal(tenantFromClaim({ app: { tenant_id: "acme" } }, "app.missing"), DEFAULT_TENANT);
|
|
37
|
+
assert.equal(tenantFromClaim({ app: { tenant_id: "acme" } }, "missing.path"), DEFAULT_TENANT);
|
|
38
|
+
});
|
|
39
|
+
test("tenantFromClaim — array claim takes first string entry", () => {
|
|
40
|
+
assert.equal(tenantFromClaim({ tenants: ["acme", "other"] }, "tenants"), "acme");
|
|
41
|
+
assert.equal(tenantFromClaim({ tenants: [123, "acme"] }, "tenants"), "acme");
|
|
42
|
+
assert.equal(tenantFromClaim({ tenants: [123, 456] }, "tenants"), DEFAULT_TENANT);
|
|
43
|
+
});
|
|
44
|
+
test("tenantFromClaim — non-string scalar falls back", () => {
|
|
45
|
+
assert.equal(tenantFromClaim({ tenant: 42 }, "tenant"), DEFAULT_TENANT);
|
|
46
|
+
assert.equal(tenantFromClaim({ tenant: true }, "tenant"), DEFAULT_TENANT);
|
|
47
|
+
assert.equal(tenantFromClaim({ tenant: null }, "tenant"), DEFAULT_TENANT);
|
|
48
|
+
});
|
|
49
|
+
test("tenantFromClaim — empty claimPath returns default", () => {
|
|
50
|
+
assert.equal(tenantFromClaim({ tenant: "acme" }, ""), DEFAULT_TENANT);
|
|
51
|
+
});
|
|
52
|
+
test("parseKeyTenants — happy path", () => {
|
|
53
|
+
const m = parseKeyTenants("ci=acme;agent=bigco; dev=team_a.us");
|
|
54
|
+
assert.equal(m.size, 3);
|
|
55
|
+
assert.equal(m.get("ci"), "acme");
|
|
56
|
+
assert.equal(m.get("agent"), "bigco");
|
|
57
|
+
assert.equal(m.get("dev"), "team_a.us");
|
|
58
|
+
});
|
|
59
|
+
test("parseKeyTenants — invalid tenant on the right-hand side normalises to default", () => {
|
|
60
|
+
const m = parseKeyTenants("ci=acme/corp;agent=BIGCO");
|
|
61
|
+
assert.equal(m.get("ci"), DEFAULT_TENANT, "slash → default");
|
|
62
|
+
assert.equal(m.get("agent"), "bigco", "uppercase OK after normalise");
|
|
63
|
+
});
|
|
64
|
+
test("parseKeyTenants — malformed entries skipped, doesn't crash", () => {
|
|
65
|
+
const m = parseKeyTenants("noequal;=novalueeither;valid=acme");
|
|
66
|
+
assert.equal(m.size, 1);
|
|
67
|
+
assert.equal(m.get("valid"), "acme");
|
|
68
|
+
});
|
|
69
|
+
test("parseKeyTenants — undefined / empty returns empty map", () => {
|
|
70
|
+
assert.equal(parseKeyTenants(undefined).size, 0);
|
|
71
|
+
assert.equal(parseKeyTenants("").size, 0);
|
|
72
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration regression suite — pre-E7 single-tenant deployments must
|
|
3
|
+
* continue to work without any config change. These tests pin the
|
|
4
|
+
* "everything defaults to `default`" contract by simulating the
|
|
5
|
+
* exact data shapes a pre-E7 server / file / token would carry.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration regression suite — pre-E7 single-tenant deployments must
|
|
3
|
+
* continue to work without any config change. These tests pin the
|
|
4
|
+
* "everything defaults to `default`" contract by simulating the
|
|
5
|
+
* exact data shapes a pre-E7 server / file / token would carry.
|
|
6
|
+
*/
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { defaultContext, principalContext } from "../context.js";
|
|
10
|
+
import { issueSession, verifySession } from "../auth/session.js";
|
|
11
|
+
import { loadCredentials } from "../auth/credentials.js";
|
|
12
|
+
import { CatalogStore } from "../catalog/loader.js";
|
|
13
|
+
import { AuditLog } from "../audit/log.js";
|
|
14
|
+
import { DEFAULT_TENANT } from "./context.js";
|
|
15
|
+
const SECRET = "x".repeat(32);
|
|
16
|
+
test("migration — anonymous context lands in DEFAULT_TENANT", () => {
|
|
17
|
+
const ctx = defaultContext();
|
|
18
|
+
assert.equal(ctx.tenant, DEFAULT_TENANT);
|
|
19
|
+
});
|
|
20
|
+
test("migration — principalContext without tenant opt → DEFAULT_TENANT", () => {
|
|
21
|
+
const ctx = principalContext("agent", ["prom-prod"]);
|
|
22
|
+
assert.equal(ctx.tenant, DEFAULT_TENANT);
|
|
23
|
+
});
|
|
24
|
+
test("migration — pre-E7 session cookie (no tenant field) verifies + reads back fine", () => {
|
|
25
|
+
// Session minted as it would have been pre-E7: no tenant.
|
|
26
|
+
const { cookie } = issueSession({ sub: "alice", name: "Alice", roles: ["operator"] }, { secret: SECRET });
|
|
27
|
+
const verified = verifySession(cookie, { secret: SECRET });
|
|
28
|
+
assert.ok(verified, "verifySession should accept a pre-E7 cookie");
|
|
29
|
+
assert.equal(verified.tenant, undefined, "tenant stays undefined; consumers default to 'default'");
|
|
30
|
+
});
|
|
31
|
+
test("migration — pre-E7 OMCP_API_KEYS (no OMCP_KEY_TENANTS) leaves credentials in DEFAULT_TENANT", () => {
|
|
32
|
+
const creds = loadCredentials({ OMCP_API_KEYS: "agent:tok_abc,ci:tok_def" });
|
|
33
|
+
assert.equal(creds.length, 2);
|
|
34
|
+
for (const c of creds) {
|
|
35
|
+
assert.equal(c.tenant, undefined, "no env → no tenant assignment → consumers default to 'default'");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
test("migration — pre-E7 catalog (entries without tenant field) still enriches DEFAULT_TENANT callers", () => {
|
|
39
|
+
const store = new CatalogStore({
|
|
40
|
+
services: {
|
|
41
|
+
"payments": { owner: "team-payments" }, // pre-E7 shape
|
|
42
|
+
"shipping": { owner: "team-shipping" },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
// A pre-E7 caller (no session, ctx.tenant = "default") sees both
|
|
46
|
+
// entries through the tenant-aware get().
|
|
47
|
+
assert.equal(store.get("payments", DEFAULT_TENANT)?.owner, "team-payments");
|
|
48
|
+
assert.equal(store.get("shipping", DEFAULT_TENANT)?.owner, "team-shipping");
|
|
49
|
+
// Same caller via the unfiltered get path also sees them (admins).
|
|
50
|
+
assert.equal(store.get("payments")?.owner, "team-payments");
|
|
51
|
+
});
|
|
52
|
+
test("migration — pre-E7 audit entries (no tenant field) surface under ?tenant=default", async () => {
|
|
53
|
+
const log = new AuditLog();
|
|
54
|
+
// Pre-E7 record: no tenant.
|
|
55
|
+
await log.record({ actor: { sub: "alice" }, resource: "sources", action: "write", method: "POST", path: "/api/sources", status: 200 });
|
|
56
|
+
const entries = log.list({ tenant: "default" });
|
|
57
|
+
assert.equal(entries.length, 1);
|
|
58
|
+
assert.equal(entries[0].actor.sub, "alice");
|
|
59
|
+
});
|
|
60
|
+
test("migration — opt-in is per-entry: an admin defining `tenant: acme` doesn't break the rest", () => {
|
|
61
|
+
const store = new CatalogStore({
|
|
62
|
+
services: {
|
|
63
|
+
"acme-app": { owner: "acme-team", tenant: "acme" }, // opted in
|
|
64
|
+
"shared-cdn": { owner: "infra" }, // pre-E7 shape
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// The acme-tenant caller sees only their entry.
|
|
68
|
+
assert.equal(store.count("acme"), 1);
|
|
69
|
+
assert.equal(store.get("shared-cdn", "acme"), undefined);
|
|
70
|
+
// The default-tenant caller (anonymous / single-tenant) sees only
|
|
71
|
+
// the pre-E7 entry — the acme entry is correctly hidden.
|
|
72
|
+
assert.equal(store.count("default"), 1);
|
|
73
|
+
assert.equal(store.get("acme-app", "default"), undefined);
|
|
74
|
+
assert.equal(store.get("shared-cdn", "default")?.owner, "infra");
|
|
75
|
+
});
|