diff --git a/src/components/layout/auth-gate.tsx b/src/components/layout/auth-gate.tsx
index 43112ef..0de8504 100644
--- a/src/components/layout/auth-gate.tsx
+++ b/src/components/layout/auth-gate.tsx
@@ -31,6 +31,18 @@ export const AuthGate = ({ children }: { children: ReactNode }) => {
return <>{children}>;
}
+ if (status === "denied") {
+ return (
+
diff --git a/src/context/auth.test.tsx b/src/context/auth.test.tsx
new file mode 100644
index 0000000..d3d2961
--- /dev/null
+++ b/src/context/auth.test.tsx
@@ -0,0 +1,64 @@
+/*
+Apache License 2.0
+
+Copyright 2026 Shane
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { render, screen, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { AuthProvider, useAuth } from "@/context/auth";
+
+const whoAmI = vi.fn();
+vi.mock("@/lib/fleet/client", () => ({ fleetClient: () => ({ whoAmI }) }));
+vi.mock("@/lib/fleet/mode", () => ({ fleetMode: () => "live" }));
+
+const Probe = () => {
+ const { operator, status } = useAuth();
+ return (
+
+ {status}:{operator?.level ?? "none"}
+
+ );
+};
+
+describe("AuthProvider (live)", () => {
+ beforeEach(() => whoAmI.mockReset());
+
+ it("populates operator + level from WhoAmI", async () => {
+ whoAmI.mockResolvedValue({
+ operator: { cn: "op@acme.example", level: "admin", serial: "0A:BC" },
+ });
+ render(
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByText("authenticated:admin")).toBeInTheDocument());
+ });
+
+ it("goes denied when WhoAmI returns no operator", async () => {
+ // The manager withholds an operator identity when the presented cert is not
+ // a valid operator; the provider renders the denied gate. (The parallel
+ // rejected-RPC path runs the same setState in the provider's .catch.)
+ whoAmI.mockResolvedValue({ operator: undefined });
+ render(
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByText("denied:none")).toBeInTheDocument());
+ });
+});
diff --git a/src/context/auth.tsx b/src/context/auth.tsx
index 64ac410..05bcd0a 100644
--- a/src/context/auth.tsx
+++ b/src/context/auth.tsx
@@ -18,22 +18,28 @@ limitations under the License.
import { createContext, type ReactNode, useContext, useEffect, useState } from "react";
-// Auth-gate stub for browser-side mTLS. In production the operator authenticates
-// with a smart-card / YubiKey-backed client certificate presented by the browser
-// during the TLS handshake; there is no password login. A dev browser cannot
-// perform smart-card mTLS, so this stub resolves to a fixed operator identity
-// after a short delay to exercise the gate. Wiring the real client-cert handshake
-// (and reading the verified subject the manager reports back) is a later concern.
+import { fleetClient } from "@/lib/fleet/client";
+import { fleetMode } from "@/lib/fleet/mode";
-export type AuthStatus = "authenticated" | "presenting";
+// Auth-gate for browser-side mTLS. In production the operator authenticates
+// with a smart-card / YubiKey-backed client certificate presented by the browser
+// during the TLS handshake; there is no password login. The manager verifies
+// the handshake and reports the operator's identity back through WhoAmI. A dev
+// browser cannot perform smart-card mTLS, so the mock data source keeps a fixed
+// operator identity to exercise the gate without a real certificate.
+export type AuthStatus = "authenticated" | "denied" | "presenting";
export interface Operator {
/** Common name from the operator's client certificate subject. */
commonName: string;
+ /** Authorization level the manager assigned to this operator. */
+ level: OperatorLevel;
/** Certificate serial the manager verified during the mTLS handshake. */
serial: string;
}
+export type OperatorLevel = "admin" | "operator" | "viewer";
+
interface AuthState {
operator: null | Operator;
status: AuthStatus;
@@ -43,18 +49,45 @@ const AuthContext = createContext
(undefined);
const DEV_OPERATOR: Operator = {
commonName: "operator@acme.example",
+ level: "admin",
serial: "3A:7F:0C:91:D2:44:8B:1E",
};
+const toLevel = (s: string): OperatorLevel => (s === "admin" || s === "operator" ? s : "viewer");
+
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = useState({ operator: null, status: "presenting" });
useEffect(() => {
- // Simulate the handshake resolving to a verified operator identity.
- const timer = setTimeout(() => {
- setState({ operator: DEV_OPERATOR, status: "authenticated" });
- }, 600);
- return () => clearTimeout(timer);
+ if (fleetMode() === "mock") {
+ // Simulate the handshake resolving to a verified operator identity.
+ const timer = setTimeout(
+ () => setState({ operator: DEV_OPERATOR, status: "authenticated" }),
+ 600,
+ );
+ return () => clearTimeout(timer);
+ }
+ let cancelled = false;
+ void fleetClient()
+ .whoAmI({})
+ .then((resp) => {
+ if (cancelled) return;
+ const op = resp.operator;
+ if (!op) {
+ setState({ operator: null, status: "denied" });
+ return;
+ }
+ setState({
+ operator: { commonName: op.cn, level: toLevel(op.level), serial: op.serial },
+ status: "authenticated",
+ });
+ })
+ .catch(() => {
+ if (!cancelled) setState({ operator: null, status: "denied" });
+ });
+ return () => {
+ cancelled = true;
+ };
}, []);
return {children};
diff --git a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
index 8a88b52..26801f4 100644
--- a/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
+++ b/src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
@@ -1,10 +1,3 @@
-// Bridge copy: generated in the api repo (gen/ts/cryptos/fleet/v1/fleet_pb.ts)
-// from proto/cryptos/fleet/v1/fleet.proto and copied here verbatim. A cross-repo
-// path alias can't resolve this file's own bare-specifier imports (its
-// node_modules lookup walks up from the api repo, not this one), so until the
-// api repo publishes this as a package, regenerate there and re-copy here on
-// any fleet.proto change.
-//
// @generated by protoc-gen-es v2.12.1 with parameter "target=ts"
// @generated from file cryptos/fleet/v1/fleet.proto (package cryptos.fleet.v1, syntax proto3)
/* eslint-disable */
@@ -17,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file cryptos/fleet/v1/fleet.proto.
*/
export const file_cryptos_fleet_v1_fleet: GenFile = /*@__PURE__*/
- fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ih4KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkiPQoPR2V0Tm9kZVJlc3BvbnNlEioKBG5vZGUYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk5vZGVEZXRhaWwiJwoXTGlzdENlcnRpZmljYXRlc1JlcXVlc3QSDAoEbm9kZRgBIAEoCSJPChhMaXN0Q2VydGlmaWNhdGVzUmVzcG9uc2USMwoMY2VydGlmaWNhdGVzGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0aWZpY2F0ZSIVChNMaXN0UHJvZmlsZXNSZXF1ZXN0IkQKFExpc3RQcm9maWxlc1Jlc3BvbnNlEiwKBWl0ZW1zGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0UHJvZmlsZSIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciISChBMaXN0QXVkaXRSZXF1ZXN0IkAKEUxpc3RBdWRpdFJlc3BvbnNlEisKBWl0ZW1zGAEgAygLMhwuY3J5cHRvcy5mbGVldC52MS5BdWRpdEV2ZW50IhgKFkxpc3RFbnJvbGxtZW50c1JlcXVlc3QiTQoXTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USMgoFaXRlbXMYASADKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0Iq8BCgtOb2RlU3VtbWFyeRIMCgRuYW1lGAEgASgJEg8KB2FkZHJlc3MYAiABKAkSDAoEcm9sZRgDIAEoCRIWCg5pZGVudGl0eV9zdGF0ZRgEIAEoCRIKCgJjbhgFIAEoCRIOCgZpc3N1ZXIYBiABKAkSKAoGaGVhbHRoGAcgASgOMhguY3J5cHRvcy5mbGVldC52MS5IZWFsdGgSFQoNaGVhbHRoX2RldGFpbBgIIAEoCSJJCgxOb2RlSWRlbnRpdHkSEQoJY2hhaW5fcGVtGAEgASgJEhEKCWNoYWluX2RlchgCIAMoDBITCgtsZWFmX3NoYTI1NhgDIAEoCSKpAQoKTm9kZURldGFpbBIuCgdzdW1tYXJ5GAEgASgLMh0uY3J5cHRvcy5mbGVldC52MS5Ob2RlU3VtbWFyeRIwCghpZGVudGl0eRgCIAEoCzIeLmNyeXB0b3MuZmxlZXQudjEuTm9kZUlkZW50aXR5EhUKDXRwbV9hdmFpbGFibGUYAyABKAgSEgoKYm9vdF9jb3VudBgEIAEoBBIOCgZ1cHRpbWUYBSABKAkiwAEKC0NlcnRpZmljYXRlEg4KBnNlcmlhbBgBIAEoCRISCgpzdWJqZWN0X2NuGAIgASgJEhMKC2lzc3Vlcl9ub2RlGAMgASgJEgwKBGtpbmQYBCABKAkSDgoGc3RhdHVzGAUgASgJEhIKCm5vdF9iZWZvcmUYBiABKAkSEQoJbm90X2FmdGVyGAcgASgJEg8KB3Byb2ZpbGUYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCRIOCgZyZWFzb24YCiABKAkinAEKC0NlcnRQcm9maWxlEgwKBG5hbWUYASABKAkSDwoHa2V5X2FsZxgCIAEoCRIRCglrZXlfdXNhZ2UYAyADKAkSFQoNZXh0X2tleV91c2FnZRgEIAMoCRINCgVpc19jYRgFIAEoCBIQCghwYXRoX2xlbhgGIAEoBRIMCgRzYW5zGAcgAygJEhUKDXZhbGlkaXR5X2RheXMYCCABKAUijQEKEUVucm9sbG1lbnRBZGFwdGVyEgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIPCgdwcm9maWxlGAQgASgJEg8KB2VuYWJsZWQYBSABKAgSEgoKY2hhbGxlbmdlcxgGIAMoCRIUCgxncG9fdGVtcGxhdGUYByABKAkibQoKQXVkaXRFdmVudBIKCgJpZBgBIAEoCRIKCgJhdBgCIAEoCRIMCgRraW5kGAMgASgJEg8KB3N1bW1hcnkYBCABKAkSEwoLdGFyZ2V0X2tpbmQYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkirAIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEhUKDXByb3Bvc2VkX25hbWUYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwYXJlbnRfY24YBCABKAkSDwoHYWRkcmVzcxgFIAEoCRIOCgZzdGF0dXMYBiABKAkSGwoTYXR0ZXN0YXRpb25fc3VtbWFyeRgHIAEoCRIbChNhdHRlc3RhdGlvbl9ub2RlX2lkGAggASgJEhQKDGNzcl9rZXlfdHlwZRgJIAEoCRIWCg5jc3Jfc3ViamVjdF9jbhgKIAEoCRIUCgxyZXF1ZXN0ZWRfYXQYCyABKAkSGAoQcmVqZWN0aW9uX3JlYXNvbhgMIAEoCRIaChJhZG1pdHRlZF9ub2RlX25hbWUYDSABKAkqUgoGSGVhbHRoEhYKEkhFQUxUSF9VTlNQRUNJRklFRBAAEg0KCUhFQUxUSF9VUBABEg8KC0hFQUxUSF9ET1dOEAISEAoMSEVBTFRIX0VSUk9SEAMymwUKDEZsZWV0U2VydmljZRJUCglMaXN0Tm9kZXMSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3ROb2Rlc1JlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3ROb2Rlc1Jlc3BvbnNlEk4KB0dldE5vZGUSIC5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXF1ZXN0GiEuY3J5cHRvcy5mbGVldC52MS5HZXROb2RlUmVzcG9uc2USaQoQTGlzdENlcnRpZmljYXRlcxIpLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1JlcXVlc3QaKi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RDZXJ0aWZpY2F0ZXNSZXNwb25zZRJdCgxMaXN0UHJvZmlsZXMSJS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RQcm9maWxlc1JlcXVlc3QaJi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RQcm9maWxlc1Jlc3BvbnNlEl0KDExpc3RBZGFwdGVycxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFkYXB0ZXJzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdEFkYXB0ZXJzUmVzcG9uc2USVAoJTGlzdEF1ZGl0EiIuY3J5cHRvcy5mbGVldC52MS5MaXN0QXVkaXRSZXF1ZXN0GiMuY3J5cHRvcy5mbGVldC52MS5MaXN0QXVkaXRSZXNwb25zZRJmCg9MaXN0RW5yb2xsbWVudHMSKC5jcnlwdG9zLmZsZWV0LnYxLkxpc3RFbnJvbGxtZW50c1JlcXVlc3QaKS5jcnlwdG9zLmZsZWV0LnYxLkxpc3RFbnJvbGxtZW50c1Jlc3BvbnNlQjhaNmdpdGh1Yi5jb20vQ3J5cHRPUy1QS0kvYXBpL2dvL2NyeXB0b3MvZmxlZXQvdjE7ZmxlZXR2MWIGcHJvdG8z");
+ fileDesc("ChxjcnlwdG9zL2ZsZWV0L3YxL2ZsZWV0LnByb3RvEhBjcnlwdG9zLmZsZWV0LnYxIhIKEExpc3ROb2Rlc1JlcXVlc3QiQQoRTGlzdE5vZGVzUmVzcG9uc2USLAoFbm9kZXMYASADKAsyHS5jcnlwdG9zLmZsZWV0LnYxLk5vZGVTdW1tYXJ5Ih4KDkdldE5vZGVSZXF1ZXN0EgwKBG5hbWUYASABKAkiPQoPR2V0Tm9kZVJlc3BvbnNlEioKBG5vZGUYASABKAsyHC5jcnlwdG9zLmZsZWV0LnYxLk5vZGVEZXRhaWwiJwoXTGlzdENlcnRpZmljYXRlc1JlcXVlc3QSDAoEbm9kZRgBIAEoCSJPChhMaXN0Q2VydGlmaWNhdGVzUmVzcG9uc2USMwoMY2VydGlmaWNhdGVzGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0aWZpY2F0ZSIVChNMaXN0UHJvZmlsZXNSZXF1ZXN0IkQKFExpc3RQcm9maWxlc1Jlc3BvbnNlEiwKBWl0ZW1zGAEgAygLMh0uY3J5cHRvcy5mbGVldC52MS5DZXJ0UHJvZmlsZSIVChNMaXN0QWRhcHRlcnNSZXF1ZXN0IkoKFExpc3RBZGFwdGVyc1Jlc3BvbnNlEjIKBWl0ZW1zGAEgAygLMiMuY3J5cHRvcy5mbGVldC52MS5FbnJvbGxtZW50QWRhcHRlciISChBMaXN0QXVkaXRSZXF1ZXN0IkAKEUxpc3RBdWRpdFJlc3BvbnNlEisKBWl0ZW1zGAEgAygLMhwuY3J5cHRvcy5mbGVldC52MS5BdWRpdEV2ZW50IhgKFkxpc3RFbnJvbGxtZW50c1JlcXVlc3QiTQoXTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USMgoFaXRlbXMYASADKAsyIy5jcnlwdG9zLmZsZWV0LnYxLkVucm9sbG1lbnRSZXF1ZXN0Iq8BCgtOb2RlU3VtbWFyeRIMCgRuYW1lGAEgASgJEg8KB2FkZHJlc3MYAiABKAkSDAoEcm9sZRgDIAEoCRIWCg5pZGVudGl0eV9zdGF0ZRgEIAEoCRIKCgJjbhgFIAEoCRIOCgZpc3N1ZXIYBiABKAkSKAoGaGVhbHRoGAcgASgOMhguY3J5cHRvcy5mbGVldC52MS5IZWFsdGgSFQoNaGVhbHRoX2RldGFpbBgIIAEoCSJJCgxOb2RlSWRlbnRpdHkSEQoJY2hhaW5fcGVtGAEgASgJEhEKCWNoYWluX2RlchgCIAMoDBITCgtsZWFmX3NoYTI1NhgDIAEoCSKpAQoKTm9kZURldGFpbBIuCgdzdW1tYXJ5GAEgASgLMh0uY3J5cHRvcy5mbGVldC52MS5Ob2RlU3VtbWFyeRIwCghpZGVudGl0eRgCIAEoCzIeLmNyeXB0b3MuZmxlZXQudjEuTm9kZUlkZW50aXR5EhUKDXRwbV9hdmFpbGFibGUYAyABKAgSEgoKYm9vdF9jb3VudBgEIAEoBBIOCgZ1cHRpbWUYBSABKAkiwAEKC0NlcnRpZmljYXRlEg4KBnNlcmlhbBgBIAEoCRISCgpzdWJqZWN0X2NuGAIgASgJEhMKC2lzc3Vlcl9ub2RlGAMgASgJEgwKBGtpbmQYBCABKAkSDgoGc3RhdHVzGAUgASgJEhIKCm5vdF9iZWZvcmUYBiABKAkSEQoJbm90X2FmdGVyGAcgASgJEg8KB3Byb2ZpbGUYCCABKAkSEgoKcmV2b2tlZF9hdBgJIAEoCRIOCgZyZWFzb24YCiABKAkinAEKC0NlcnRQcm9maWxlEgwKBG5hbWUYASABKAkSDwoHa2V5X2FsZxgCIAEoCRIRCglrZXlfdXNhZ2UYAyADKAkSFQoNZXh0X2tleV91c2FnZRgEIAMoCRINCgVpc19jYRgFIAEoCBIQCghwYXRoX2xlbhgGIAEoBRIMCgRzYW5zGAcgAygJEhUKDXZhbGlkaXR5X2RheXMYCCABKAUijQEKEUVucm9sbG1lbnRBZGFwdGVyEgwKBGtpbmQYASABKAkSDAoEbmFtZRgCIAEoCRIQCghlbmRwb2ludBgDIAEoCRIPCgdwcm9maWxlGAQgASgJEg8KB2VuYWJsZWQYBSABKAgSEgoKY2hhbGxlbmdlcxgGIAMoCRIUCgxncG9fdGVtcGxhdGUYByABKAkibQoKQXVkaXRFdmVudBIKCgJpZBgBIAEoCRIKCgJhdBgCIAEoCRIMCgRraW5kGAMgASgJEg8KB3N1bW1hcnkYBCABKAkSEwoLdGFyZ2V0X2tpbmQYBSABKAkSEwoLdGFyZ2V0X3BhdGgYBiABKAkirAIKEUVucm9sbG1lbnRSZXF1ZXN0EgoKAmlkGAEgASgJEhUKDXByb3Bvc2VkX25hbWUYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwYXJlbnRfY24YBCABKAkSDwoHYWRkcmVzcxgFIAEoCRIOCgZzdGF0dXMYBiABKAkSGwoTYXR0ZXN0YXRpb25fc3VtbWFyeRgHIAEoCRIbChNhdHRlc3RhdGlvbl9ub2RlX2lkGAggASgJEhQKDGNzcl9rZXlfdHlwZRgJIAEoCRIWCg5jc3Jfc3ViamVjdF9jbhgKIAEoCRIUCgxyZXF1ZXN0ZWRfYXQYCyABKAkSGAoQcmVqZWN0aW9uX3JlYXNvbhgMIAEoCRIaChJhZG1pdHRlZF9ub2RlX25hbWUYDSABKAkiDwoNV2hvQW1JUmVxdWVzdCI9ChBPcGVyYXRvcklkZW50aXR5EgoKAmNuGAEgASgJEg4KBnNlcmlhbBgCIAEoCRINCgVsZXZlbBgDIAEoCSJGCg5XaG9BbUlSZXNwb25zZRI0CghvcGVyYXRvchgBIAEoCzIiLmNyeXB0b3MuZmxlZXQudjEuT3BlcmF0b3JJZGVudGl0eSpSCgZIZWFsdGgSFgoSSEVBTFRIX1VOU1BFQ0lGSUVEEAASDQoJSEVBTFRIX1VQEAESDwoLSEVBTFRIX0RPV04QAhIQCgxIRUFMVEhfRVJST1IQAzLoBQoMRmxlZXRTZXJ2aWNlElQKCUxpc3ROb2RlcxIiLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVxdWVzdBojLmNyeXB0b3MuZmxlZXQudjEuTGlzdE5vZGVzUmVzcG9uc2USTgoHR2V0Tm9kZRIgLmNyeXB0b3MuZmxlZXQudjEuR2V0Tm9kZVJlcXVlc3QaIS5jcnlwdG9zLmZsZWV0LnYxLkdldE5vZGVSZXNwb25zZRJpChBMaXN0Q2VydGlmaWNhdGVzEikuY3J5cHRvcy5mbGVldC52MS5MaXN0Q2VydGlmaWNhdGVzUmVxdWVzdBoqLmNyeXB0b3MuZmxlZXQudjEuTGlzdENlcnRpZmljYXRlc1Jlc3BvbnNlEl0KDExpc3RQcm9maWxlcxIlLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVxdWVzdBomLmNyeXB0b3MuZmxlZXQudjEuTGlzdFByb2ZpbGVzUmVzcG9uc2USXQoMTGlzdEFkYXB0ZXJzEiUuY3J5cHRvcy5mbGVldC52MS5MaXN0QWRhcHRlcnNSZXF1ZXN0GiYuY3J5cHRvcy5mbGVldC52MS5MaXN0QWRhcHRlcnNSZXNwb25zZRJUCglMaXN0QXVkaXQSIi5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlcXVlc3QaIy5jcnlwdG9zLmZsZWV0LnYxLkxpc3RBdWRpdFJlc3BvbnNlEmYKD0xpc3RFbnJvbGxtZW50cxIoLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVxdWVzdBopLmNyeXB0b3MuZmxlZXQudjEuTGlzdEVucm9sbG1lbnRzUmVzcG9uc2USSwoGV2hvQW1JEh8uY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXF1ZXN0GiAuY3J5cHRvcy5mbGVldC52MS5XaG9BbUlSZXNwb25zZUI4WjZnaXRodWIuY29tL0NyeXB0T1MtUEtJL2FwaS9nby9jcnlwdG9zL2ZsZWV0L3YxO2ZsZWV0djFiBnByb3RvMw");
/**
* @generated from message cryptos.fleet.v1.ListNodesRequest
@@ -683,6 +676,68 @@ export type EnrollmentRequest = Message<"cryptos.fleet.v1.EnrollmentRequest"> &
export const EnrollmentRequestSchema: GenMessage = /*@__PURE__*/
messageDesc(file_cryptos_fleet_v1_fleet, 21);
+/**
+ * @generated from message cryptos.fleet.v1.WhoAmIRequest
+ */
+export type WhoAmIRequest = Message<"cryptos.fleet.v1.WhoAmIRequest"> & {
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.WhoAmIRequest.
+ * Use `create(WhoAmIRequestSchema)` to create a new message.
+ */
+export const WhoAmIRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 22);
+
+/**
+ * OperatorIdentity is the operator the manager verified from the mTLS client
+ * certificate presented on the management-plane connection.
+ *
+ * @generated from message cryptos.fleet.v1.OperatorIdentity
+ */
+export type OperatorIdentity = Message<"cryptos.fleet.v1.OperatorIdentity"> & {
+ /**
+ * @generated from field: string cn = 1;
+ */
+ cn: string;
+
+ /**
+ * @generated from field: string serial = 2;
+ */
+ serial: string;
+
+ /**
+ * access level: "viewer" | "operator" | "admin"
+ *
+ * @generated from field: string level = 3;
+ */
+ level: string;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.OperatorIdentity.
+ * Use `create(OperatorIdentitySchema)` to create a new message.
+ */
+export const OperatorIdentitySchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 23);
+
+/**
+ * @generated from message cryptos.fleet.v1.WhoAmIResponse
+ */
+export type WhoAmIResponse = Message<"cryptos.fleet.v1.WhoAmIResponse"> & {
+ /**
+ * @generated from field: cryptos.fleet.v1.OperatorIdentity operator = 1;
+ */
+ operator?: OperatorIdentity | undefined;
+};
+
+/**
+ * Describes the message cryptos.fleet.v1.WhoAmIResponse.
+ * Use `create(WhoAmIResponseSchema)` to create a new message.
+ */
+export const WhoAmIResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_cryptos_fleet_v1_fleet, 24);
+
/**
* Health reports the manager's view of node reachability, independent of
* the node's own reported identity state.
@@ -799,6 +854,17 @@ export const FleetService: GenService<{
input: typeof ListEnrollmentsRequestSchema;
output: typeof ListEnrollmentsResponseSchema;
},
+ /**
+ * WhoAmI echoes the operator identity the manager verified from the
+ * presented client certificate (subject CN, cert serial, access level).
+ *
+ * @generated from rpc cryptos.fleet.v1.FleetService.WhoAmI
+ */
+ whoAmI: {
+ methodKind: "unary";
+ input: typeof WhoAmIRequestSchema;
+ output: typeof WhoAmIResponseSchema;
+ },
}> = /*@__PURE__*/
serviceDesc(file_cryptos_fleet_v1_fleet, 0);
diff --git a/src/lib/fleet/client.ts b/src/lib/fleet/client.ts
index 9dd5d67..34d8e5b 100644
--- a/src/lib/fleet/client.ts
+++ b/src/lib/fleet/client.ts
@@ -30,5 +30,6 @@ export const fleetClient = () =>
FleetService,
createConnectTransport({
baseUrl: import.meta.env.VITE_FLEET_API ?? "http://localhost:8080",
+ fetch: (input, init) => fetch(input, { ...init, credentials: "include" }),
}),
);