@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.
Files changed (111) hide show
  1. package/config/products.yaml.example +48 -0
  2. package/dist/audit/log.d.ts +99 -0
  3. package/dist/audit/log.js +180 -0
  4. package/dist/audit/log.test.d.ts +1 -0
  5. package/dist/audit/log.test.js +147 -0
  6. package/dist/audit/middleware.d.ts +20 -0
  7. package/dist/audit/middleware.js +50 -0
  8. package/dist/auth/credentials.d.ts +18 -0
  9. package/dist/auth/credentials.js +26 -1
  10. package/dist/auth/credentials.test.js +26 -1
  11. package/dist/auth/local-users.d.ts +62 -0
  12. package/dist/auth/local-users.js +143 -0
  13. package/dist/auth/local-users.test.d.ts +1 -0
  14. package/dist/auth/local-users.test.js +80 -0
  15. package/dist/auth/middleware.d.ts +48 -0
  16. package/dist/auth/middleware.js +65 -0
  17. package/dist/auth/middleware.test.d.ts +1 -0
  18. package/dist/auth/middleware.test.js +90 -0
  19. package/dist/auth/oidc/client.d.ts +73 -0
  20. package/dist/auth/oidc/client.js +104 -0
  21. package/dist/auth/oidc/client.test.d.ts +1 -0
  22. package/dist/auth/oidc/client.test.js +121 -0
  23. package/dist/auth/oidc/discovery.d.ts +38 -0
  24. package/dist/auth/oidc/discovery.js +48 -0
  25. package/dist/auth/oidc/discovery.test.d.ts +1 -0
  26. package/dist/auth/oidc/discovery.test.js +68 -0
  27. package/dist/auth/oidc/endpoints.d.ts +20 -0
  28. package/dist/auth/oidc/endpoints.js +124 -0
  29. package/dist/auth/oidc/endpoints.test.d.ts +7 -0
  30. package/dist/auth/oidc/endpoints.test.js +304 -0
  31. package/dist/auth/oidc/flow-cookie.d.ts +57 -0
  32. package/dist/auth/oidc/flow-cookie.js +142 -0
  33. package/dist/auth/oidc/flow-cookie.test.d.ts +1 -0
  34. package/dist/auth/oidc/flow-cookie.test.js +0 -0
  35. package/dist/auth/oidc/index.d.ts +7 -0
  36. package/dist/auth/oidc/index.js +6 -0
  37. package/dist/auth/oidc/jwks.d.ts +36 -0
  38. package/dist/auth/oidc/jwks.js +69 -0
  39. package/dist/auth/oidc/jwks.test.d.ts +1 -0
  40. package/dist/auth/oidc/jwks.test.js +65 -0
  41. package/dist/auth/oidc/jwt.d.ts +62 -0
  42. package/dist/auth/oidc/jwt.js +113 -0
  43. package/dist/auth/oidc/jwt.test.d.ts +1 -0
  44. package/dist/auth/oidc/jwt.test.js +141 -0
  45. package/dist/auth/oidc/pkce.d.ts +19 -0
  46. package/dist/auth/oidc/pkce.js +43 -0
  47. package/dist/auth/oidc/pkce.test.d.ts +1 -0
  48. package/dist/auth/oidc/pkce.test.js +55 -0
  49. package/dist/auth/oidc/runtime.d.ts +63 -0
  50. package/dist/auth/oidc/runtime.js +129 -0
  51. package/dist/auth/oidc/runtime.test.d.ts +1 -0
  52. package/dist/auth/oidc/runtime.test.js +180 -0
  53. package/dist/auth/policy/engine.d.ts +48 -0
  54. package/dist/auth/policy/engine.js +73 -0
  55. package/dist/auth/policy/engine.test.d.ts +1 -0
  56. package/dist/auth/policy/engine.test.js +98 -0
  57. package/dist/auth/policy/loader.d.ts +35 -0
  58. package/dist/auth/policy/loader.js +100 -0
  59. package/dist/auth/policy/opa.d.ts +69 -0
  60. package/dist/auth/policy/opa.js +162 -0
  61. package/dist/auth/policy/opa.test.d.ts +1 -0
  62. package/dist/auth/policy/opa.test.js +158 -0
  63. package/dist/auth/rbac.d.ts +40 -0
  64. package/dist/auth/rbac.js +120 -0
  65. package/dist/auth/rbac.test.d.ts +1 -0
  66. package/dist/auth/rbac.test.js +121 -0
  67. package/dist/auth/session.d.ts +66 -0
  68. package/dist/auth/session.js +146 -0
  69. package/dist/auth/session.test.d.ts +1 -0
  70. package/dist/auth/session.test.js +90 -0
  71. package/dist/catalog/loader.d.ts +67 -0
  72. package/dist/catalog/loader.js +122 -0
  73. package/dist/catalog/loader.test.d.ts +1 -0
  74. package/dist/catalog/loader.test.js +108 -0
  75. package/dist/connectors/kubernetes.d.ts +1 -0
  76. package/dist/connectors/kubernetes.js +12 -2
  77. package/dist/connectors/topology-vocabulary.d.ts +41 -0
  78. package/dist/connectors/topology-vocabulary.js +120 -0
  79. package/dist/connectors/topology-vocabulary.test.d.ts +1 -0
  80. package/dist/connectors/topology-vocabulary.test.js +63 -0
  81. package/dist/context.d.ts +13 -1
  82. package/dist/context.js +5 -1
  83. package/dist/index.js +1012 -29
  84. package/dist/net/egress-policy.js +2 -0
  85. package/dist/openapi.js +440 -0
  86. package/dist/openapi.test.d.ts +1 -0
  87. package/dist/openapi.test.js +64 -0
  88. package/dist/policy/redact.d.ts +44 -0
  89. package/dist/policy/redact.js +144 -0
  90. package/dist/policy/redact.test.d.ts +1 -0
  91. package/dist/policy/redact.test.js +172 -0
  92. package/dist/products/loader.d.ts +84 -0
  93. package/dist/products/loader.js +216 -0
  94. package/dist/products/loader.test.d.ts +1 -0
  95. package/dist/products/loader.test.js +168 -0
  96. package/dist/quota/limiter.d.ts +72 -0
  97. package/dist/quota/limiter.js +105 -0
  98. package/dist/quota/limiter.test.d.ts +1 -0
  99. package/dist/quota/limiter.test.js +119 -0
  100. package/dist/quota/token-budget.d.ts +119 -0
  101. package/dist/quota/token-budget.js +297 -0
  102. package/dist/quota/token-budget.test.d.ts +1 -0
  103. package/dist/quota/token-budget.test.js +215 -0
  104. package/dist/tenancy/context.d.ts +45 -0
  105. package/dist/tenancy/context.js +97 -0
  106. package/dist/tenancy/context.test.d.ts +1 -0
  107. package/dist/tenancy/context.test.js +72 -0
  108. package/dist/tenancy/migration.test.d.ts +7 -0
  109. package/dist/tenancy/migration.test.js +75 -0
  110. package/dist/ui/index.html +1454 -88
  111. package/package.json +20 -3
