@automagik/omni 2.260907.5 → 2.260907.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAySpC,wBAAgB,iBAAiB,IAAI,OAAO,CA6J3C"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAySpC,wBAAgB,iBAAiB,IAAI,OAAO,CA+J3C"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Multitenancy Commands (read-only; issue #982)
3
+ *
4
+ * omni multitenancy status - Show the server's tenancy posture
5
+ *
6
+ * STRICTLY VISIBILITY, BY DESIGN
7
+ * ------------------------------
8
+ * There is deliberately no `enable`, no cutover, and no write of any kind
9
+ * here: production cutover is receipt-gated and belongs to the separate
10
+ * private production authority. `status` wraps the posture the server already
11
+ * reports on the authenticated `POST /auth/validate` surface — it issues no
12
+ * additional server-side queries.
13
+ *
14
+ * A server that reports no posture is itself an answer: it predates posture
15
+ * reporting (and therefore the tenant control plane), which is one of the
16
+ * three states an operator staring at a `/api/v2/platform` 404 needs to tell
17
+ * apart. That case prints an explanation rather than fabricated defaults.
18
+ */
19
+ import { Command } from 'commander';
20
+ export declare function createMultitenancyCommand(): Command;
21
+ //# sourceMappingURL=multitenancy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multitenancy.d.ts","sourceRoot":"","sources":["../../src/commands/multitenancy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,yBAAyB,IAAI,OAAO,CAgCnD"}
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsLpC,wBAAgB,mBAAmB,IAAI,OAAO,CAU7C"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyLpC,wBAAgB,mBAAmB,IAAI,OAAO,CAU7C"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Platform Tenant Control-Plane Commands (`omni tenants`)
3
+ *
4
+ * Tenant lifecycle (list/get/create/suspend/archive) and memberships
5
+ * (list/add/disable/status/role) over the platform control plane
6
+ * (issue #981; wish: omni-full-multitenancy, Group G1; ADR-0005).
7
+ *
8
+ * Every command carries an audited `--reason` and refuses to run without it —
9
+ * the server enforces the same rule (reads validate the `x-platform-reason`
10
+ * header, mutations validate body `reason`), but a local check turns a
11
+ * validation 400 into an actionable message before any request is sent.
12
+ *
13
+ * The control plane is feature-flagged server-side: when
14
+ * `OMNI_MULTITENANCY_ENABLED` is not `true` the whole surface 404s, and only
15
+ * PLATFORM-class credentials are accepted. Both failure modes are rendered as
16
+ * explanations, not raw HTTP dumps.
17
+ *
18
+ * The `keys issue-root` subcommand is intentionally absent — the route ships
19
+ * separately (#979) and the subcommand follows it.
20
+ */
21
+ import { type OmniClient } from '@omni/sdk';
22
+ import { Command } from 'commander';
23
+ interface ReasonOption {
24
+ reason?: string;
25
+ }
26
+ interface CreateOptions extends ReasonOption {
27
+ slug: string;
28
+ name: string;
29
+ maxKeyTtl: number;
30
+ maxKeyRate: number;
31
+ maxKeyBudget: number;
32
+ }
33
+ interface MembershipAddOptions extends ReasonOption {
34
+ principal: string;
35
+ role: string;
36
+ }
37
+ interface MembershipStatusOptions extends ReasonOption {
38
+ status: string;
39
+ }
40
+ interface MembershipRoleOptions extends ReasonOption {
41
+ role: string;
42
+ }
43
+ /**
44
+ * Enforce the audited `--reason` before any request is sent. The server
45
+ * requires 3-500 characters; mirroring the bounds here keeps the refusal
46
+ * local, immediate, and explained.
47
+ */
48
+ declare function requireReason(reason: string | undefined, action: string): string;
49
+ /**
50
+ * Render a control-plane failure as an explanation, not a raw HTTP dump.
51
+ *
52
+ * - An "Endpoint not found" 404 means the surface is unmounted: multitenancy
53
+ * is disabled (`OMNI_MULTITENANCY_ENABLED`) or the server predates the
54
+ * control plane. Any other 404 is a genuine missing tenant/membership
55
+ * (the API is deliberately non-enumerating there).
56
+ * - 401/403 mean the configured key is not a PLATFORM-class credential with
57
+ * the required scope — tenant and legacy data-plane keys are denied by
58
+ * design, so "log in harder" is not the fix; a platform credential is.
59
+ */
60
+ declare function renderPlatformError(err: unknown, action: string): never;
61
+ declare function handleList(client: OmniClient, options: ReasonOption): Promise<void>;
62
+ declare function handleGet(client: OmniClient, id: string, options: ReasonOption): Promise<void>;
63
+ declare function handleCreate(client: OmniClient, options: CreateOptions): Promise<void>;
64
+ declare function handleSuspend(client: OmniClient, id: string, options: ReasonOption): Promise<void>;
65
+ declare function handleArchive(client: OmniClient, id: string, options: ReasonOption): Promise<void>;
66
+ declare function handleMembershipsList(client: OmniClient, tenantId: string, options: ReasonOption): Promise<void>;
67
+ declare function handleMembershipsAdd(client: OmniClient, tenantId: string, options: MembershipAddOptions): Promise<void>;
68
+ declare function handleMembershipsDisable(client: OmniClient, tenantId: string, membershipId: string, options: ReasonOption): Promise<void>;
69
+ declare function handleMembershipsStatus(client: OmniClient, tenantId: string, membershipId: string, options: MembershipStatusOptions): Promise<void>;
70
+ declare function handleMembershipsRole(client: OmniClient, tenantId: string, membershipId: string, options: MembershipRoleOptions): Promise<void>;
71
+ export declare function createTenantsCommand(): Command;
72
+ /**
73
+ * Exported for tests — lets us exercise reason enforcement, SDK-call mapping,
74
+ * and control-plane error rendering without reaching into Commander internals.
75
+ */
76
+ export declare const __testables: {
77
+ requireReason: typeof requireReason;
78
+ renderPlatformError: typeof renderPlatformError;
79
+ handleList: typeof handleList;
80
+ handleGet: typeof handleGet;
81
+ handleCreate: typeof handleCreate;
82
+ handleSuspend: typeof handleSuspend;
83
+ handleArchive: typeof handleArchive;
84
+ handleMembershipsList: typeof handleMembershipsList;
85
+ handleMembershipsAdd: typeof handleMembershipsAdd;
86
+ handleMembershipsDisable: typeof handleMembershipsDisable;
87
+ handleMembershipsStatus: typeof handleMembershipsStatus;
88
+ handleMembershipsRole: typeof handleMembershipsRole;
89
+ };
90
+ export {};
91
+ //# sourceMappingURL=tenants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenants.d.ts","sourceRoot":"","sources":["../../src/commands/tenants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAgB,KAAK,UAAU,EAAsD,MAAM,WAAW,CAAC;AAC9G,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAepC,UAAU,YAAY;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,aAAc,SAAQ,YAAY;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,oBAAqB,SAAQ,YAAY;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,uBAAwB,SAAQ,YAAY;IACpD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,qBAAsB,SAAQ,YAAY;IAClD,IAAI,EAAE,MAAM,CAAC;CACd;AA0BD;;;;GAIG;AACH,iBAAS,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMzE;AAgBD;;;;;;;;;;GAUG;AACH,iBAAS,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAchE;AA8BD,iBAAe,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlF;AAED,iBAAe,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAI7F;AAED,iBAAe,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAYrF;AAED,iBAAe,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAKjG;AAED,iBAAe,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjG;AAMD,iBAAe,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/G;AAED,iBAAe,oBAAoB,CACjC,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CASf;AAED,iBAAe,wBAAwB,CACrC,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,iBAAe,uBAAuB,CACpC,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,iBAAe,qBAAqB,CAClC,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAKf;AAQD,wBAAgB,oBAAoB,IAAI,OAAO,CAqJ9C;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;CAavB,CAAC"}
package/dist/index.js CHANGED
@@ -135777,6 +135777,132 @@ function createOmniClient(config) {
135777
135777
  throw OmniApiError.from(await resp.json(), resp.status);
135778
135778
  }
135779
135779
  },
135780
+ platform: {
135781
+ tenants: {
135782
+ async list(reason) {
135783
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants`, {
135784
+ headers: { "x-platform-reason": reason }
135785
+ });
135786
+ const json = await resp.json();
135787
+ if (!resp.ok)
135788
+ throw OmniApiError.from(json, resp.status);
135789
+ return { items: json?.items ?? [] };
135790
+ },
135791
+ async get(id, reason) {
135792
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${id}`, {
135793
+ headers: { "x-platform-reason": reason }
135794
+ });
135795
+ const json = await resp.json();
135796
+ if (!resp.ok)
135797
+ throw OmniApiError.from(json, resp.status);
135798
+ if (!json?.data)
135799
+ throw new OmniApiError("Tenant not found", "NOT_FOUND", undefined, 404);
135800
+ return json.data;
135801
+ },
135802
+ async create(body) {
135803
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants`, {
135804
+ method: "POST",
135805
+ headers: { "Content-Type": "application/json" },
135806
+ body: JSON.stringify(body)
135807
+ });
135808
+ const json = await resp.json();
135809
+ if (!resp.ok)
135810
+ throw OmniApiError.from(json, resp.status);
135811
+ if (!json?.data)
135812
+ throw new OmniApiError("Failed to create tenant", "CREATE_FAILED", undefined, resp.status);
135813
+ return json.data;
135814
+ },
135815
+ async suspend(id, reason) {
135816
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${id}/suspend`, {
135817
+ method: "POST",
135818
+ headers: { "Content-Type": "application/json" },
135819
+ body: JSON.stringify({ reason })
135820
+ });
135821
+ const json = await resp.json();
135822
+ if (!resp.ok)
135823
+ throw OmniApiError.from(json, resp.status);
135824
+ if (!json?.data)
135825
+ throw new OmniApiError("Failed to suspend tenant", "SUSPEND_FAILED", undefined, resp.status);
135826
+ return json.data;
135827
+ },
135828
+ async archive(id, reason) {
135829
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${id}/archive`, {
135830
+ method: "POST",
135831
+ headers: { "Content-Type": "application/json" },
135832
+ body: JSON.stringify({ reason })
135833
+ });
135834
+ const json = await resp.json();
135835
+ if (!resp.ok)
135836
+ throw OmniApiError.from(json, resp.status);
135837
+ if (!json?.data)
135838
+ throw new OmniApiError("Failed to archive tenant", "ARCHIVE_FAILED", undefined, resp.status);
135839
+ return json.data;
135840
+ },
135841
+ memberships: {
135842
+ async list(tenantId, reason) {
135843
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${tenantId}/memberships`, {
135844
+ headers: { "x-platform-reason": reason }
135845
+ });
135846
+ const json = await resp.json();
135847
+ if (!resp.ok)
135848
+ throw OmniApiError.from(json, resp.status);
135849
+ return { items: json?.items ?? [] };
135850
+ },
135851
+ async attach(tenantId, body) {
135852
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${tenantId}/memberships`, {
135853
+ method: "POST",
135854
+ headers: { "Content-Type": "application/json" },
135855
+ body: JSON.stringify(body)
135856
+ });
135857
+ const json = await resp.json();
135858
+ if (!resp.ok)
135859
+ throw OmniApiError.from(json, resp.status);
135860
+ if (!json?.data)
135861
+ throw new OmniApiError("Failed to attach membership", "ATTACH_FAILED", undefined, resp.status);
135862
+ return json.data;
135863
+ },
135864
+ async disable(tenantId, membershipId, reason) {
135865
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${tenantId}/memberships/${membershipId}/disable`, {
135866
+ method: "POST",
135867
+ headers: { "Content-Type": "application/json" },
135868
+ body: JSON.stringify({ reason })
135869
+ });
135870
+ const json = await resp.json();
135871
+ if (!resp.ok)
135872
+ throw OmniApiError.from(json, resp.status);
135873
+ if (!json?.data)
135874
+ throw new OmniApiError("Failed to disable membership", "DISABLE_FAILED", undefined, resp.status);
135875
+ return json.data;
135876
+ },
135877
+ async setStatus(tenantId, membershipId, status, reason) {
135878
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${tenantId}/memberships/${membershipId}/status`, {
135879
+ method: "POST",
135880
+ headers: { "Content-Type": "application/json" },
135881
+ body: JSON.stringify({ status, reason })
135882
+ });
135883
+ const json = await resp.json();
135884
+ if (!resp.ok)
135885
+ throw OmniApiError.from(json, resp.status);
135886
+ if (!json?.data)
135887
+ throw new OmniApiError("Failed to set membership status", "STATUS_FAILED", undefined, resp.status);
135888
+ return json.data;
135889
+ },
135890
+ async setRole(tenantId, membershipId, role, reason) {
135891
+ const resp = await apiFetch(`${baseUrl}/api/v2/platform/tenants/${tenantId}/memberships/${membershipId}/role`, {
135892
+ method: "POST",
135893
+ headers: { "Content-Type": "application/json" },
135894
+ body: JSON.stringify({ role, reason })
135895
+ });
135896
+ const json = await resp.json();
135897
+ if (!resp.ok)
135898
+ throw OmniApiError.from(json, resp.status);
135899
+ if (!json?.data)
135900
+ throw new OmniApiError("Failed to set membership role", "ROLE_FAILED", undefined, resp.status);
135901
+ return json.data;
135902
+ }
135903
+ }
135904
+ }
135905
+ },
135780
135906
  context: {
135781
135907
  async get() {
135782
135908
  const resp = await apiFetch(`${baseUrl}/api/v2/context`, {});
@@ -136392,7 +136518,7 @@ import { fileURLToPath } from "url";
136392
136518
  // package.json
136393
136519
  var package_default = {
136394
136520
  name: "@automagik/omni",
136395
- version: "2.260907.5",
136521
+ version: "2.260907.6",
136396
136522
  repository: {
136397
136523
  type: "git",
136398
136524
  url: "git+https://github.com/automagik-dev/omni.git",
@@ -137598,6 +137724,20 @@ function credentialStatusFields(auth) {
137598
137724
  delegationDepth: credential.delegationDepth
137599
137725
  };
137600
137726
  }
137727
+ function serverPostureFields(auth) {
137728
+ const posture = auth.server;
137729
+ if (!posture)
137730
+ return {};
137731
+ const fields = {
137732
+ serverMultitenancy: posture.multitenancyEnabled ? "enabled" : "disabled",
137733
+ serverControlPlane: posture.controlPlaneMounted ? "mounted" : "not mounted",
137734
+ serverDbEnforcement: posture.dbEnforcement
137735
+ };
137736
+ if (posture.multitenancyEnabled && posture.dbEnforcement === "legacy") {
137737
+ fields.tenancyWarning = "isolation not enforced: multitenancy is enabled but DB enforcement is legacy \u2014 the tenant boundary is advisory only (migration state)";
137738
+ }
137739
+ return fields;
137740
+ }
137601
137741
 
137602
137742
  // src/commands/auth.ts
137603
137743
  init_output();
@@ -138292,6 +138432,7 @@ one, or the entry named by the global flag:
138292
138432
  keyPrefix: result.keyPrefix,
138293
138433
  scopes: result.scopes,
138294
138434
  ...credentialStatusFields(result),
138435
+ ...serverPostureFields(result),
138295
138436
  configDir: getConfigDir()
138296
138437
  });
138297
138438
  } catch (err) {
@@ -146496,6 +146637,30 @@ async function handleCloseContact(options) {
146496
146637
  }
146497
146638
  }
146498
146639
 
146640
+ // src/commands/multitenancy.ts
146641
+ init_output();
146642
+ function createMultitenancyCommand() {
146643
+ const multitenancy = new Command("multitenancy").description("Multitenancy posture (read-only)");
146644
+ multitenancy.command("status").description("Show the server's multitenancy flag, control plane, and DB enforcement posture").action(async () => {
146645
+ const client = getClient();
146646
+ try {
146647
+ const result = await client.auth.validate();
146648
+ if (!result.server) {
146649
+ data({
146650
+ posture: "unreported",
146651
+ note: "This server does not report tenancy posture \u2014 it predates posture reporting " + "(and likely the tenant control plane). Platform commands will 404 regardless of flags. " + "Upgrade the server to get posture visibility."
146652
+ });
146653
+ return;
146654
+ }
146655
+ data(serverPostureFields(result));
146656
+ } catch (err2) {
146657
+ const message2 = err2 instanceof Error ? err2.message : "Unknown error";
146658
+ error(`Failed to read server posture: ${message2}`, undefined, 2);
146659
+ }
146660
+ });
146661
+ return multitenancy;
146662
+ }
146663
+
146499
146664
  // src/commands/music.ts
146500
146665
  import { writeFileSync as writeFileSync9 } from "fs";
146501
146666
  import { resolve as resolvePath2 } from "path";
@@ -149603,6 +149768,7 @@ async function validateAuthKey(statusInfo, client) {
149603
149768
  statusInfo.keyPrefix = auth.keyPrefix;
149604
149769
  statusInfo.scopes = auth.scopes;
149605
149770
  Object.assign(statusInfo, credentialStatusFields(auth));
149771
+ Object.assign(statusInfo, serverPostureFields(auth));
149606
149772
  } catch {
149607
149773
  statusInfo.keyValid = false;
149608
149774
  }
@@ -149674,6 +149840,233 @@ function createStopCommand() {
149674
149840
  return new Command("stop").description("Stop all omni PM2 processes").action(runStop);
149675
149841
  }
149676
149842
 
149843
+ // src/commands/tenants.ts
149844
+ init_output();
149845
+ var TENANT_ROLES = ["tenant-owner", "tenant-admin", "tenant-operator", "tenant-viewer"];
149846
+ var MEMBERSHIP_STATUSES = ["active", "disabled"];
149847
+ var REASON_REQUIRED_HINT = "Every platform control-plane call is audited; " + 'provide a justification of 3-500 characters, e.g. --reason "onboarding request OPS-1421".';
149848
+ var CONTROL_PLANE_UNAVAILABLE = "the platform control plane is not available on this server. " + "Either multitenancy is disabled (start the API with OMNI_MULTITENANCY_ENABLED=true) " + "or the server predates the tenant control plane \u2014 upgrade it.";
149849
+ var CREDENTIAL_REJECTED_401 = "the server rejected the credential (401). " + "The platform control plane accepts only PLATFORM-class credentials \u2014 legacy admin/god keys and " + "tenant data-plane keys are denied by design. Authenticate with a platform key: " + "omni auth login --api-key <platform-key>.";
149850
+ var CREDENTIAL_FORBIDDEN_403 = "the credential authenticated but is not allowed (403). " + "A PLATFORM-class credential with the matching platform:* scope is required \u2014 tenant-class " + "credentials can never reach the control plane, and platform keys act only within their scopes.";
149851
+ function requireReason(reason, action) {
149852
+ const trimmed = reason?.trim() ?? "";
149853
+ if (trimmed.length < 3 || trimmed.length > 500) {
149854
+ error(`--reason is required to ${action}. ${REASON_REQUIRED_HINT}`, undefined, 1);
149855
+ }
149856
+ return trimmed;
149857
+ }
149858
+ function parseRole(value) {
149859
+ if (!TENANT_ROLES.includes(value)) {
149860
+ error(`Invalid --role "${value}". Valid roles: ${TENANT_ROLES.join(", ")}`, undefined, 1);
149861
+ }
149862
+ return value;
149863
+ }
149864
+ function parseStatus(value) {
149865
+ if (!MEMBERSHIP_STATUSES.includes(value)) {
149866
+ error(`Invalid --status "${value}". Valid statuses: ${MEMBERSHIP_STATUSES.join(", ")}`, undefined, 1);
149867
+ }
149868
+ return value;
149869
+ }
149870
+ function renderPlatformError(err2, action) {
149871
+ if (err2 instanceof OmniApiError) {
149872
+ if (err2.status === 404 && err2.message.startsWith("Endpoint not found")) {
149873
+ error(`Failed to ${action}: ${CONTROL_PLANE_UNAVAILABLE}`, undefined, 1);
149874
+ }
149875
+ if (err2.status === 401) {
149876
+ error(`Failed to ${action}: ${CREDENTIAL_REJECTED_401}`, undefined, 1);
149877
+ }
149878
+ if (err2.status === 403) {
149879
+ error(`Failed to ${action}: ${CREDENTIAL_FORBIDDEN_403}`, undefined, 1);
149880
+ }
149881
+ }
149882
+ const message2 = err2 instanceof Error ? err2.message : "Unknown error";
149883
+ error(`Failed to ${action}: ${message2}`);
149884
+ }
149885
+ function formatTenantRow(tenant) {
149886
+ return {
149887
+ id: tenant.id,
149888
+ slug: tenant.slug,
149889
+ name: tenant.displayName,
149890
+ status: tenant.status,
149891
+ keyTtl: tenant.maxKeyTtlSeconds,
149892
+ keyRate: tenant.maxKeyRateLimit,
149893
+ keyBudget: tenant.maxKeyBudget,
149894
+ created: new Date(tenant.createdAt).toLocaleDateString()
149895
+ };
149896
+ }
149897
+ function formatMembershipRow(membership) {
149898
+ return {
149899
+ id: membership.id,
149900
+ principal: membership.principalId,
149901
+ role: membership.role,
149902
+ status: membership.status,
149903
+ invitedBy: membership.invitedByPrincipalId ?? "-",
149904
+ created: new Date(membership.createdAt).toLocaleDateString()
149905
+ };
149906
+ }
149907
+ async function handleList5(client, options3) {
149908
+ const reason = requireReason(options3.reason, "list tenants");
149909
+ const result = await client.platform.tenants.list(reason);
149910
+ list(result.items.map(formatTenantRow), {
149911
+ emptyMessage: "No tenants found.",
149912
+ rawData: result.items
149913
+ });
149914
+ }
149915
+ async function handleGet3(client, id, options3) {
149916
+ const reason = requireReason(options3.reason, "read a tenant");
149917
+ const tenant = await client.platform.tenants.get(id, reason);
149918
+ data(tenant);
149919
+ }
149920
+ async function handleCreate4(client, options3) {
149921
+ const reason = requireReason(options3.reason, "create a tenant");
149922
+ const tenant = await client.platform.tenants.create({
149923
+ slug: options3.slug,
149924
+ displayName: options3.name,
149925
+ maxKeyTtlSeconds: options3.maxKeyTtl,
149926
+ maxKeyRateLimit: options3.maxKeyRate,
149927
+ maxKeyBudget: options3.maxKeyBudget,
149928
+ reason
149929
+ });
149930
+ success(`Tenant created: ${tenant.slug} (${tenant.id})`);
149931
+ data(tenant);
149932
+ }
149933
+ async function handleSuspend(client, id, options3) {
149934
+ const reason = requireReason(options3.reason, "suspend a tenant");
149935
+ const tenant = await client.platform.tenants.suspend(id, reason);
149936
+ success(`Tenant suspended: ${tenant.slug} (${tenant.id})`);
149937
+ info(`Reason: ${reason}`);
149938
+ }
149939
+ async function handleArchive(client, id, options3) {
149940
+ const reason = requireReason(options3.reason, "archive a tenant");
149941
+ const tenant = await client.platform.tenants.archive(id, reason);
149942
+ success(`Tenant archived: ${tenant.slug} (${tenant.id})`);
149943
+ warn("Archived is terminal \u2014 there is no un-archive and no hard delete.");
149944
+ info(`Reason: ${reason}`);
149945
+ }
149946
+ async function handleMembershipsList(client, tenantId, options3) {
149947
+ const reason = requireReason(options3.reason, "list memberships");
149948
+ const result = await client.platform.tenants.memberships.list(tenantId, reason);
149949
+ list(result.items.map(formatMembershipRow), {
149950
+ emptyMessage: "No memberships found.",
149951
+ rawData: result.items
149952
+ });
149953
+ }
149954
+ async function handleMembershipsAdd(client, tenantId, options3) {
149955
+ const reason = requireReason(options3.reason, "attach a membership");
149956
+ const role = parseRole(options3.role);
149957
+ const membership = await client.platform.tenants.memberships.attach(tenantId, {
149958
+ principalId: options3.principal,
149959
+ role,
149960
+ reason
149961
+ });
149962
+ success(`Membership attached: ${membership.principalId} as ${membership.role} (${membership.id})`);
149963
+ }
149964
+ async function handleMembershipsDisable(client, tenantId, membershipId, options3) {
149965
+ const reason = requireReason(options3.reason, "disable a membership");
149966
+ const membership = await client.platform.tenants.memberships.disable(tenantId, membershipId, reason);
149967
+ success(`Membership disabled: ${membership.id}`);
149968
+ info(`Reason: ${reason}`);
149969
+ }
149970
+ async function handleMembershipsStatus(client, tenantId, membershipId, options3) {
149971
+ const reason = requireReason(options3.reason, "set a membership status");
149972
+ const status = parseStatus(options3.status);
149973
+ const membership = await client.platform.tenants.memberships.setStatus(tenantId, membershipId, status, reason);
149974
+ success(`Membership ${membership.id} status set to ${membership.status}`);
149975
+ }
149976
+ async function handleMembershipsRole(client, tenantId, membershipId, options3) {
149977
+ const reason = requireReason(options3.reason, "set a membership role");
149978
+ const role = parseRole(options3.role);
149979
+ const membership = await client.platform.tenants.memberships.setRole(tenantId, membershipId, role, reason);
149980
+ success(`Membership ${membership.id} role set to ${membership.role}`);
149981
+ }
149982
+ var REASON_HELP = "Audited justification (required, 3-500 chars)";
149983
+ function createTenantsCommand() {
149984
+ const tenants2 = new Command("tenants").description("Platform tenant control plane (requires a PLATFORM-class credential and OMNI_MULTITENANCY_ENABLED=true)");
149985
+ tenants2.command("list").description("List tenants, newest first (the read itself is audited)").option("--reason <text>", REASON_HELP).action(async (options3) => {
149986
+ const client = getClient();
149987
+ try {
149988
+ await handleList5(client, options3);
149989
+ } catch (err2) {
149990
+ renderPlatformError(err2, "list tenants");
149991
+ }
149992
+ });
149993
+ tenants2.command("get <id>").description("Get one tenant by id").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
149994
+ const client = getClient();
149995
+ try {
149996
+ await handleGet3(client, id, options3);
149997
+ } catch (err2) {
149998
+ renderPlatformError(err2, "get the tenant");
149999
+ }
150000
+ });
150001
+ tenants2.command("create").description("Create a tenant with its mandatory credential ceilings").requiredOption("--slug <slug>", "Immutable lowercase DNS-like slug (e.g. acme)").requiredOption("--name <name>", "Human-readable tenant name").requiredOption("--max-key-ttl <seconds>", "Ceiling for credential TTL, in seconds", Number.parseInt).requiredOption("--max-key-rate <n>", "Ceiling for credential rate limit", Number.parseInt).requiredOption("--max-key-budget <n>", "Ceiling for credential budget", Number.parseInt).option("--reason <text>", REASON_HELP).action(async (options3) => {
150002
+ const client = getClient();
150003
+ try {
150004
+ await handleCreate4(client, options3);
150005
+ } catch (err2) {
150006
+ renderPlatformError(err2, "create the tenant");
150007
+ }
150008
+ });
150009
+ tenants2.command("suspend <id>").description("Suspend a tenant (bumps its revocation epoch, invalidating tenant credentials)").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
150010
+ const client = getClient();
150011
+ try {
150012
+ await handleSuspend(client, id, options3);
150013
+ } catch (err2) {
150014
+ renderPlatformError(err2, "suspend the tenant");
150015
+ }
150016
+ });
150017
+ tenants2.command("archive <id>").description("Archive a tenant (TERMINAL \u2014 no un-archive, no hard delete)").option("--reason <text>", REASON_HELP).action(async (id, options3) => {
150018
+ const client = getClient();
150019
+ try {
150020
+ await handleArchive(client, id, options3);
150021
+ } catch (err2) {
150022
+ renderPlatformError(err2, "archive the tenant");
150023
+ }
150024
+ });
150025
+ const memberships = new Command("memberships").description("Manage tenant memberships");
150026
+ memberships.command("list <tenant-id>").description("List the memberships of one tenant (the read itself is audited)").option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
150027
+ const client = getClient();
150028
+ try {
150029
+ await handleMembershipsList(client, tenantId, options3);
150030
+ } catch (err2) {
150031
+ renderPlatformError(err2, "list memberships");
150032
+ }
150033
+ });
150034
+ memberships.command("add <tenant-id>").description("Grant a principal a role in the tenant").requiredOption("--principal <uuid>", "Principal to grant access to").requiredOption("--role <role>", `Tenant role: ${TENANT_ROLES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, options3) => {
150035
+ const client = getClient();
150036
+ try {
150037
+ await handleMembershipsAdd(client, tenantId, options3);
150038
+ } catch (err2) {
150039
+ renderPlatformError(err2, "attach the membership");
150040
+ }
150041
+ });
150042
+ memberships.command("disable <tenant-id> <membership-id>").description("Disable a membership (memberships are never hard-deleted)").option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
150043
+ const client = getClient();
150044
+ try {
150045
+ await handleMembershipsDisable(client, tenantId, membershipId, options3);
150046
+ } catch (err2) {
150047
+ renderPlatformError(err2, "disable the membership");
150048
+ }
150049
+ });
150050
+ memberships.command("status <tenant-id> <membership-id>").description("Activate or disable an existing membership").requiredOption("--status <status>", `New status: ${MEMBERSHIP_STATUSES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
150051
+ const client = getClient();
150052
+ try {
150053
+ await handleMembershipsStatus(client, tenantId, membershipId, options3);
150054
+ } catch (err2) {
150055
+ renderPlatformError(err2, "set the membership status");
150056
+ }
150057
+ });
150058
+ memberships.command("role <tenant-id> <membership-id>").description("Change the role an existing membership acts under").requiredOption("--role <role>", `New role: ${TENANT_ROLES.join(" | ")}`).option("--reason <text>", REASON_HELP).action(async (tenantId, membershipId, options3) => {
150059
+ const client = getClient();
150060
+ try {
150061
+ await handleMembershipsRole(client, tenantId, membershipId, options3);
150062
+ } catch (err2) {
150063
+ renderPlatformError(err2, "set the membership role");
150064
+ }
150065
+ });
150066
+ tenants2.addCommand(memberships);
150067
+ return tenants2;
150068
+ }
150069
+
149677
150070
  // src/commands/trust.ts
149678
150071
  import { hostname as osHostname } from "os";
149679
150072
  init_config();
@@ -149733,7 +150126,7 @@ function formatHostRow(host) {
149733
150126
  status: host.revokedAt ? "revoked" : "active"
149734
150127
  };
149735
150128
  }
149736
- async function handleList5() {
150129
+ async function handleList6() {
149737
150130
  try {
149738
150131
  const { items } = await callApi("GET", "/hosts");
149739
150132
  list(items.map(formatHostRow), {
@@ -149744,7 +150137,7 @@ async function handleList5() {
149744
150137
  error(`Failed to list genie hosts: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
149745
150138
  }
