Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/components/layout/auth-gate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ export const AuthGate = ({ children }: { children: ReactNode }) => {
return <>{children}</>;
}

if (status === "denied") {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-6 bg-background">
<Wordmark className="text-2xl" />
<div className="flex flex-col items-center gap-2 font-mono text-sm text-muted-foreground">
<span className="text-destructive">No valid operator certificate</span>
<span>Install an operator certificate issued by this fleet&apos;s PKI to continue.</span>
</div>
</div>
);
}

return (
<div className="flex min-h-screen flex-col items-center justify-center gap-6 bg-background">
<Wordmark className="text-2xl" />
Expand Down
64 changes: 64 additions & 0 deletions src/context/auth.test.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
{status}:{operator?.level ?? "none"}
</div>
);
};

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(
<AuthProvider>
<Probe />
</AuthProvider>,
);
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(
<AuthProvider>
<Probe />
</AuthProvider>,
);
await waitFor(() => expect(screen.getByText("denied:none")).toBeInTheDocument());
});
});
57 changes: 45 additions & 12 deletions src/context/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,18 +49,45 @@ const AuthContext = createContext<AuthState | undefined>(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<AuthState>({ 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 <AuthContext.Provider value={state}>{children}</AuthContext.Provider>;
Expand Down
82 changes: 74 additions & 8 deletions src/gen/fleet/cryptos/fleet/v1/fleet_pb.ts
Original file line number Diff line number Diff line change
@@ -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 */
Expand All @@ -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
Expand Down Expand Up @@ -683,6 +676,68 @@ export type EnrollmentRequest = Message<"cryptos.fleet.v1.EnrollmentRequest"> &
export const EnrollmentRequestSchema: GenMessage<EnrollmentRequest> = /*@__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<WhoAmIRequest> = /*@__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<OperatorIdentity> = /*@__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<WhoAmIResponse> = /*@__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.
Expand Down Expand Up @@ -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);

1 change: 1 addition & 0 deletions src/lib/fleet/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }),
}),
);
Loading