@@ -0,0 +1,108 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { readCatalogFile, validateCatalog, CatalogStore } from "./loader.js";
7
+ test("readCatalogFile — missing path returns empty catalog", async () => {
8
+ const c = await readCatalogFile(undefined);
9
+ assert.deepEqual(c, { services: {} });
10
+ });
11
+ test("readCatalogFile — missing file returns empty catalog (no crash)", async () => {
12
+ const c = await readCatalogFile("/tmp/definitely-does-not-exist-omcp.json");
13
+ assert.deepEqual(c, { services: {} });
14
+ });
15
+ test("readCatalogFile — malformed JSON returns empty catalog", async () => {
16
+ const dir = await mkdtemp(join(tmpdir(), "omcp-catalog-"));
17
+ try {
18
+ const file = join(dir, "catalog.json");
19
+ await writeFile(file, "{this is not json", "utf8");
20
+ const c = await readCatalogFile(file);
21
+ assert.deepEqual(c, { services: {} });
22
+ }
23
+ finally {
24
+ await rm(dir, { recursive: true, force: true });
25
+ }
26
+ });
27
+ test("readCatalogFile — valid file returns parsed catalog", async () => {
28
+ const dir = await mkdtemp(join(tmpdir(), "omcp-catalog-"));
29
+ try {
30
+ const file = join(dir, "catalog.json");
31
+ await writeFile(file, JSON.stringify({
32
+ services: {
33
+ "payment-service": {
34
+ owner: "team-payments",
35
+ tier: "tier-1",
36
+ dataClassification: "confidential",
37
+ slo: "99.9%",
38
+ runbooks: ["https://runbooks.example/payments"],
39
+ tags: ["pci", "regulated"],
40
+ },
41
+ },
42
+ }), "utf8");
43
+ const c = await readCatalogFile(file);
44
+ assert.equal(c.services["payment-service"].owner, "team-payments");
45
+ assert.equal(c.services["payment-service"].tier, "tier-1");
46
+ assert.deepEqual(c.services["payment-service"].tags, ["pci", "regulated"]);
47
+ }
48
+ finally {
49
+ await rm(dir, { recursive: true, force: true });
50
+ }
51
+ });
52
+ test("validateCatalog — rejects invalid tier values", () => {
53
+ const c = validateCatalog({ services: { svc: { tier: "tier-9" } } });
54
+ assert.equal(c.services.svc.tier, undefined);
55
+ });
56
+ test("validateCatalog — rejects invalid data classification", () => {
57
+ const c = validateCatalog({ services: { svc: { dataClassification: "ultra-secret" } } });
58
+ assert.equal(c.services.svc.dataClassification, undefined);
59
+ });
60
+ test("validateCatalog — strips non-string entries from runbooks / tags", () => {
61
+ const c = validateCatalog({
62
+ services: { svc: { runbooks: ["a", 1, "b", null], tags: [{}, "tag1"] } },
63
+ });
64
+ assert.deepEqual(c.services.svc.runbooks, ["a", "b"]);
65
+ assert.deepEqual(c.services.svc.tags, ["tag1"]);
66
+ });
67
+ test("validateCatalog — skips non-object service entries", () => {
68
+ const c = validateCatalog({ services: { svc1: "string", svc2: null, svc3: { owner: "team" } } });
69
+ assert.equal(c.services.svc1, undefined);
70
+ assert.equal(c.services.svc2, undefined);
71
+ assert.equal(c.services.svc3.owner, "team");
72
+ });
73
+ test("CatalogStore — get / list / count / replace", () => {
74
+ const store = new CatalogStore({
75
+ services: {
76
+ a: { owner: "team-a" },
77
+ b: { owner: "team-b" },
78
+ },
79
+ });
80
+ assert.equal(store.count(), 2);
81
+ assert.equal(store.get("a")?.owner, "team-a");
82
+ assert.equal(store.get("nope"), undefined);
83
+ assert.equal(Object.keys(store.list()).length, 2);
84
+ store.replace({ services: { x: { owner: "team-x" } } });
85
+ assert.equal(store.count(), 1);
86
+ assert.equal(store.get("x")?.owner, "team-x");
87
+ });
88
+ test("CatalogStore — tenant filter scopes get / list / count", () => {
89
+ const store = new CatalogStore({
90
+ services: {
91
+ "acme-payments": { owner: "team-a", tenant: "acme" },
92
+ "bigco-payments": { owner: "team-b", tenant: "bigco" },
93
+ "shared-cdn": { owner: "team-c" }, // no tenant → "default"
94
+ },
95
+ });
96
+ // No tenant filter → see everything (admin-style read)
97
+ assert.equal(store.count(), 3);
98
+ // Tenant-scoped: each sees its own entries + nothing else
99
+ assert.equal(store.count("acme"), 1);
100
+ assert.equal(store.get("acme-payments", "acme")?.owner, "team-a");
101
+ assert.equal(store.get("bigco-payments", "acme"), undefined, "cross-tenant get must return undefined");
102
+ assert.equal(Object.keys(store.list("acme"))[0], "acme-payments");
103
+ // Default tenant includes entries with no tenant field
104
+ assert.equal(store.count("default"), 1);
105
+ assert.equal(store.get("shared-cdn", "default")?.owner, "team-c");
106
+ // Unknown tenant → empty
107
+ assert.equal(store.count("unknown"), 0);
108
+ });
@@ -29,6 +29,7 @@ export declare class KubernetesConnector implements ObservabilityConnector {
29
29
  readonly signalType: SignalType;
30
30
  name: string;
31
31
  private store;
32
+ private warnedVocab;
32
33
  private factory?;
33
34
  private informers;
34
35
  private providerOverride?;
@@ -1,4 +1,5 @@
1
1
  import { TopologyStore, podResource, podEdges, nodeResource, deploymentResource, deploymentEdges, replicaSetResource, replicaSetEdges, namespaceResource, namespacedId, clusterScopedId, } from "./kubernetes-graph.js";
2
+ import { validateSnapshot } from "./topology-vocabulary.js";
2
3
  // Default provider is loaded lazily so tests don't pay the
3
4
  // @kubernetes/client-node import cost (and so the module is usable in
4
5
  // environments where the SDK isn't installed yet — e.g. unit tests in CI
@@ -19,6 +20,7 @@ export class KubernetesConnector {
19
20
  signalType = "topology";
20
21
  name = "";
21
22
  store;
23
+ warnedVocab = new Set();
22
24
  factory;
23
25
  informers = [];
24
26
  providerOverride;
@@ -141,12 +143,20 @@ export class KubernetesConnector {
141
143
  return this.store?.listEdges() ?? [];
142
144
  }
143
145
  async getTopologySnapshot() {
144
- return (this.store?.snapshot() ?? {
146
+ const snap = this.store?.snapshot() ?? {
145
147
  source: this.name,
146
148
  resources: [],
147
149
  edges: [],
148
150
  revision: 0,
149
- });
151
+ };
152
+ for (const w of validateSnapshot(snap.resources, snap.edges)) {
153
+ const key = `${w.kind}:${w.value}`;
154
+ if (this.warnedVocab.has(key))
155
+ continue;
156
+ this.warnedVocab.add(key);
157
+ console.warn("topology vocabulary warning (source=%s): %s", this.name, w.message);
158
+ }
159
+ return snap;
150
160
  }
151
161
  watchTopology(listener) {
152
162
  if (!this.store)
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Canonical vocabulary for the connector-agnostic topology graph.
3
+ *
4
+ * The set is intentionally small: only values that are emitted by a shipped
5
+ * connector OR are reserved as the agreed name for a near-term connector.
6
+ * Adding a value is a documentation change in docs/topology-vocabulary.md
7
+ * plus an entry in the const arrays below; never invent a value at the call
8
+ * site without that paper trail.
9
+ *
10
+ * Validation is warn-only by design — a connector that emits an unknown
11
+ * value still works, but the warning shows up in logs and unit tests so
12
+ * vocabulary drift gets caught before it spreads.
13
+ */
14
+ import type { Edge, Resource } from "../types.js";
15
+ export declare const KINDS: readonly ["pod", "node", "deployment", "replicaset", "namespace", "service", "container", "vm", "host", "hypervisor", "cluster"];
16
+ export type Kind = (typeof KINDS)[number];
17
+ export declare const RELATIONS: readonly ["RUNS_ON", "OWNED_BY", "IN_NAMESPACE", "CALLS", "CONTAINS", "DEPENDS_ON"];
18
+ export type Relation = (typeof RELATIONS)[number];
19
+ export declare function isKnownKind(k: string): k is Kind;
20
+ export declare function isKnownRelation(r: string): r is Relation;
21
+ export interface VocabularyWarning {
22
+ kind: "unknown_resource_kind" | "unknown_relation" | "case_mismatch";
23
+ message: string;
24
+ value: string;
25
+ }
26
+ /**
27
+ * Lint a single resource. Returns warnings for unknown or miscased `kind`
28
+ * values; an empty array means the resource passes the vocabulary.
29
+ */
30
+ export declare function validateResource(r: Pick<Resource, "kind">): VocabularyWarning[];
31
+ /**
32
+ * Lint a single edge. Returns warnings for unknown or miscased `relation`
33
+ * values; an empty array means the edge passes the vocabulary.
34
+ */
35
+ export declare function validateEdge(e: Pick<Edge, "relation">): VocabularyWarning[];
36
+ /**
37
+ * Convenience: lint a full snapshot. Returns the de-duplicated set of
38
+ * warnings (one per distinct value) so a noisy connector does not flood
39
+ * the log on each tick.
40
+ */
41
+ export declare function validateSnapshot(resources: Pick<Resource, "kind">[], edges: Pick<Edge, "relation">[]): VocabularyWarning[];
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Canonical vocabulary for the connector-agnostic topology graph.
3
+ *
4
+ * The set is intentionally small: only values that are emitted by a shipped
5
+ * connector OR are reserved as the agreed name for a near-term connector.
6
+ * Adding a value is a documentation change in docs/topology-vocabulary.md
7
+ * plus an entry in the const arrays below; never invent a value at the call
8
+ * site without that paper trail.
9
+ *
10
+ * Validation is warn-only by design — a connector that emits an unknown
11
+ * value still works, but the warning shows up in logs and unit tests so
12
+ * vocabulary drift gets caught before it spreads.
13
+ */
14
+ export const KINDS = [
15
+ "pod",
16
+ "node",
17
+ "deployment",
18
+ "replicaset",
19
+ "namespace",
20
+ "service",
21
+ "container",
22
+ "vm",
23
+ "host",
24
+ "hypervisor",
25
+ "cluster",
26
+ ];
27
+ export const RELATIONS = [
28
+ "RUNS_ON",
29
+ "OWNED_BY",
30
+ "IN_NAMESPACE",
31
+ "CALLS",
32
+ "CONTAINS",
33
+ "DEPENDS_ON",
34
+ ];
35
+ const KIND_SET = new Set(KINDS);
36
+ const RELATION_SET = new Set(RELATIONS);
37
+ export function isKnownKind(k) {
38
+ return KIND_SET.has(k);
39
+ }
40
+ export function isKnownRelation(r) {
41
+ return RELATION_SET.has(r);
42
+ }
43
+ /**
44
+ * Lint a single resource. Returns warnings for unknown or miscased `kind`
45
+ * values; an empty array means the resource passes the vocabulary.
46
+ */
47
+ export function validateResource(r) {
48
+ const out = [];
49
+ if (!isKnownKind(r.kind)) {
50
+ const lower = r.kind.toLowerCase();
51
+ if (isKnownKind(lower)) {
52
+ out.push({
53
+ kind: "case_mismatch",
54
+ value: r.kind,
55
+ message: `resource kind "${r.kind}" should be lowercase "${lower}" — see docs/topology-vocabulary.md`,
56
+ });
57
+ }
58
+ else {
59
+ out.push({
60
+ kind: "unknown_resource_kind",
61
+ value: r.kind,
62
+ message: `resource kind "${r.kind}" is not in the canonical vocabulary; either rename or extend docs/topology-vocabulary.md + KINDS`,
63
+ });
64
+ }
65
+ }
66
+ return out;
67
+ }
68
+ /**
69
+ * Lint a single edge. Returns warnings for unknown or miscased `relation`
70
+ * values; an empty array means the edge passes the vocabulary.
71
+ */
72
+ export function validateEdge(e) {
73
+ const out = [];
74
+ if (!isKnownRelation(e.relation)) {
75
+ const upper = e.relation.toUpperCase();
76
+ if (isKnownRelation(upper)) {
77
+ out.push({
78
+ kind: "case_mismatch",
79
+ value: e.relation,
80
+ message: `relation "${e.relation}" should be UPPER_SNAKE "${upper}" — see docs/topology-vocabulary.md`,
81
+ });
82
+ }
83
+ else {
84
+ out.push({
85
+ kind: "unknown_relation",
86
+ value: e.relation,
87
+ message: `relation "${e.relation}" is not in the canonical vocabulary; either rename or extend docs/topology-vocabulary.md + RELATIONS`,
88
+ });
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+ /**
94
+ * Convenience: lint a full snapshot. Returns the de-duplicated set of
95
+ * warnings (one per distinct value) so a noisy connector does not flood
96
+ * the log on each tick.
97
+ */
98
+ export function validateSnapshot(resources, edges) {
99
+ const seen = new Set();
100
+ const out = [];
101
+ for (const r of resources) {
102
+ for (const w of validateResource(r)) {
103
+ const key = `r:${w.kind}:${w.value}`;
104
+ if (!seen.has(key)) {
105
+ seen.add(key);
106
+ out.push(w);
107
+ }
108
+ }
109
+ }
110
+ for (const e of edges) {
111
+ for (const w of validateEdge(e)) {
112
+ const key = `e:${w.kind}:${w.value}`;
113
+ if (!seen.has(key)) {
114
+ seen.add(key);
115
+ out.push(w);
116
+ }
117
+ }
118
+ }
119
+ return out;
120
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { KINDS, RELATIONS, isKnownKind, isKnownRelation, validateResource, validateEdge, validateSnapshot, } from "./topology-vocabulary.js";
4
+ test("vocabulary — KINDS and RELATIONS contain what the kubernetes connector emits today", () => {
5
+ for (const k of ["pod", "node", "deployment", "replicaset", "namespace"]) {
6
+ assert.equal(isKnownKind(k), true, `kind "${k}" must be in the canonical vocabulary`);
7
+ }
8
+ for (const r of ["RUNS_ON", "OWNED_BY", "IN_NAMESPACE"]) {
9
+ assert.equal(isKnownRelation(r), true, `relation "${r}" must be in the canonical vocabulary`);
10
+ }
11
+ });
12
+ test("vocabulary — CALLS is reserved for the upcoming trace connector", () => {
13
+ assert.equal(isKnownRelation("CALLS"), true);
14
+ });
15
+ test("validateResource — canonical kinds produce no warnings", () => {
16
+ for (const k of KINDS) {
17
+ assert.deepEqual(validateResource({ kind: k }), []);
18
+ }
19
+ });
20
+ test("validateResource — unknown kind warns", () => {
21
+ const w = validateResource({ kind: "frobnicator" });
22
+ assert.equal(w.length, 1);
23
+ assert.equal(w[0].kind, "unknown_resource_kind");
24
+ assert.equal(w[0].value, "frobnicator");
25
+ });
26
+ test("validateResource — uppercase kind triggers a case-mismatch hint", () => {
27
+ const w = validateResource({ kind: "Pod" });
28
+ assert.equal(w.length, 1);
29
+ assert.equal(w[0].kind, "case_mismatch");
30
+ assert.match(w[0].message, /lowercase "pod"/);
31
+ });
32
+ test("validateEdge — canonical relations produce no warnings", () => {
33
+ for (const r of RELATIONS) {
34
+ assert.deepEqual(validateEdge({ relation: r }), []);
35
+ }
36
+ });
37
+ test("validateEdge — unknown relation warns", () => {
38
+ const w = validateEdge({ relation: "FROBNICATES" });
39
+ assert.equal(w.length, 1);
40
+ assert.equal(w[0].kind, "unknown_relation");
41
+ });
42
+ test("validateEdge — lowercase relation triggers a case-mismatch hint", () => {
43
+ const w = validateEdge({ relation: "runs_on" });
44
+ assert.equal(w.length, 1);
45
+ assert.equal(w[0].kind, "case_mismatch");
46
+ assert.match(w[0].message, /UPPER_SNAKE "RUNS_ON"/);
47
+ });
48
+ test("validateSnapshot — de-duplicates repeated offenders", () => {
49
+ const warnings = validateSnapshot([
50
+ { kind: "frobnicator" },
51
+ { kind: "frobnicator" },
52
+ { kind: "pod" },
53
+ ], [
54
+ { relation: "FROBNICATES" },
55
+ { relation: "FROBNICATES" },
56
+ { relation: "RUNS_ON" },
57
+ ]);
58
+ assert.equal(warnings.length, 2, `expected one warning per distinct offender, got ${warnings.length}`);
59
+ });
60
+ test("validateSnapshot — a fully canonical snapshot is silent", () => {
61
+ const warnings = validateSnapshot([{ kind: "pod" }, { kind: "node" }, { kind: "deployment" }], [{ relation: "RUNS_ON" }, { relation: "OWNED_BY" }, { relation: "IN_NAMESPACE" }]);
62
+ assert.deepEqual(warnings, []);
63
+ });
package/dist/context.d.ts CHANGED
@@ -18,10 +18,22 @@ export interface RequestContext {
18
18
  * scoping (tools/services/lookback/read-only) is a separate concern.
19
19
  */
20
20
  allowedSources?: string[];
21
+ /** When true, the credential is allowed to opt out of redaction on a
22
+ * per-tool-call basis. The actual bypass is engaged only when the
23
+ * tool call ALSO sets `bypass_redaction: true` in its args. Default
24
+ * false. Configured via OMCP_KEY_BYPASS_REDACTION. */
25
+ allowBypassRedaction?: boolean;
26
+ /** Tenant the request operates in. ALWAYS set — defaults to
27
+ * "default" for anonymous principals + missing-tenant credentials,
28
+ * preserving the single-namespace behaviour of pre-E7 deployments. */
29
+ tenant: string;
21
30
  /** Correlates all tool calls within one transport request/session. */
22
31
  correlationId: string;
23
32
  }
24
33
  /** Default all-access anonymous context — preserves current behaviour. */
25
34
  export declare function defaultContext(): RequestContext;
26
35
  /** Context for an authenticated API-key principal. */
27
- export declare function principalContext(principalId: string, allowedSources?: string[]): RequestContext;
36
+ export declare function principalContext(principalId: string, allowedSources?: string[], opts?: {
37
+ allowBypassRedaction?: boolean;
38
+ tenant?: string;
39
+ }): RequestContext;
package/dist/context.js CHANGED
@@ -1,18 +1,22 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { DEFAULT_TENANT, normaliseTenant } from "./tenancy/context.js";
2
3
  /** Default all-access anonymous context — preserves current behaviour. */
3
4
  export function defaultContext() {
4
5
  return {
5
6
  principalId: "anonymous",
6
7
  auth: "anonymous",
8
+ tenant: DEFAULT_TENANT,
7
9
  correlationId: randomUUID(),
8
10
  };
9
11
  }
10
12
  /** Context for an authenticated API-key principal. */
11
- export function principalContext(principalId, allowedSources) {
13
+ export function principalContext(principalId, allowedSources, opts = {}) {
12
14
  return {
13
15
  principalId,
14
16
  auth: "apikey",
15
17
  allowedSources: allowedSources && allowedSources.length > 0 ? allowedSources : undefined,
18
+ allowBypassRedaction: opts.allowBypassRedaction || undefined,
19
+ tenant: normaliseTenant(opts.tenant),
16
20
  correlationId: randomUUID(),
17
21
  };
18
22
  }