149746
150139
  }
149747
- async function handleGet3(id) {
150140
+ async function handleGet4(id) {
149748
150141
  try {
149749
150142
  const { data: data2 } = await callApi("GET", `/hosts/${encodeURIComponent(id)}`);
149750
150143
  data(data2);
@@ -149847,8 +150240,8 @@ Signing is per-server: only servers you have handshaken against recognize this
149847
150240
  keypair, so calls to other entries stay bearer-only. Bind another one with:
149848
150241
  omni trust handshake --server <name>
149849
150242
  `).option("--rotate", "Issue a new keypair even if one already exists (unbinds every previously bound server)").option("--hostname <name>", "Override the hostname reported to omni (defaults to os.hostname())").action(handleHandshake);
149850
- trust.command("list").description("List active (non-revoked) genie hosts").action(handleList5);
149851
- trust.command("get <id>").description("Show details for one genie host (active or revoked)").action(handleGet3);
150243
+ trust.command("list").description("List active (non-revoked) genie hosts").action(handleList6);
150244
+ trust.command("get <id>").description("Show details for one genie host (active or revoked)").action(handleGet4);
149852
150245
  trust.command("update <id>").description("Replace a genie host scopes (comma-separated)").requiredOption("--scope <list>", 'Comma-separated scope list, e.g. "agents:write,providers:write"').action(handleUpdate3);
149853
150246
  trust.command("revoke <id>").description("Revoke a genie host (irreversible \u2014 re-register with a fresh keypair to restore trust)").action(handleRevoke2);
149854
150247
  return trust;
@@ -151718,6 +152111,12 @@ var COMMANDS = [
151718
152111
  helpGroup: "Management",
151719
152112
  helpDescription: "API key management"
151720
152113
  },
152114
+ {
152115
+ create: createTenantsCommand,
152116
+ category: "advanced",
152117
+ helpGroup: "Management",
152118
+ helpDescription: "Platform tenant control plane (multitenancy)"
152119
+ },
151721
152120
  {
151722
152121
  create: createTrustCommand,
151723
152122
  category: "advanced",
@@ -151756,6 +152155,12 @@ var COMMANDS = [
151756
152155
  },
151757
152156
  { create: createEventsCommand, category: "standard", helpGroup: "System", helpDescription: "Query message history" },
151758
152157
  { create: createAuthCommand, category: "core", helpGroup: "System", helpDescription: "Authentication management" },
152158
+ {
152159
+ create: createMultitenancyCommand,
152160
+ category: "advanced",
152161
+ helpGroup: "System",
152162
+ helpDescription: "Server multitenancy posture (read-only)"
152163
+ },
151759
152164
  {
151760
152165
  create: createServerCommand,
151761
152166
  category: "core",
@@ -25,4 +25,22 @@ import type { AuthValidateResponse } from '@omni/sdk';
25
25
  * the legacy projection the surrounding output already prints.
26
26
  */
27
27
  export declare function credentialStatusFields(auth: AuthValidateResponse): Record<string, unknown>;
28
+ /**
29
+ * Project the server's tenancy posture into display fields (issue #982).
30
+ *
31
+ * Same empty-object contract as above, for the same reason: a server that does
32
+ * not report posture — any server predating it — must add NOTHING, so output
33
+ * against an old server stays field-for-field what it is today. "Absent" is
34
+ * itself the operator's signal that the server predates the control plane.
35
+ *
36
+ * Field names are prefixed `server*` so they cannot collide with the
37
+ * credential projection or the existing status fields (`omni auth status`
38
+ * already prints a `server` key naming the config entry).
39
+ *
40
+ * The `tenancyWarning` field mirrors `mixedTenancyStateWarning` semantics on
41
+ * the API side: it appears exactly when the flag is on but the database is in
42
+ * legacy enforcement — the advisory-only migration state — and never in the
43
+ * three coherent combinations.
44
+ */
45
+ export declare function serverPostureFields(auth: AuthValidateResponse): Record<string, unknown>;
28
46
  //# sourceMappingURL=credential-status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credential-status.d.ts","sourceRoot":"","sources":["../../src/lib/credential-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAEtD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgB1F"}
1
+ {"version":3,"file":"credential-status.d.ts","sourceRoot":"","sources":["../../src/lib/credential-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAEtD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgB1F;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgBvF"}