refactor: remove server-global runtime trust
This commit is contained in:
@@ -373,14 +373,9 @@ export type RuntimeVerificationEvidenceSummary = {
|
||||
runtime_identity_revision: number;
|
||||
};
|
||||
|
||||
export type WorkspaceRuntimeAuthenticationMode =
|
||||
| "legacy_server_issuer"
|
||||
| "workspace_identity";
|
||||
|
||||
export type WorkspaceRuntimeBindingSummary = {
|
||||
state: WorkspaceRuntimeBindingState;
|
||||
connection_state: RuntimeConnectionDisplayState;
|
||||
authentication_mode: WorkspaceRuntimeAuthenticationMode;
|
||||
revision: number;
|
||||
workspace_key_id?: string | null;
|
||||
workspace_key_generation?: number | null;
|
||||
@@ -446,11 +441,6 @@ export type WorkspaceRuntimeDetail = {
|
||||
|
||||
export type RuntimeTrustKeyRevealResponse = { public_key: string };
|
||||
|
||||
export type PutRuntimeTrustKeyRequest = {
|
||||
public_key: string;
|
||||
expected_revision: number | null;
|
||||
};
|
||||
|
||||
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number };
|
||||
|
||||
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {
|
||||
CreateRemoteRuntimeRequest,
|
||||
Diagnostic,
|
||||
PutRuntimeTrustKeyRequest,
|
||||
RevokeRuntimeTrustKeyRequest,
|
||||
RuntimeConnectionDisplayState,
|
||||
RuntimeIdentityAuthority,
|
||||
@@ -17,7 +16,6 @@ import type {
|
||||
RuntimeTrustKeyState,
|
||||
RuntimeTrustKeyStatus,
|
||||
RuntimeVerificationEvidenceSummary,
|
||||
WorkspaceRuntimeAuthenticationMode,
|
||||
WorkspaceRuntimeBindingState,
|
||||
WorkspaceRuntimeBindingSummary,
|
||||
WorkspaceRuntimeDetail,
|
||||
@@ -85,11 +83,6 @@ const CONNECTION_STATES = new Set<RuntimeConnectionDisplayState>([
|
||||
"revoked",
|
||||
]);
|
||||
|
||||
const AUTHENTICATION_MODES = new Set<WorkspaceRuntimeAuthenticationMode>([
|
||||
"legacy_server_issuer",
|
||||
"workspace_identity",
|
||||
]);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
@@ -388,15 +381,10 @@ function runtimeBinding(
|
||||
const item = object(value, path);
|
||||
exactKeys(
|
||||
item,
|
||||
["state", "connection_state", "authentication_mode", "revision"],
|
||||
["state", "connection_state", "revision"],
|
||||
["workspace_key_id", "workspace_key_generation", "verification"],
|
||||
path,
|
||||
);
|
||||
const authenticationMode = enumValue(
|
||||
item.authentication_mode,
|
||||
`${path}.authentication_mode`,
|
||||
AUTHENTICATION_MODES,
|
||||
);
|
||||
const workspaceKeyId = optionalNullableString(
|
||||
item.workspace_key_id,
|
||||
`${path}.workspace_key_id`,
|
||||
@@ -406,22 +394,13 @@ function runtimeBinding(
|
||||
item.workspace_key_generation,
|
||||
`${path}.workspace_key_generation`,
|
||||
);
|
||||
const state = enumValue(item.state, `${path}.state`, BINDING_STATES);
|
||||
if (
|
||||
authenticationMode === "workspace_identity" &&
|
||||
state !== "revoked" &&
|
||||
(workspaceKeyId == null || workspaceKeyGeneration == null)
|
||||
) {
|
||||
return fail(path, "requires Workspace signing key identity metadata");
|
||||
}
|
||||
if (
|
||||
authenticationMode === "legacy_server_issuer" &&
|
||||
(workspaceKeyId != null || workspaceKeyGeneration != null)
|
||||
) {
|
||||
return fail(
|
||||
path,
|
||||
"must not attach Workspace key metadata to legacy authority",
|
||||
);
|
||||
}
|
||||
const state = enumValue(item.state, `${path}.state`, BINDING_STATES);
|
||||
const connectionState = enumValue(
|
||||
item.connection_state,
|
||||
`${path}.connection_state`,
|
||||
@@ -437,7 +416,6 @@ function runtimeBinding(
|
||||
return fail(path, "verification must match the current binding revision");
|
||||
}
|
||||
if (
|
||||
authenticationMode === "workspace_identity" &&
|
||||
connectionState === "verified" &&
|
||||
(verification === undefined ||
|
||||
verification.verified_at === null ||
|
||||
@@ -451,7 +429,6 @@ function runtimeBinding(
|
||||
return {
|
||||
state,
|
||||
connection_state: connectionState,
|
||||
authentication_mode: authenticationMode,
|
||||
revision,
|
||||
...(workspaceKeyId === undefined
|
||||
? {}
|
||||
@@ -988,29 +965,6 @@ export async function previewRuntimePublicKeyFingerprint(
|
||||
return `sha256:${hex}`;
|
||||
}
|
||||
|
||||
export async function putRuntimeTrustKey(
|
||||
workspaceId: string,
|
||||
runtimeId: string,
|
||||
request: PutRuntimeTrustKeyRequest,
|
||||
fetchImpl: typeof fetch = fetch,
|
||||
): Promise<WorkspaceRuntimeDetail> {
|
||||
const response = await fetchImpl(
|
||||
workspaceApiPath(
|
||||
workspaceId,
|
||||
`/runtimes/${encodeURIComponent(runtimeId)}/trust-key`,
|
||||
),
|
||||
{
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
public_key: request.public_key,
|
||||
expected_revision: revisionForJson(request.expected_revision),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return await finishMutation(response, workspaceId, runtimeId);
|
||||
}
|
||||
|
||||
export async function revokeRuntimeTrustKey(
|
||||
workspaceId: string,
|
||||
runtimeId: string,
|
||||
|
||||
+13
-23
@@ -1,14 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import type {
|
||||
PutRuntimeTrustKeyRequest,
|
||||
RevokeRuntimeTrustKeyRequest,
|
||||
RuntimeTrustKeyStatus,
|
||||
} from '$lib/generated/workspace-api';
|
||||
import {
|
||||
createRemoteRuntime,
|
||||
previewRuntimePublicKeyFingerprint,
|
||||
putRuntimeTrustKey,
|
||||
revealRuntimeTrustKey,
|
||||
revokeRuntimeTrustKey,
|
||||
RuntimeTrustConflictError,
|
||||
@@ -145,31 +143,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
const request: PutRuntimeTrustKeyRequest = {
|
||||
public_key: key,
|
||||
expected_revision: trust.revision ?? null,
|
||||
};
|
||||
|
||||
const operation = routeFence.capture(data.runtimeId);
|
||||
busyAction = 'save';
|
||||
try {
|
||||
const binding = data.runtimeDetail.runtime.management.binding;
|
||||
if (binding?.authentication_mode === 'workspace_identity') {
|
||||
if (!data.runtimeDetail.endpoint) {
|
||||
throw new RuntimeTrustRequestError('The authoritative Runtime endpoint is unavailable.');
|
||||
}
|
||||
await createRemoteRuntime(data.workspaceId, {
|
||||
public_bundle: {
|
||||
identity_id: operation.runtimeId,
|
||||
public_key: key,
|
||||
},
|
||||
display_name: data.runtimeDetail.runtime.label,
|
||||
endpoint: data.runtimeDetail.endpoint,
|
||||
expected_revision: binding.revision,
|
||||
});
|
||||
} else {
|
||||
await putRuntimeTrustKey(data.workspaceId, operation.runtimeId, request);
|
||||
if (!binding || !data.runtimeDetail.endpoint) {
|
||||
throw new RuntimeTrustRequestError(
|
||||
'The Workspace identity binding and authoritative Runtime endpoint are required.',
|
||||
);
|
||||
}
|
||||
await createRemoteRuntime(data.workspaceId, {
|
||||
public_bundle: {
|
||||
identity_id: operation.runtimeId,
|
||||
public_key: key,
|
||||
},
|
||||
display_name: data.runtimeDetail.runtime.label,
|
||||
endpoint: data.runtimeDetail.endpoint,
|
||||
expected_revision: binding.revision,
|
||||
});
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
publicKey = '';
|
||||
fingerprintConfirmation = '';
|
||||
@@ -333,7 +324,6 @@
|
||||
<div><dt>Endpoint</dt><dd>{detail.endpoint ?? 'Not configured'}</dd></div>
|
||||
<div><dt>Status</dt><dd>{runtime.status}</dd></div>
|
||||
<div><dt>Connection state</dt><dd>{runtime.management.binding?.connection_state ?? 'Not configured'}</dd></div>
|
||||
<div><dt>Authentication mode</dt><dd>{runtime.management.binding?.authentication_mode ?? '—'}</dd></div>
|
||||
<div><dt>Workspace signing key</dt><dd><code>{runtime.management.binding?.workspace_key_id ?? '—'}</code></dd></div>
|
||||
<div><dt>Verified</dt><dd>{formatTimestamp(runtime.management.binding?.verification?.verified_at)}</dd></div>
|
||||
<div><dt>Verified binding revision</dt><dd>{runtime.management.binding?.verification?.binding_revision?.toString() ?? '—'}</dd></div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
parseWorkspaceRuntimeDetail,
|
||||
parseWorkspaceRuntimeList,
|
||||
previewRuntimePublicKeyFingerprint,
|
||||
putRuntimeTrustKey,
|
||||
revokeRuntimeTrustKey,
|
||||
RuntimeTrustConflictError,
|
||||
RuntimeTrustRouteFence,
|
||||
@@ -43,7 +42,6 @@ function runtime() {
|
||||
binding: {
|
||||
state: "verified",
|
||||
connection_state: "verified",
|
||||
authentication_mode: "workspace_identity",
|
||||
revision: 3,
|
||||
workspace_key_id: "WK-1",
|
||||
workspace_key_generation: 1,
|
||||
@@ -279,52 +277,6 @@ Deno.test("Runtime public key preview matches the Server fingerprint contract",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("typed trust conflict is validated and preserves authoritative revision", async () => {
|
||||
let sentBody: unknown = null;
|
||||
const fetchImpl = ((_: RequestInfo | URL, init?: RequestInit) => {
|
||||
sentBody = JSON.parse(String(init?.body)) as unknown;
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
error: "stale_revision",
|
||||
message: "Runtime trust changed",
|
||||
current_revision: 4,
|
||||
current_fingerprint: "SHA256:new",
|
||||
}),
|
||||
{ status: 409, headers: { "content-type": "application/json" } },
|
||||
),
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
await putRuntimeTrustKey(
|
||||
"workspace-a",
|
||||
"arcadia",
|
||||
{ public_key: "ssh-ed25519 AAAA-new", expected_revision: 3 },
|
||||
fetchImpl,
|
||||
);
|
||||
throw new Error("expected mutation to reject");
|
||||
} catch (error) {
|
||||
assert(
|
||||
error instanceof RuntimeTrustConflictError,
|
||||
"expected typed conflict",
|
||||
);
|
||||
assert(
|
||||
error.conflict.current_revision === 4,
|
||||
"authoritative revision was lost",
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
JSON.stringify(sentBody) ===
|
||||
JSON.stringify({
|
||||
public_key: "ssh-ed25519 AAAA-new",
|
||||
expected_revision: 3,
|
||||
}),
|
||||
"request should serialize the generated bigint revision as a safe JSON integer",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime create surfaces bounded Settings error details", async () => {
|
||||
const fetchImpl = (() =>
|
||||
Promise.resolve(
|
||||
|
||||
Reference in New Issue
Block a user