From 5fd2ccf0840c7e6f723f8edc884416f177e671df Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 6 Sep 2026 05:50:29 +0900 Subject: [PATCH] fix: gate runtime key reveal and revoke confirmation --- crates/client/src/workspace_product.rs | 16 ++- crates/workspace-api/src/lib.rs | 18 +++- crates/workspace-server/src/server.rs | 86 +++++++++++----- .../src/lib/generated/workspace-api.ts | 3 +- .../lib/workspace/api/runtime-management.ts | 56 ++++++++--- .../src/lib/workspace/styles/settings.css | 12 ++- .../runtimes/[runtimeId]/+page.svelte | 97 +++++++++++++++---- .../tests/runtime-management-source.test.ts | 2 + .../tests/runtime-management.test.ts | 34 ++++++- 9 files changed, 253 insertions(+), 71 deletions(-) diff --git a/crates/client/src/workspace_product.rs b/crates/client/src/workspace_product.rs index bdc97f1f..99d72538 100644 --- a/crates/client/src/workspace_product.rs +++ b/crates/client/src/workspace_product.rs @@ -13,9 +13,9 @@ use workspace_api::{ CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse, ObjectiveCreateRequest, ObjectiveDetail, ObjectiveEditRequest, ObjectiveLinkTicketRequest, ObjectiveStateRequest, ObjectiveSummary, PutRuntimeTrustKeyRequest, - RevokeRuntimeTrustKeyRequest, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, - TICKET_RELATIONS_QUERY_PATH, WorkerLaunchOptionsResponse, WorkspaceRuntimeDetail, - WorkspaceRuntimeResource, + RevokeRuntimeTrustKeyRequest, RuntimeTrustKeyRevealResponse, + TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, + WorkerLaunchOptionsResponse, WorkspaceRuntimeDetail, WorkspaceRuntimeResource, }; use crate::{BackendApiClient, BackendWorkspaceClientError}; @@ -256,6 +256,16 @@ impl BackendWorkspaceProductClient { self.get_json(&format!("/runtimes/{}", encode_path_segment(runtime_id))) } + pub fn reveal_runtime_trust_key( + &self, + runtime_id: &str, + ) -> Result { + self.get_json(&format!( + "/runtimes/{}/trust-key", + encode_path_segment(runtime_id) + )) + } + pub fn put_runtime_trust_key( &self, runtime_id: &str, diff --git a/crates/workspace-api/src/lib.rs b/crates/workspace-api/src/lib.rs index 6cc62968..a2cf8f87 100644 --- a/crates/workspace-api/src/lib.rs +++ b/crates/workspace-api/src/lib.rs @@ -1220,8 +1220,6 @@ pub enum RuntimeTrustKeyStatus { pub struct RuntimeTrustKeyState { pub status: RuntimeTrustKeyStatus, #[serde(default, skip_serializing_if = "Option::is_none")] - pub public_key: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub fingerprint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "typescript", ts(type = "number | null"))] @@ -1272,6 +1270,13 @@ pub struct WorkspaceRuntimeDetail { pub recent_audit: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +#[serde(deny_unknown_fields)] +pub struct RuntimeTrustKeyRevealResponse { + pub public_key: String, +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(deny_unknown_fields)] @@ -2522,6 +2527,7 @@ pub fn catalog_typescript() -> String { RuntimeTrustAuditAction::decl(&config), RuntimeTrustAuditEntry::decl(&config), WorkspaceRuntimeDetail::decl(&config), + RuntimeTrustKeyRevealResponse::decl(&config), PutRuntimeTrustKeyRequest::decl(&config), RevokeRuntimeTrustKeyRequest::decl(&config), RuntimeTrustConflictKind::decl(&config), @@ -3250,7 +3256,6 @@ mod tests { "endpoint": "https://runtime.example", "trust_key": { "status": "active", - "public_key": "ssh-ed25519 AAAA runtime-test", "fingerprint": "SHA256:test", "revision": 2, "created_at": "2026-09-01T12:00:00Z", @@ -3271,6 +3276,13 @@ mod tests { let mut unknown = detail; unknown["trust_key"]["private_key"] = serde_json::json!("forbidden"); assert!(serde_json::from_value::(unknown).is_err()); + assert!( + serde_json::from_value::(serde_json::json!({ + "public_key": "yoi-ed25519-pub:v1:key", + "private_key": "forbidden" + })) + .is_err() + ); assert!( serde_json::from_value::(serde_json::json!({ "public_key": "key", diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 41492447..ba0fa922 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -78,10 +78,11 @@ use workspace_api::{ RequestActor, RevokeRuntimeTrustKeyRequest, RotateRepositorySshCredentialRequest, RuntimeConnectionTestFailureKind, RuntimeConnectionTestResponse, RuntimeConnectionTestStatus, RuntimeManagementSummary, RuntimeTrustAuditAction, RuntimeTrustAuditEntry, - RuntimeTrustConflictKind, RuntimeTrustConflictResponse, RuntimeTrustKeyState, - RuntimeTrustKeyStatus, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, - UpdateWorkspaceMetadataRequest, WhoamiResponse, WorkerLaunchOptionsResponse, - WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary, + RuntimeTrustConflictKind, RuntimeTrustConflictResponse, RuntimeTrustKeyRevealResponse, + RuntimeTrustKeyState, RuntimeTrustKeyStatus, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, + TICKET_RELATIONS_QUERY_PATH, UpdateWorkspaceMetadataRequest, WhoamiResponse, + WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption, + WorkerLaunchWorkerSummary, WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest, WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse, WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse, @@ -2672,7 +2673,9 @@ fn build_inner_router(api: WorkspaceApi) -> Router { ) .route( "/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key", - put(scoped_put_runtime_trust_key).delete(scoped_revoke_runtime_trust_key), + get(scoped_reveal_runtime_trust_key) + .put(scoped_put_runtime_trust_key) + .delete(scoped_revoke_runtime_trust_key), ) .route( "/api/w/{workspace_id}/runtimes/{runtime_id}/connection-tests", @@ -10884,20 +10887,44 @@ async fn scoped_create_remote_runtime( async fn scoped_get_runtime_detail( State(api): State, AxumPath(path): AxumPath, - Extension(actor): Extension, ) -> ApiResult> { validate_workspace_scope(&api, &path.workspace_id)?; - let workspace = api - .store - .get_workspace(&path.workspace_id) - .await? - .ok_or(Error::WorkspaceIdMismatch)?; - let is_owner = workspace.owner_account_id == actor.account_id; Ok(Json( - workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id, is_owner).await?, + workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id).await?, )) } +async fn scoped_reveal_runtime_trust_key( + State(api): State, + AxumPath(path): AxumPath, + Extension(actor): Extension, +) -> ApiResult> { + validate_workspace_scope(&api, &path.workspace_id)?; + require_workspace_owner( + &api, + &path.workspace_id, + &actor, + "Runtime public key reveal", + ) + .await?; + if path.runtime_id == EMBEDDED_WORKER_RUNTIME_ID { + return Err(settings_bad_request( + "embedded_runtime_trust_managed_internally", + "the embedded Runtime trust key is managed by Server identity authority", + )); + } + let binding = api + .store + .get_workspace_runtime_binding(&path.workspace_id, &path.runtime_id) + .await? + .ok_or_else(|| Error::RuntimeBindingNotFound { + runtime_id: path.runtime_id.clone(), + })?; + Ok(Json(RuntimeTrustKeyRevealResponse { + public_key: binding.public_key, + })) +} + async fn scoped_put_runtime_trust_key( State(api): State, AxumPath(path): AxumPath, @@ -10993,7 +11020,7 @@ async fn scoped_put_runtime_trust_key( .register_remote_runtime(source); } Ok( - Json(workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id, true).await?) + Json(workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id).await?) .into_response(), ) } @@ -11046,7 +11073,7 @@ async fn scoped_revoke_runtime_trust_key( api.runtime_subscription_broker .unregister_runtime(&path.runtime_id); Ok( - Json(workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id, true).await?) + Json(workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id).await?) .into_response(), ) } @@ -14738,7 +14765,6 @@ async fn workspace_runtime_detail( api: &WorkspaceApi, workspace_id: &str, runtime_id: &str, - include_public_key: bool, ) -> ApiResult { let binding = api .store @@ -14800,7 +14826,6 @@ async fn workspace_runtime_detail( let trust_key = binding.as_ref().map_or( RuntimeTrustKeyState { status: RuntimeTrustKeyStatus::Unconfigured, - public_key: None, fingerprint: None, revision: None, created_at: None, @@ -14813,7 +14838,6 @@ async fn workspace_runtime_detail( } else { RuntimeTrustKeyStatus::Active }, - public_key: include_public_key.then(|| binding.public_key.clone()), fingerprint: Some(binding.public_key_fingerprint.clone()), revision: Some(binding.binding_revision), created_at: Some(binding.created_at.clone()), @@ -22529,7 +22553,18 @@ mod tests { .await .unwrap(); - let Json(owner_detail) = scoped_get_runtime_detail( + let Json(detail) = scoped_get_runtime_detail( + State(api.clone()), + AxumPath(ScopedRuntimePath { + workspace_id: TEST_WORKSPACE_ID.to_string(), + runtime_id: "runtime-a".to_string(), + }), + ) + .await + .unwrap(); + assert_eq!(detail.trust_key.revision, Some(1)); + assert!(detail.trust_key.fingerprint.is_some()); + let Json(revealed) = scoped_reveal_runtime_trust_key( State(api.clone()), AxumPath(ScopedRuntimePath { workspace_id: TEST_WORKSPACE_ID.to_string(), @@ -22539,9 +22574,8 @@ mod tests { ) .await .unwrap(); - assert!(owner_detail.trust_key.public_key.is_some()); - assert_eq!(owner_detail.trust_key.revision, Some(1)); - let Json(reader_detail) = scoped_get_runtime_detail( + assert!(revealed.public_key.starts_with("yoi-ed25519-pub:v1:")); + let denied_reveal = scoped_reveal_runtime_trust_key( State(api.clone()), AxumPath(ScopedRuntimePath { workspace_id: TEST_WORKSPACE_ID.to_string(), @@ -22550,9 +22584,11 @@ mod tests { Extension(non_owner.clone()), ) .await - .unwrap(); - assert!(reader_detail.trust_key.public_key.is_none()); - assert!(reader_detail.trust_key.fingerprint.is_some()); + .unwrap_err(); + assert_eq!( + denied_reveal.into_response().status(), + StatusCode::FORBIDDEN + ); let response = scoped_put_runtime_trust_key( State(api.clone()), diff --git a/web/workspace/src/lib/generated/workspace-api.ts b/web/workspace/src/lib/generated/workspace-api.ts index f28915e7..09be8bf9 100644 --- a/web/workspace/src/lib/generated/workspace-api.ts +++ b/web/workspace/src/lib/generated/workspace-api.ts @@ -276,7 +276,6 @@ export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked"; export type RuntimeTrustKeyState = { status: RuntimeTrustKeyStatus; - public_key?: string | null; fingerprint?: string | null; revision?: number | null; created_at?: string | null; @@ -307,6 +306,8 @@ export type WorkspaceRuntimeDetail = { recent_audit: Array; }; +export type RuntimeTrustKeyRevealResponse = { public_key: string }; + export type PutRuntimeTrustKeyRequest = { public_key: string; expected_revision: number | null; diff --git a/web/workspace/src/lib/workspace/api/runtime-management.ts b/web/workspace/src/lib/workspace/api/runtime-management.ts index cefb9785..2b6a8ee3 100644 --- a/web/workspace/src/lib/workspace/api/runtime-management.ts +++ b/web/workspace/src/lib/workspace/api/runtime-management.ts @@ -11,6 +11,7 @@ import type { RuntimeTrustAuditEntry, RuntimeTrustConflictKind, RuntimeTrustConflictResponse, + RuntimeTrustKeyRevealResponse, RuntimeTrustKeyState, RuntimeTrustKeyStatus, WorkspaceRuntimeDetail, @@ -354,23 +355,11 @@ function trustKey(value: unknown, path: string): RuntimeTrustKeyState { exactKeys( item, ["status"], - [ - "public_key", - "fingerprint", - "revision", - "created_at", - "updated_at", - "revoked_at", - ], + ["fingerprint", "revision", "created_at", "updated_at", "revoked_at"], path, ); const result: RuntimeTrustKeyState = { status: enumValue(item.status, `${path}.status`, TRUST_STATUSES), - public_key: optionalNullableString( - item.public_key, - `${path}.public_key`, - LIMITS.publicKeyBytes, - ), fingerprint: optionalNullableString( item.fingerprint, `${path}.fingerprint`, @@ -538,6 +527,25 @@ export function parseWorkspaceRuntimeDetail( }; } +export function parseRuntimeTrustKeyRevealResponse( + value: unknown, +): RuntimeTrustKeyRevealResponse { + const response = object(value, "Runtime trust key reveal response"); + exactKeys( + response, + ["public_key"], + [], + "Runtime trust key reveal response", + ); + return { + public_key: boundedString( + response.public_key, + "Runtime trust key reveal response.public_key", + LIMITS.publicKeyBytes, + ), + }; +} + export function parseRuntimeTrustConflict( value: unknown, ): RuntimeTrustConflictResponse { @@ -677,6 +685,21 @@ async function finishMutation( return detail; } +export async function revealRuntimeTrustKey( + workspaceId: string, + runtimeId: string, +): Promise { + const response = await fetch( + workspaceApiPath( + workspaceId, + `/runtimes/${encodeURIComponent(runtimeId)}/trust-key`, + ), + ); + const payload = await readBoundedJson(response); + if (!response.ok) throw requestErrorFrom(payload, response.status); + return parseRuntimeTrustKeyRevealResponse(payload); +} + export async function previewRuntimePublicKeyFingerprint( publicKey: string, ): Promise { @@ -739,8 +762,15 @@ export async function revokeRuntimeTrustKey( workspaceId: string, runtimeId: string, request: RevokeRuntimeTrustKeyRequest, + currentFingerprint: string, + confirmation: string, fetchImpl: typeof fetch = fetch, ): Promise { + if (!currentFingerprint || confirmation.trim() !== currentFingerprint) { + throw new RuntimeTrustRequestError( + "Enter the current fingerprint exactly before revoking Workspace trust.", + ); + } const response = await fetchImpl( workspaceApiPath( workspaceId, diff --git a/web/workspace/src/lib/workspace/styles/settings.css b/web/workspace/src/lib/workspace/styles/settings.css index f085acea..03b4c3ad 100644 --- a/web/workspace/src/lib/workspace/styles/settings.css +++ b/web/workspace/src/lib/workspace/styles/settings.css @@ -426,7 +426,8 @@ .runtime-public-key, .runtime-trust-form textarea, - .runtime-trust-form input { + .runtime-trust-form input, + .runtime-revoke-row input { border: 1px solid var(--line); border-radius: 0.5rem; background: var(--bg-raised); @@ -450,14 +451,16 @@ max-width: 56rem; } - .runtime-trust-form label { + .runtime-trust-form label, + .runtime-revoke-row label { color: var(--text-muted); font-size: 0.78rem; font-weight: 700; } .runtime-trust-form textarea, - .runtime-trust-form input { + .runtime-trust-form input, + .runtime-revoke-row input { width: 100%; padding: 0.65rem 0.75rem; } @@ -466,7 +469,8 @@ resize: vertical; } - .runtime-trust-form small { + .runtime-trust-form small, + .runtime-revoke-row small { color: var(--text-muted); } diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte index 13a36d5e..2aa5d7be 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte @@ -8,6 +8,7 @@ import { previewRuntimePublicKeyFingerprint, putRuntimeTrustKey, + revealRuntimeTrustKey, revokeRuntimeTrustKey, RuntimeTrustConflictError, RuntimeTrustRequestError, @@ -17,10 +18,12 @@ type TrustAction = 'create' | 'replace' | 'reactivate'; let { data }: PageProps = $props(); - let revealPublicKey = $state(false); + let showPublicKey = $state(false); + let revealedPublicKey = $state(null); let publicKey = $state(''); let fingerprintConfirmation = $state(''); - let busyAction = $state<'save' | 'revoke' | 'copy' | null>(null); + let revokeFingerprintConfirmation = $state(''); + let busyAction = $state<'save' | 'revoke' | 'reveal' | 'copy' | null>(null); let fieldError = $state(null); let requestError = $state(null); let successMessage = $state(null); @@ -125,7 +128,9 @@ await putRuntimeTrustKey(data.workspaceId, data.runtimeId, request); publicKey = ''; fingerprintConfirmation = ''; - revealPublicKey = false; + revokeFingerprintConfirmation = ''; + showPublicKey = false; + revealedPublicKey = null; successMessage = action === 'create' ? 'Workspace trust was created.' : action === 'replace' @@ -154,6 +159,13 @@ requestError = 'Only active Workspace trust can be revoked.'; return; } + if ( + !trust.fingerprint || + revokeFingerprintConfirmation.trim() !== trust.fingerprint + ) { + fieldError = 'Enter the current fingerprint exactly before revoking Workspace trust.'; + return; + } fieldError = null; requestError = null; @@ -164,10 +176,18 @@ }; try { - await revokeRuntimeTrustKey(data.workspaceId, data.runtimeId, request); + await revokeRuntimeTrustKey( + data.workspaceId, + data.runtimeId, + request, + trust.fingerprint, + revokeFingerprintConfirmation, + ); publicKey = ''; fingerprintConfirmation = ''; - revealPublicKey = false; + revokeFingerprintConfirmation = ''; + showPublicKey = false; + revealedPublicKey = null; successMessage = 'Workspace trust was revoked.'; await reloadAuthority(); } catch (error) { @@ -182,16 +202,40 @@ } } + async function togglePublicKeyReveal(): Promise { + if (showPublicKey) { + showPublicKey = false; + revealedPublicKey = null; + return; + } + if (busyAction !== null) return; + busyAction = 'reveal'; + requestError = null; + successMessage = null; + try { + const response = await revealRuntimeTrustKey(data.workspaceId, data.runtimeId); + revealedPublicKey = response.public_key; + showPublicKey = true; + } catch (error) { + requestError = error instanceof Error ? error.message : 'Public key reveal failed.'; + } finally { + busyAction = null; + } + } + async function copyPublicKey(): Promise { - const key = data.runtimeDetail?.trust_key.public_key; - if (!key || busyAction !== null) return; + if (busyAction !== null) return; busyAction = 'copy'; requestError = null; + successMessage = null; try { - await navigator.clipboard.writeText(key); + const response = await revealRuntimeTrustKey(data.workspaceId, data.runtimeId); + await navigator.clipboard.writeText(response.public_key); successMessage = 'Public key copied.'; - } catch { - requestError = 'The browser could not copy the public key.'; + } catch (error) { + requestError = error instanceof Error + ? error.message + : 'The browser could not copy the public key.'; } finally { busyAction = null; } @@ -255,20 +299,23 @@

Workspace trust

- {#if trust.public_key} + {#if trust.status !== 'unconfigured'}
-
- {#if revealPublicKey} -
{trust.public_key}
+ {#if showPublicKey && revealedPublicKey} +
{revealedPublicKey}
{/if} - {:else if trust.status !== 'unconfigured'} -

The public key was not included in this authorized response.

{/if}
@@ -324,11 +371,25 @@
Revoke Workspace trust

Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.

+
diff --git a/web/workspace/tests/runtime-management-source.test.ts b/web/workspace/tests/runtime-management-source.test.ts index ac08da59..d1ef4af4 100644 --- a/web/workspace/tests/runtime-management-source.test.ts +++ b/web/workspace/tests/runtime-management-source.test.ts @@ -110,6 +110,8 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as "Revoke Workspace trust", "Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.", "RuntimeTrustConflictError", + "revealRuntimeTrustKey", + "revokeFingerprintConfirmation.trim() !== trust.fingerprint", "await reloadAuthority()", "busyAction !== null", "Workdirs", diff --git a/web/workspace/tests/runtime-management.test.ts b/web/workspace/tests/runtime-management.test.ts index daf80b71..9e9daf5b 100644 --- a/web/workspace/tests/runtime-management.test.ts +++ b/web/workspace/tests/runtime-management.test.ts @@ -4,10 +4,12 @@ declare const Deno: { import { parseRuntimeTrustConflict, + parseRuntimeTrustKeyRevealResponse, parseWorkspaceRuntimeDetail, parseWorkspaceRuntimeList, previewRuntimePublicKeyFingerprint, putRuntimeTrustKey, + revokeRuntimeTrustKey, RuntimeTrustConflictError, } from "../src/lib/workspace/api/runtime-management.ts"; @@ -62,7 +64,6 @@ function detail() { endpoint: "https://runtime.example.test", trust_key: { status: "active", - public_key: "ssh-ed25519 AAAA-test", fingerprint: "SHA256:current", revision: 3, created_at: "2026-09-01T12:00:00Z", @@ -162,10 +163,11 @@ Deno.test("Runtime validators reject unsafe revisions and bounded collection ove }); Deno.test("Runtime detail rejects unbounded strings and incoherent trust state", () => { - const largeKey = structuredClone(detail()); - largeKey.trust_key.public_key = "x".repeat(16 * 1024 + 1); assertThrows( - () => parseWorkspaceRuntimeDetail(largeKey), + () => + parseRuntimeTrustKeyRevealResponse({ + public_key: "x".repeat(16 * 1024 + 1), + }), "must be at most 16384 UTF-8 bytes", ); @@ -181,6 +183,30 @@ Deno.test("Runtime detail rejects unbounded strings and incoherent trust state", ); }); +Deno.test("mismatched revoke fingerprint never sends a request", async () => { + let requests = 0; + const fetchImpl: typeof fetch = () => { + requests += 1; + return Promise.reject(new Error("request must not be sent")); + }; + let rejected = false; + try { + await revokeRuntimeTrustKey( + "workspace-a", + "runtime-a", + { expected_revision: 3 }, + "sha256:current", + "sha256:different", + fetchImpl, + ); + } catch (error) { + rejected = error instanceof Error && + error.message.includes("current fingerprint exactly"); + } + assert(rejected, "mismatched fingerprint should be rejected locally"); + assert(requests === 0, "mismatched fingerprint sent a revoke request"); +}); + Deno.test("Runtime public key preview matches the Server fingerprint contract", async () => { const fingerprint = await previewRuntimePublicKeyFingerprint( "yoi-ed25519-pub:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",