fix: lock verified runtime public keys

This commit is contained in:
2026-09-13 00:32:44 +09:00
9 changed files with 515 additions and 15 deletions
+37
View File
@@ -1839,6 +1839,15 @@ pub struct CreateRemoteRuntimeRequest {
pub expected_revision: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct UpdateRemoteRuntimeRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
pub endpoint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
@@ -3098,6 +3107,7 @@ pub fn catalog_typescript() -> String {
RuntimeTrustConflictResponse::decl(&config),
RuntimePublicIdentityBundle::decl(&config),
CreateRemoteRuntimeRequest::decl(&config),
UpdateRemoteRuntimeRequest::decl(&config),
RuntimeConnectionTestStatus::decl(&config),
RuntimeConnectionTestFailureKind::decl(&config),
RuntimeConnectionTestResponse::decl(&config),
@@ -3617,6 +3627,33 @@ mod tests {
}
}
#[test]
fn remote_runtime_metadata_update_cannot_carry_public_key_authority() {
let request = UpdateRemoteRuntimeRequest {
display_name: Some("Runtime A".to_string()),
endpoint: "https://runtime.example.test".to_string(),
};
assert_eq!(
serde_json::to_value(&request).unwrap(),
serde_json::json!({
"display_name": "Runtime A",
"endpoint": "https://runtime.example.test",
})
);
assert!(
serde_json::from_value::<UpdateRemoteRuntimeRequest>(serde_json::json!({
"display_name": "Runtime A",
"endpoint": "https://runtime.example.test",
"public_bundle": {
"identity_id": "runtime-a",
"public_key": "yoi-ed25519-pub:v1:not-accepted",
},
}))
.is_err(),
"metadata updates must reject public key fields"
);
}
#[test]
fn worker_launch_optional_omission_and_request_shape_are_stable() {
assert_eq!(
+184 -10
View File
@@ -93,9 +93,9 @@ use workspace_api::{
RuntimeManagementSummary, RuntimeTrustAuditAction, RuntimeTrustAuditEntry,
RuntimeTrustConflictKind, RuntimeTrustConflictResponse, RuntimeTrustKeyRevealResponse,
RuntimeTrustKeyState, RuntimeTrustKeyStatus, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
TICKET_RELATIONS_QUERY_PATH, UpdateWorkspaceMetadataRequest, WhoamiResponse,
WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption,
WorkerLaunchWorkerSummary,
TICKET_RELATIONS_QUERY_PATH, UpdateRemoteRuntimeRequest, UpdateWorkspaceMetadataRequest,
WhoamiResponse, WorkerLaunchOptionsResponse, WorkerLaunchProfileCandidate,
WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
@@ -3420,7 +3420,9 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}",
get(scoped_get_runtime_detail).delete(scoped_delete_remote_runtime),
get(scoped_get_runtime_detail)
.post(scoped_update_remote_runtime)
.delete(scoped_delete_remote_runtime),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
@@ -12075,6 +12077,60 @@ async fn scoped_get_runtime_detail(
))
}
async fn scoped_update_remote_runtime(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<UpdateRemoteRuntimeRequest>,
) -> ApiResult<Json<WorkspaceRuntimeDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
require_workspace_owner(
&api,
&path.workspace_id,
&actor,
"manage Workspace Runtimes",
)
.await?;
if path.runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
return Err(settings_bad_request(
"embedded_runtime_not_config_managed",
"the embedded Runtime is built in and cannot be edited",
));
}
let endpoint =
validate_runtime_metadata(request.display_name.as_deref(), &request.endpoint).await?;
let display_name = request
.display_name
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(&path.runtime_id);
let binding = api
.store
.update_workspace_runtime_binding_metadata(
&path.workspace_id,
&path.runtime_id,
display_name,
endpoint.as_str().trim_end_matches('/'),
&Utc::now().to_rfc3339(),
)
.await?;
let activate_runtime = binding.state == StoredRuntimeBindingState::Verified;
api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(
(path.workspace_id.clone(), path.runtime_id.clone()),
binding.clone(),
);
api.runtime_subscription_broker
.unregister_runtime(&path.runtime_id);
api.register_workspace_runtime_binding(binding, activate_runtime)?;
Ok(Json(
workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id).await?,
))
}
async fn scoped_reveal_runtime_trust_key(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
@@ -16593,7 +16649,11 @@ async fn validate_runtime_connection_request(
"Runtime public bundle must contain a public key",
));
}
let endpoint = Url::parse(request.endpoint.trim()).map_err(|_| {
validate_runtime_metadata(request.display_name.as_deref(), &request.endpoint).await
}
async fn validate_runtime_metadata(display_name: Option<&str>, endpoint: &str) -> ApiResult<Url> {
let endpoint = Url::parse(endpoint.trim()).map_err(|_| {
settings_bad_request(
"invalid_remote_runtime_endpoint",
"endpoint must be an absolute HTTP or HTTPS URL",
@@ -16656,11 +16716,7 @@ async fn validate_runtime_connection_request(
));
}
}
if request
.display_name
.as_deref()
.is_some_and(|value| value.is_empty() || value.chars().any(char::is_control))
{
if display_name.is_some_and(|value| value.is_empty() || value.chars().any(char::is_control)) {
return Err(settings_bad_request(
"invalid_remote_runtime_display_name",
"display_name must be non-empty when supplied and cannot contain control characters",
@@ -20869,6 +20925,124 @@ mod tests {
);
}
#[tokio::test]
async fn verified_remote_runtime_metadata_update_preserves_public_key_and_verification() {
let dir = tempfile::tempdir().unwrap();
let api = test_api(dir.path()).await;
let actor = test_owner_actor();
let workspace_identity = api
.signing_identities
.provision_existing(&api.config.workspace_id, &actor.account_id)
.unwrap();
let runtime_identity = RuntimeIdentityMaterial::generate("verified-runtime").unwrap();
api.store
.upsert_workspace_runtime_binding_record(
WorkspaceRuntimeBinding {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "verified-runtime".to_string(),
display_name: "Old label".to_string(),
base_url: "https://8.8.8.8".to_string(),
public_key: runtime_identity.public_key.clone(),
public_key_fingerprint: String::new(),
binding_revision: 1,
state: StoredRuntimeBindingState::Configured,
authentication_mode: StoredRuntimeAuthenticationMode::WorkspaceIdentity,
workspace_key_id: Some(workspace_identity.key_id.clone()),
workspace_key_generation: Some(workspace_identity.revision),
created_at: "1".to_string(),
updated_at: "1".to_string(),
revoked_at: None,
},
false,
)
.await
.unwrap();
let configured = api
.store
.get_workspace_runtime_binding(&api.config.workspace_id, "verified-runtime")
.await
.unwrap()
.unwrap();
let evidence = crate::store::WorkspaceRuntimeVerificationEvidence {
workspace_id: configured.workspace_id.clone(),
runtime_id: configured.runtime_id.clone(),
binding_revision: configured.binding_revision,
workspace_key_id: workspace_identity.key_id,
workspace_identity_revision: workspace_identity.revision,
workspace_trust_generation: workspace_identity.revision,
runtime_public_key_fingerprint: configured.public_key_fingerprint.clone(),
runtime_identity_revision: 1,
challenge_id: "verified-metadata-update".to_string(),
state: "verified".to_string(),
last_outcome: "verified".to_string(),
verified_at: Some("2".to_string()),
checked_at: "2".to_string(),
};
api.store
.record_workspace_runtime_verification_attempt(&evidence)
.await
.unwrap();
api.store
.complete_workspace_runtime_verification(&evidence)
.await
.unwrap();
let Json(updated) = scoped_update_remote_runtime(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "verified-runtime".to_string(),
}),
Extension(actor.clone()),
Json(UpdateRemoteRuntimeRequest {
display_name: Some("New label".to_string()),
endpoint: "https://8.8.4.4".to_string(),
}),
)
.await
.unwrap();
assert_eq!(updated.runtime.runtime.label, "New label");
assert_eq!(updated.endpoint.as_deref(), Some("https://8.8.4.4"));
let binding = updated.runtime.management.binding.unwrap();
assert_eq!(binding.state, WorkspaceRuntimeBindingState::Verified);
assert_eq!(binding.revision, 1);
assert_eq!(binding.verification.unwrap().binding_revision, 1);
assert_eq!(
updated.trust_key.fingerprint.as_deref(),
Some(configured.public_key_fingerprint.as_str())
);
let stored = api
.store
.get_workspace_runtime_binding(&api.config.workspace_id, "verified-runtime")
.await
.unwrap()
.unwrap();
assert_eq!(stored.public_key, runtime_identity.public_key);
assert_eq!(
stored.public_key_fingerprint,
configured.public_key_fingerprint
);
let replacement = RuntimeIdentityMaterial::generate("verified-runtime").unwrap();
let error = create_remote_runtime(
State(api),
Extension(actor),
Json(CreateRemoteRuntimeRequest {
public_bundle: workspace_api::RuntimePublicIdentityBundle {
identity_id: "verified-runtime".to_string(),
public_key: replacement.public_key,
},
display_name: Some("New label".to_string()),
endpoint: "https://8.8.4.4".to_string(),
expected_revision: Some(1),
}),
)
.await
.unwrap_err();
assert_eq!(error.into_response().status(), StatusCode::CONFLICT);
}
#[tokio::test]
async fn remote_runtime_registration_is_workspace_scoped_revisioned_and_configured() {
let dir = tempfile::tempdir().unwrap();
+79
View File
@@ -832,6 +832,14 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
expected_revision: Option<u64>,
actor_account_id: &str,
) -> Result<(WorkspaceRuntimeBindingMutation, WorkspaceRuntimeBinding)>;
async fn update_workspace_runtime_binding_metadata(
&self,
workspace_id: &str,
runtime_id: &str,
display_name: &str,
base_url: &str,
updated_at: &str,
) -> Result<WorkspaceRuntimeBinding>;
async fn revoke_workspace_runtime_binding_key(
&self,
workspace_id: &str,
@@ -2277,6 +2285,59 @@ impl SqliteWorkspaceStore {
})
}
pub fn update_workspace_runtime_binding_metadata(
&self,
workspace_id: &str,
runtime_id: &str,
display_name: &str,
base_url: &str,
updated_at: &str,
) -> Result<WorkspaceRuntimeBinding> {
validate_identifier("workspace_id", workspace_id)?;
validate_identifier("runtime_id", runtime_id)?;
validate_non_empty("runtime display_name", display_name)?;
validate_runtime_base_url(base_url)?;
validate_non_empty("updated_at", updated_at)?;
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let existing = tx
.query_row(
r#"SELECT workspace_id, runtime_id, display_name, base_url, public_key,
public_key_fingerprint, binding_revision, state, authentication_mode,
workspace_key_id, workspace_key_generation, created_at, updated_at, revoked_at
FROM workspace_runtime_bindings
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
params![workspace_id, runtime_id],
read_workspace_runtime_binding,
)
.optional()?
.ok_or_else(|| Error::RuntimeBindingNotFound {
runtime_id: runtime_id.to_string(),
})?;
if existing.display_name == display_name && existing.base_url == base_url {
tx.commit()?;
return Ok(existing);
}
tx.execute(
r#"UPDATE workspace_runtime_bindings
SET display_name = ?3, base_url = ?4, updated_at = ?5
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
params![workspace_id, runtime_id, display_name, base_url, updated_at],
)?;
let updated = tx.query_row(
r#"SELECT workspace_id, runtime_id, display_name, base_url, public_key,
public_key_fingerprint, binding_revision, state, authentication_mode,
workspace_key_id, workspace_key_generation, created_at, updated_at, revoked_at
FROM workspace_runtime_bindings
WHERE workspace_id = ?1 AND runtime_id = ?2"#,
params![workspace_id, runtime_id],
read_workspace_runtime_binding,
)?;
tx.commit()?;
Ok(updated)
})
}
pub fn revoke_workspace_runtime_binding_key(
&self,
workspace_id: &str,
@@ -3505,6 +3566,24 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
)
}
async fn update_workspace_runtime_binding_metadata(
&self,
workspace_id: &str,
runtime_id: &str,
display_name: &str,
base_url: &str,
updated_at: &str,
) -> Result<WorkspaceRuntimeBinding> {
SqliteWorkspaceStore::update_workspace_runtime_binding_metadata(
self,
workspace_id,
runtime_id,
display_name,
base_url,
updated_at,
)
}
async fn revoke_workspace_runtime_binding_key(
&self,
workspace_id: &str,
@@ -496,6 +496,11 @@ export type CreateRemoteRuntimeRequest = {
expected_revision?: number | null;
};
export type UpdateRemoteRuntimeRequest = {
display_name?: string | null;
endpoint: string;
};
export type RuntimeConnectionTestStatus = "compatible" | "failed";
export type RuntimeConnectionTestFailureKind =
@@ -16,6 +16,7 @@ import type {
RuntimeTrustKeyState,
RuntimeTrustKeyStatus,
RuntimeVerificationEvidenceSummary,
UpdateRemoteRuntimeRequest,
WorkspaceRuntimeBindingState,
WorkspaceRuntimeBindingSummary,
WorkspaceRuntimeDetail,
@@ -919,6 +920,26 @@ export async function createRemoteRuntime(
return runtime;
}
export async function updateRemoteRuntime(
workspaceId: string,
runtimeId: string,
request: UpdateRemoteRuntimeRequest,
fetchImpl: typeof fetch = fetch,
): Promise<WorkspaceRuntimeDetail> {
const response = await fetchImpl(
workspaceApiPath(
workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}`,
),
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
},
);
return finishMutation(response, workspaceId, runtimeId);
}
export async function deleteRemoteRuntime(
workspaceId: string,
runtimeId: string,
@@ -445,6 +445,20 @@
overflow-wrap: anywhere;
}
.runtime-public-key-readonly {
display: grid;
gap: var(--space-1);
max-width: 56rem;
}
.runtime-public-key-readonly p {
color: var(--text-muted);
}
.runtime-public-key-readonly code {
overflow-wrap: anywhere;
}
.runtime-trust-form {
display: grid;
gap: var(--space-2);
@@ -10,6 +10,7 @@
previewRuntimePublicKeyFingerprint,
revealRuntimeTrustKey,
revokeRuntimeTrustKey,
updateRemoteRuntime,
RuntimeTrustConflictError,
RuntimeTrustRouteFence,
RuntimeTrustRequestError,
@@ -23,8 +24,11 @@
let showPublicKey = $state(false);
let revealedPublicKey = $state<string | null>(null);
let publicKey = $state('');
let displayName = $state('');
let endpoint = $state('');
let editingMetadata = $state(false);
let deleteRuntimeConfirmation = $state('');
let busyAction = $state<'save' | 'revoke' | 'reveal' | 'copy' | 'delete' | null>(null);
let busyAction = $state<'metadata' | 'trust' | 'revoke' | 'reveal' | 'copy' | 'delete' | null>(null);
let fieldError = $state<string | null>(null);
let deleteRuntimeError = $state<string | null>(null);
let requestError = $state<string | null>(null);
@@ -43,6 +47,9 @@
showPublicKey = false;
revealedPublicKey = null;
publicKey = '';
displayName = data.runtimeDetail?.runtime.label ?? '';
endpoint = data.runtimeDetail?.endpoint ?? '';
editingMetadata = false;
deleteRuntimeConfirmation = '';
busyAction = null;
fieldError = null;
@@ -105,6 +112,49 @@
return routeFence.isCurrent(operation, data.runtimeId);
}
function cancelRuntimeMetadataEdit(): void {
displayName = data.runtimeDetail?.runtime.label ?? '';
endpoint = data.runtimeDetail?.endpoint ?? '';
editingMetadata = false;
requestError = null;
}
async function saveRuntimeMetadata(event: SubmitEvent): Promise<void> {
event.preventDefault();
if (busyAction !== null || !data.runtimeDetail) return;
requestError = null;
successMessage = null;
const normalizedDisplayName = displayName.trim();
const normalizedEndpoint = endpoint.trim();
if (!normalizedDisplayName) {
requestError = 'Enter a Runtime label.';
return;
}
if (!normalizedEndpoint) {
requestError = 'Enter the Runtime endpoint.';
return;
}
const operation = routeFence.capture(data.runtimeId);
busyAction = 'metadata';
try {
await updateRemoteRuntime(data.workspaceId, operation.runtimeId, {
display_name: normalizedDisplayName,
endpoint: normalizedEndpoint,
});
if (!isCurrentRoute(operation)) return;
successMessage = 'Runtime settings were updated.';
editingMetadata = false;
await reloadAuthority();
} catch (error) {
if (!isCurrentRoute(operation)) return;
requestError = error instanceof Error ? error.message : 'Runtime settings update failed.';
} finally {
if (isCurrentRoute(operation)) busyAction = null;
}
}
async function saveTrustKey(event: SubmitEvent): Promise<void> {
event.preventDefault();
if (busyAction !== null || !data.runtimeDetail) return;
@@ -135,7 +185,7 @@
const action = trustAction(trust.status);
const operation = routeFence.capture(data.runtimeId);
busyAction = 'save';
busyAction = 'trust';
try {
const binding = data.runtimeDetail.runtime.management.binding;
if (!binding || !data.runtimeDetail.endpoint) {
@@ -339,6 +389,7 @@
{@const detail = data.runtimeDetail}
{@const runtime = detail.runtime}
{@const trust = detail.trust_key}
{@const verifiedTrust = runtime.management.binding?.state === 'verified'}
{@const currentAction = trustAction(trust.status)}
<section class="runtime-detail-section" aria-labelledby="runtime-identity-heading">
@@ -373,6 +424,50 @@
</section>
{#if data.workspace.permissions.manage_runtimes && !runtime.management.built_in}
<section class="runtime-detail-section" aria-labelledby="runtime-settings-heading">
<h2 id="runtime-settings-heading">Runtime settings</h2>
{#if editingMetadata}
<form class="runtime-trust-form" onsubmit={saveRuntimeMetadata}>
<label for="runtime-display-name-input">Label</label>
<input
id="runtime-display-name-input"
bind:value={displayName}
autocomplete="off"
disabled={busyAction !== null}
/>
<label for="runtime-endpoint-input">Endpoint</label>
<input
id="runtime-endpoint-input"
bind:value={endpoint}
inputmode="url"
autocomplete="url"
spellcheck="false"
disabled={busyAction !== null}
/>
<div class="settings-action-row">
<button type="submit" disabled={busyAction !== null}>
{busyAction === 'metadata' ? 'Saving…' : 'Save Runtime settings'}
</button>
<button
class="settings-secondary-action"
type="button"
onclick={cancelRuntimeMetadataEdit}
disabled={busyAction !== null}
>Cancel</button>
</div>
</form>
{:else}
<div class="settings-action-row">
<button
class="settings-secondary-action"
type="button"
onclick={() => (editingMetadata = true)}
disabled={busyAction !== null}
>Edit Runtime</button>
</div>
{/if}
</section>
<section class="runtime-detail-section" aria-labelledby="runtime-trust-heading">
<h2 id="runtime-trust-heading">Workspace trust</h2>
@@ -395,7 +490,16 @@
{/if}
{/if}
<form class="runtime-trust-form" onsubmit={saveTrustKey}>
{#if verifiedTrust}
<div class="runtime-public-key-readonly">
<strong>Runtime public key</strong>
<p>
This verified key is read-only. Revoke Workspace trust and register the Runtime again to use a different public key.
</p>
<code>{trust.fingerprint}</code>
</div>
{:else}
<form class="runtime-trust-form" onsubmit={saveTrustKey}>
<label for="runtime-public-key-input">Runtime public key</label>
<textarea
id="runtime-public-key-input"
@@ -427,10 +531,11 @@
{/if}
<div class="settings-action-row">
<button type="submit" disabled={busyAction !== null}>
{busyAction === 'save' ? 'Saving…' : actionLabel(currentAction)}
{busyAction === 'trust' ? 'Saving…' : actionLabel(currentAction)}
</button>
</div>
</form>
</form>
{/if}
<div class="runtime-revoke-row">
<div>
@@ -129,8 +129,29 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
const ownerGate = page.indexOf("data.workspace.permissions.manage_runtimes");
const reveal = page.indexOf("Reveal public key");
const verifiedKey = page.indexOf("{#if verifiedTrust}");
const mutation = page.indexOf('id="runtime-public-key-input"');
const metadataStart = page.indexOf("async function saveRuntimeMetadata");
const trustStart = page.indexOf("async function saveTrustKey");
const metadataMutation = page.slice(metadataStart, trustStart);
assert(ownerGate >= 0, "Runtime trust controls should use manage_runtimes");
assert(
verifiedKey >= 0 && verifiedKey < mutation &&
page.slice(verifiedKey, mutation).includes("{:else}"),
"verified Runtime public key must be read-only while unverified trust keeps key input",
);
assert(
metadataMutation.includes("updateRemoteRuntime(") &&
metadataMutation.includes("display_name: normalizedDisplayName") &&
metadataMutation.includes("endpoint: normalizedEndpoint"),
"mutable Runtime settings should use the metadata-only update request",
);
assert(
!metadataMutation.includes("publicKey") &&
!metadataMutation.includes("public_bundle") &&
!metadataMutation.includes("public_key"),
"verified Runtime metadata updates must not carry public key authority",
);
assert(
page.includes("Current fingerprint"),
"current fingerprint must be explicit",
@@ -168,6 +189,10 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
"routeFence.enter(data.runtimeId)",
"showPublicKey = false",
"revealedPublicKey = null",
"saveRuntimeMetadata",
"Runtime settings",
"Edit Runtime",
"This verified key is read-only",
"publicKey = ''",
"requestError = null",
"successMessage = null",
@@ -13,6 +13,7 @@ import {
revokeRuntimeTrustKey,
RuntimeTrustConflictError,
RuntimeTrustRouteFence,
updateRemoteRuntime,
} from "../src/lib/workspace/api/runtime-management.ts";
function assert(condition: unknown, message: string): asserts condition {
@@ -273,6 +274,45 @@ Deno.test("mismatched revoke fingerprint never sends a request", async () => {
assert(requests === 0, "mismatched fingerprint sent a revoke request");
});
Deno.test("Runtime metadata update never sends public key authority", async () => {
let requestedUrl = "";
let requestedMethod = "";
let requestedBody: unknown = null;
const fetchImpl = ((input: string | URL | Request, init?: RequestInit) => {
requestedUrl = String(input);
requestedMethod = init?.method ?? "GET";
requestedBody = JSON.parse(String(init?.body));
return Promise.resolve(Response.json(detail()));
}) as typeof fetch;
await updateRemoteRuntime(
"workspace-a",
"arcadia",
{
display_name: "Updated Runtime",
endpoint: "https://runtime.example.test/v2",
},
fetchImpl,
);
assert(
requestedUrl === "/api/w/workspace-a/runtimes/arcadia",
`unexpected update URL: ${requestedUrl}`,
);
assert(requestedMethod === "POST", "Runtime update must use POST");
assert(
JSON.stringify(requestedBody) ===
JSON.stringify({
display_name: "Updated Runtime",
endpoint: "https://runtime.example.test/v2",
}),
`unexpected update body: ${JSON.stringify(requestedBody)}`,
);
const body = requestedBody as Record<string, unknown>;
assert(!("public_bundle" in body), "metadata update sent public_bundle");
assert(!("public_key" in body), "metadata update sent public_key");
});
Deno.test("Runtime registration delete uses the Workspace-scoped resource route", async () => {
let requestedUrl = "";
let requestedMethod = "";