feat: complete Workspace Runtime management flow
This commit is contained in:
@@ -919,6 +919,30 @@ export async function createRemoteRuntime(
|
||||
return runtime;
|
||||
}
|
||||
|
||||
export async function deleteRemoteRuntime(
|
||||
workspaceId: string,
|
||||
runtimeId: string,
|
||||
fetchImpl: typeof fetch = fetch,
|
||||
): Promise<void> {
|
||||
const response = await fetchImpl(
|
||||
workspaceApiPath(
|
||||
workspaceId,
|
||||
`/runtimes/${encodeURIComponent(runtimeId)}`,
|
||||
),
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
if (response.ok) return;
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await readBoundedJson(response);
|
||||
} catch {
|
||||
throw new RuntimeTrustRequestError(
|
||||
`Runtime registration delete failed (${response.status})`,
|
||||
);
|
||||
}
|
||||
throw requestErrorFrom(payload, response.status);
|
||||
}
|
||||
|
||||
export async function revealRuntimeTrustKey(
|
||||
workspaceId: string,
|
||||
runtimeId: string,
|
||||
|
||||
@@ -678,7 +678,7 @@ Deno.test("workspace Runtime inventory lives under Settings admin routes", async
|
||||
"Runtimes should be admin Settings navigation, not primary workspace sidebar navigation",
|
||||
);
|
||||
assert(
|
||||
runtimesPage.includes("Add remote Runtime") &&
|
||||
runtimesPage.includes("Connect a remote Runtime") &&
|
||||
runtimesPage.includes("Open workdirs") &&
|
||||
runtimesPage.includes("settings-runtime-table") &&
|
||||
runtimesPage.includes("testRuntimeConnection") &&
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
RuntimeTrustRequestError,
|
||||
} from '$lib/workspace/api/runtime-management';
|
||||
import { testRuntimeConnection } from '$lib/workspace/api/runtime-connection';
|
||||
import { provisionWorkspaceSigningIdentity } from '$lib/workspace/settings/profile-api';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
const runtimeBundlePlaceholder =
|
||||
@@ -21,10 +22,10 @@
|
||||
let displayName = $state('');
|
||||
let endpoint = $state('');
|
||||
let runtimeFingerprint = $state<string | null>(null);
|
||||
let fingerprintConfirmation = $state('');
|
||||
let showAddRuntime = $state(false);
|
||||
let busyRuntimeId = $state<string | null>(null);
|
||||
let requestError = $state<string | null>(null);
|
||||
let requestNotice = $state<string | null>(null);
|
||||
let testResults = $state<Record<string, RuntimeConnectionTestResponse>>({});
|
||||
let connectionTestGeneration = 0;
|
||||
|
||||
@@ -88,6 +89,25 @@
|
||||
: '';
|
||||
}
|
||||
|
||||
function workspaceBundleFilename(): string {
|
||||
return `workspace-${data.workspaceId}-public-bundle.json`;
|
||||
}
|
||||
|
||||
async function provisionSigningIdentity(): Promise<void> {
|
||||
requestError = null;
|
||||
requestNotice = null;
|
||||
busyRuntimeId = 'provision-workspace-identity';
|
||||
try {
|
||||
await provisionWorkspaceSigningIdentity(data.workspaceId);
|
||||
await invalidateAll();
|
||||
requestNotice = 'Workspace identity provisioned. Copy its public bundle to the Runtime host.';
|
||||
} catch (error) {
|
||||
requestError = error instanceof Error ? error.message : String(error);
|
||||
} finally {
|
||||
busyRuntimeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyWorkspaceBundle(): Promise<void> {
|
||||
requestError = null;
|
||||
try {
|
||||
@@ -100,7 +120,6 @@
|
||||
async function previewRuntimeFingerprint(): Promise<void> {
|
||||
requestError = null;
|
||||
runtimeFingerprint = null;
|
||||
fingerprintConfirmation = '';
|
||||
busyRuntimeId = 'preview';
|
||||
try {
|
||||
const bundle = parseRuntimePublicBundle(runtimePublicBundle);
|
||||
@@ -115,15 +134,13 @@
|
||||
async function addRuntime(event: SubmitEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
requestError = null;
|
||||
requestNotice = null;
|
||||
busyRuntimeId = 'create';
|
||||
try {
|
||||
const publicBundle = parseRuntimePublicBundle(runtimePublicBundle);
|
||||
const currentFingerprint = await previewRuntimePublicKeyFingerprint(publicBundle.public_key);
|
||||
if (
|
||||
runtimeFingerprint !== currentFingerprint ||
|
||||
fingerprintConfirmation.trim() !== currentFingerprint
|
||||
) {
|
||||
throw new Error('Preview and confirm the exact Runtime public key fingerprint before registration');
|
||||
if (runtimeFingerprint !== currentFingerprint) {
|
||||
throw new Error('Preview the Runtime public key fingerprint before registration');
|
||||
}
|
||||
await createRemoteRuntime(data.workspaceId, {
|
||||
public_bundle: publicBundle,
|
||||
@@ -133,10 +150,10 @@
|
||||
});
|
||||
runtimePublicBundle = '';
|
||||
runtimeFingerprint = null;
|
||||
fingerprintConfirmation = '';
|
||||
displayName = '';
|
||||
endpoint = '';
|
||||
showAddRuntime = false;
|
||||
requestNotice = 'Runtime registered for this Workspace. Run Test to complete authenticated verification.';
|
||||
await invalidateAll();
|
||||
} catch (error) {
|
||||
requestError = error instanceof RuntimeTrustRequestError || error instanceof Error
|
||||
@@ -205,16 +222,63 @@
|
||||
|
||||
{#if showAddRuntime && data.workspace.permissions.manage_runtimes}
|
||||
<form class="settings-runtime-form" onsubmit={addRuntime}>
|
||||
<h2>Add remote Runtime</h2>
|
||||
<header>
|
||||
<h2>Connect a remote Runtime</h2>
|
||||
<p>
|
||||
This creates a binding for this Workspace. The Runtime can remain connected to other Workspaces;
|
||||
their trust entries are not replaced.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="settings-runtime-trust-instructions" aria-labelledby="workspace-to-runtime-heading">
|
||||
<h3 id="workspace-to-runtime-heading">1. Trust this Workspace on the Runtime</h3>
|
||||
<p>
|
||||
Each Workspace has its own signing identity. Add this Workspace public bundle to the same store used
|
||||
when starting the Runtime.
|
||||
</p>
|
||||
{#if data.signingIdentityError}
|
||||
<p class="section-state error">{data.signingIdentityError}</p>
|
||||
{:else if data.signingIdentity?.identity.state === 'pending_provisioning'}
|
||||
<p>This Workspace does not have an active signing identity yet.</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyRuntimeId !== null}
|
||||
onclick={() => void provisionSigningIdentity()}
|
||||
>
|
||||
{busyRuntimeId === 'provision-workspace-identity' ? 'Provisioning…' : 'Provision Workspace identity'}
|
||||
</button>
|
||||
{:else if data.signingIdentity?.public_bundle}
|
||||
<p>
|
||||
Save the bundle as <code>{workspaceBundleFilename()}</code> on the Runtime host. It contains no
|
||||
private key material.
|
||||
</p>
|
||||
<pre>{workspacePublicBundle()}</pre>
|
||||
<button type="button" disabled={busyRuntimeId !== null} onclick={copyWorkspaceBundle}>
|
||||
Copy Workspace public bundle
|
||||
</button>
|
||||
<pre>yoi-runtime trust-workspace add --bundle {workspaceBundleFilename()}</pre>
|
||||
<small>
|
||||
Pass the same <code>--fs-root</code> and <code>--fs-runtime-dir</code> options used by the Runtime
|
||||
service. Existing Workspace trust entries are preserved.
|
||||
</small>
|
||||
{:else}
|
||||
<p class="section-state">Loading Workspace public identity…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="settings-runtime-trust-instructions" aria-labelledby="runtime-to-workspace-heading">
|
||||
<h3 id="runtime-to-workspace-heading">2. Verify the Runtime identity</h3>
|
||||
<p>
|
||||
On the Runtime host, run <code>yoi-runtime identity show --json</code> with the same Runtime storage
|
||||
options, then paste the public bundle below.
|
||||
</p>
|
||||
<div class="settings-form-grid">
|
||||
<label class="settings-form-wide">
|
||||
Runtime public bundle
|
||||
<small>Run <code>yoi-runtime identity show --json</code> on the Runtime host and paste the result.</small>
|
||||
<textarea
|
||||
bind:value={runtimePublicBundle}
|
||||
oninput={() => {
|
||||
runtimeFingerprint = null;
|
||||
fingerprintConfirmation = '';
|
||||
}}
|
||||
required
|
||||
rows="5"
|
||||
@@ -226,17 +290,19 @@
|
||||
</button>
|
||||
</label>
|
||||
{#if runtimeFingerprint}
|
||||
<label>
|
||||
Runtime key fingerprint
|
||||
<code>{runtimeFingerprint}</code>
|
||||
<input
|
||||
bind:value={fingerprintConfirmation}
|
||||
required
|
||||
autocomplete="off"
|
||||
placeholder="Enter the fingerprint exactly"
|
||||
/>
|
||||
</label>
|
||||
<dl class="runtime-facts">
|
||||
<div>
|
||||
<dt>Runtime fingerprint</dt>
|
||||
<dd><code>{runtimeFingerprint}</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-runtime-trust-instructions" aria-labelledby="runtime-connection-heading">
|
||||
<h3 id="runtime-connection-heading">3. Register the connection</h3>
|
||||
<div class="settings-form-grid">
|
||||
<label>
|
||||
Display name
|
||||
<input bind:value={displayName} autocomplete="off" />
|
||||
@@ -246,30 +312,17 @@
|
||||
<input bind:value={endpoint} type="url" required placeholder="https://runtime.example" />
|
||||
</label>
|
||||
</div>
|
||||
<section class="settings-runtime-trust-instructions" aria-labelledby="runtime-trust-heading">
|
||||
<h3 id="runtime-trust-heading">Trust this Workspace on the Runtime</h3>
|
||||
{#if data.signingIdentityError}
|
||||
<p class="section-state error">{data.signingIdentityError}</p>
|
||||
{:else if data.signingIdentity?.public_bundle}
|
||||
<p>
|
||||
Save this public bundle as <code>workspace-public-bundle.json</code> on the Runtime host.
|
||||
It contains no private key material.
|
||||
Registration stores this Workspace-scoped binding. After it appears in the list, run
|
||||
<strong>Test</strong> to complete authenticated verification.
|
||||
</p>
|
||||
<pre>{workspacePublicBundle()}</pre>
|
||||
<button type="button" onclick={copyWorkspaceBundle}>Copy Workspace public bundle</button>
|
||||
<pre>yoi-runtime trust-workspace add --bundle workspace-public-bundle.json</pre>
|
||||
<p>
|
||||
Runtime registration remains <code>configured</code> until authenticated verification is completed.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="section-state">Loading Workspace public identity…</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<div class="settings-action-row">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busyRuntimeId !== null || !runtimeFingerprint || fingerprintConfirmation.trim() !== runtimeFingerprint}
|
||||
>Add Runtime</button>
|
||||
disabled={busyRuntimeId !== null || !data.signingIdentity?.public_bundle || !runtimeFingerprint}
|
||||
>Register Runtime</button>
|
||||
<button type="button" disabled={busyRuntimeId !== null} onclick={() => showAddRuntime = false}>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -280,6 +333,9 @@
|
||||
{#if requestError}
|
||||
<p class="section-state error">{requestError}</p>
|
||||
{/if}
|
||||
{#if requestNotice}
|
||||
<p class="section-state">{requestNotice}</p>
|
||||
{/if}
|
||||
|
||||
{#if data.runtimesError}
|
||||
<p class="section-state error">{data.runtimesError}</p>
|
||||
|
||||
+93
-54
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import type {
|
||||
RevokeRuntimeTrustKeyRequest,
|
||||
RuntimeTrustKeyStatus,
|
||||
} from '$lib/generated/workspace-api';
|
||||
import {
|
||||
createRemoteRuntime,
|
||||
deleteRemoteRuntime,
|
||||
previewRuntimePublicKeyFingerprint,
|
||||
revealRuntimeTrustKey,
|
||||
revokeRuntimeTrustKey,
|
||||
@@ -22,10 +23,10 @@
|
||||
let showPublicKey = $state(false);
|
||||
let revealedPublicKey = $state<string | null>(null);
|
||||
let publicKey = $state('');
|
||||
let fingerprintConfirmation = $state('');
|
||||
let revokeFingerprintConfirmation = $state('');
|
||||
let busyAction = $state<'save' | 'revoke' | 'reveal' | 'copy' | null>(null);
|
||||
let deleteRuntimeConfirmation = $state('');
|
||||
let busyAction = $state<'save' | 'revoke' | 'reveal' | 'copy' | 'delete' | null>(null);
|
||||
let fieldError = $state<string | null>(null);
|
||||
let deleteRuntimeError = $state<string | null>(null);
|
||||
let requestError = $state<string | null>(null);
|
||||
let successMessage = $state<string | null>(null);
|
||||
let replacementFingerprint = $state<string | null>(null);
|
||||
@@ -42,10 +43,10 @@
|
||||
showPublicKey = false;
|
||||
revealedPublicKey = null;
|
||||
publicKey = '';
|
||||
fingerprintConfirmation = '';
|
||||
revokeFingerprintConfirmation = '';
|
||||
deleteRuntimeConfirmation = '';
|
||||
busyAction = null;
|
||||
fieldError = null;
|
||||
deleteRuntimeError = null;
|
||||
requestError = null;
|
||||
successMessage = null;
|
||||
replacementFingerprint = null;
|
||||
@@ -132,16 +133,6 @@
|
||||
|
||||
const trust = data.runtimeDetail.trust_key;
|
||||
const action = trustAction(trust.status);
|
||||
if (action !== 'create') {
|
||||
if (!trust.fingerprint) {
|
||||
requestError = 'The authoritative fingerprint is unavailable. Reload before changing trust.';
|
||||
return;
|
||||
}
|
||||
if (fingerprintConfirmation.trim() !== trust.fingerprint) {
|
||||
fieldError = 'Enter the current fingerprint exactly to confirm this change.';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const operation = routeFence.capture(data.runtimeId);
|
||||
busyAction = 'save';
|
||||
@@ -163,8 +154,6 @@
|
||||
});
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
publicKey = '';
|
||||
fingerprintConfirmation = '';
|
||||
revokeFingerprintConfirmation = '';
|
||||
showPublicKey = false;
|
||||
revealedPublicKey = null;
|
||||
successMessage = action === 'create'
|
||||
@@ -175,7 +164,6 @@
|
||||
await reloadAuthority();
|
||||
} catch (error) {
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
fingerprintConfirmation = '';
|
||||
if (error instanceof RuntimeTrustConflictError) {
|
||||
requestError = `${error.message} Authoritative Runtime trust has been reloaded.`;
|
||||
await reloadAuthority();
|
||||
@@ -196,11 +184,8 @@
|
||||
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.';
|
||||
if (!trust.fingerprint) {
|
||||
requestError = 'The authoritative fingerprint is unavailable. Reload before revoking trust.';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,12 +204,10 @@
|
||||
operation.runtimeId,
|
||||
request,
|
||||
trust.fingerprint,
|
||||
revokeFingerprintConfirmation,
|
||||
trust.fingerprint,
|
||||
);
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
publicKey = '';
|
||||
fingerprintConfirmation = '';
|
||||
revokeFingerprintConfirmation = '';
|
||||
showPublicKey = false;
|
||||
revealedPublicKey = null;
|
||||
successMessage = 'Workspace trust was revoked.';
|
||||
@@ -242,6 +225,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRegistration(): Promise<void> {
|
||||
if (busyAction !== null || !data.runtimeDetail) return;
|
||||
const runtime = data.runtimeDetail.runtime;
|
||||
if (runtime.management.built_in) return;
|
||||
if (deleteRuntimeConfirmation.trim() !== data.runtimeId) {
|
||||
deleteRuntimeError = 'Enter the Runtime ID exactly to confirm deletion.';
|
||||
return;
|
||||
}
|
||||
|
||||
const operation = routeFence.capture(data.runtimeId);
|
||||
busyAction = 'delete';
|
||||
deleteRuntimeError = null;
|
||||
try {
|
||||
if (data.runtimeDetail.trust_key.status !== 'revoked') {
|
||||
const trust = data.runtimeDetail.trust_key;
|
||||
if (trust.revision == null || !trust.fingerprint) {
|
||||
throw new Error('Runtime trust revision and fingerprint are required before deletion.');
|
||||
}
|
||||
await revokeRuntimeTrustKey(
|
||||
data.workspaceId,
|
||||
operation.runtimeId,
|
||||
{ expected_revision: trust.revision },
|
||||
trust.fingerprint,
|
||||
trust.fingerprint,
|
||||
);
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
}
|
||||
await deleteRemoteRuntime(data.workspaceId, operation.runtimeId);
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
await goto(`/w/${encodeURIComponent(data.workspaceId)}/settings/runtimes`, {
|
||||
replaceState: true,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
deleteRuntimeError = error instanceof Error
|
||||
? error.message
|
||||
: 'Runtime registration deletion failed.';
|
||||
} finally {
|
||||
if (isCurrentRoute(operation)) busyAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePublicKeyReveal(): Promise<void> {
|
||||
if (showPublicKey) {
|
||||
showPublicKey = false;
|
||||
@@ -397,18 +422,6 @@
|
||||
<p class="field-error">{replacementFingerprintError}</p>
|
||||
{/if}
|
||||
|
||||
{#if currentAction !== 'create'}
|
||||
<label for="runtime-fingerprint-confirmation">Confirm current fingerprint</label>
|
||||
<input
|
||||
id="runtime-fingerprint-confirmation"
|
||||
bind:value={fingerprintConfirmation}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder={trust.fingerprint ?? ''}
|
||||
/>
|
||||
<small>Enter <code>{trust.fingerprint ?? 'the current fingerprint'}</code> exactly.</small>
|
||||
{/if}
|
||||
|
||||
{#if fieldError}
|
||||
<p id="runtime-public-key-error" class="field-error">{fieldError}</p>
|
||||
{/if}
|
||||
@@ -423,25 +436,11 @@
|
||||
<div>
|
||||
<strong>Revoke Workspace trust</strong>
|
||||
<p>Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.</p>
|
||||
<label>
|
||||
Confirm current fingerprint
|
||||
<input
|
||||
bind:value={revokeFingerprintConfirmation}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
disabled={trust.status !== 'active' || busyAction !== null}
|
||||
/>
|
||||
<small>Enter <code>{trust.fingerprint ?? 'the current fingerprint'}</code> exactly before revocation.</small>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="danger"
|
||||
disabled={
|
||||
busyAction !== null ||
|
||||
trust.status !== 'active' ||
|
||||
revokeFingerprintConfirmation.trim() !== trust.fingerprint
|
||||
}
|
||||
disabled={busyAction !== null || trust.status !== 'active'}
|
||||
onclick={revokeTrust}
|
||||
>{busyAction === 'revoke' ? 'Revoking…' : 'Revoke trust'}</button>
|
||||
</div>
|
||||
@@ -480,5 +479,45 @@
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if data.workspace.permissions.manage_runtimes && !runtime.management.built_in}
|
||||
<section class="runtime-detail-section runtime-danger-zone" aria-labelledby="runtime-delete-heading">
|
||||
<h2 id="runtime-delete-heading">Delete Runtime registration</h2>
|
||||
<p>
|
||||
Remove this Runtime binding from the current Workspace. This does not stop the Runtime process,
|
||||
delete its Workers or Workdirs, or revoke this Workspace on the Runtime host.
|
||||
</p>
|
||||
{#if trust.status !== 'revoked'}
|
||||
<p class="section-state warning">
|
||||
Deletion will revoke this Workspace trust first. Stop or move active Workers before continuing.
|
||||
</p>
|
||||
{/if}
|
||||
<label for="runtime-delete-confirmation">Confirm Runtime ID</label>
|
||||
<input
|
||||
id="runtime-delete-confirmation"
|
||||
bind:value={deleteRuntimeConfirmation}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
disabled={busyAction !== null}
|
||||
placeholder={data.runtimeId}
|
||||
/>
|
||||
<small>Enter <code>{data.runtimeId}</code> exactly.</small>
|
||||
{#if deleteRuntimeError}
|
||||
<p class="section-state error" role="alert">{deleteRuntimeError}</p>
|
||||
{/if}
|
||||
<div class="settings-action-row">
|
||||
<button
|
||||
type="button"
|
||||
class="danger"
|
||||
disabled={busyAction !== null || deleteRuntimeConfirmation.trim() !== data.runtimeId}
|
||||
onclick={deleteRegistration}
|
||||
>{busyAction === 'delete'
|
||||
? 'Deleting…'
|
||||
: trust.status === 'revoked'
|
||||
? 'Delete registration'
|
||||
: 'Revoke trust and delete registration'}</button>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -44,6 +44,48 @@ Deno.test("Runtime Settings routes validate unknown JSON through the shared Runt
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime registration presents the complete multi-Workspace trust sequence", async () => {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../src/routes/w/[workspaceId]/settings/runtimes/+page.svelte",
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
|
||||
for (
|
||||
const token of [
|
||||
"1. Trust this Workspace on the Runtime",
|
||||
"Provision Workspace identity",
|
||||
"provisionWorkspaceSigningIdentity(data.workspaceId)",
|
||||
"Existing Workspace trust entries are preserved.",
|
||||
"--fs-root",
|
||||
"--fs-runtime-dir",
|
||||
"2. Verify the Runtime identity",
|
||||
"yoi-runtime identity show --json",
|
||||
"3. Register the connection",
|
||||
"Register Runtime",
|
||||
"Run Test to complete authenticated verification.",
|
||||
]
|
||||
) {
|
||||
assert(
|
||||
page.includes(token),
|
||||
`Runtime registration should include ${token}`,
|
||||
);
|
||||
}
|
||||
assert(
|
||||
page.includes(
|
||||
"data.signingIdentity?.identity.state === 'pending_provisioning'",
|
||||
) &&
|
||||
page.includes("Loading Workspace public identity…"),
|
||||
"pending identity must have a dedicated provisioning state before loading fallback",
|
||||
);
|
||||
assert(
|
||||
!page.includes("fingerprintConfirmation") &&
|
||||
!page.includes("Confirm Runtime fingerprint"),
|
||||
"Runtime registration must not require retyping a fingerprint",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime list links to canonical detail and has no inline delete action", async () => {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
@@ -111,22 +153,26 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
|
||||
"Create Workspace trust",
|
||||
"Replace trusted key",
|
||||
"Reactivate with this key",
|
||||
"Confirm current fingerprint",
|
||||
"Revoke Workspace trust",
|
||||
"Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.",
|
||||
"await revokeRuntimeTrustKey(",
|
||||
"deleteRemoteRuntime(data.workspaceId, operation.runtimeId)",
|
||||
"Revoke trust and delete registration",
|
||||
"trust.status !== 'revoked'",
|
||||
"deleteRuntimeConfirmation.trim() !== data.runtimeId",
|
||||
"Delete Runtime registration",
|
||||
"Delete registration",
|
||||
"This does not stop the Runtime process",
|
||||
"RuntimeTrustConflictError",
|
||||
"RuntimeTrustRouteFence",
|
||||
"routeFence.enter(data.runtimeId)",
|
||||
"showPublicKey = false",
|
||||
"revealedPublicKey = null",
|
||||
"publicKey = ''",
|
||||
"fingerprintConfirmation = ''",
|
||||
"revokeFingerprintConfirmation = ''",
|
||||
"requestError = null",
|
||||
"successMessage = null",
|
||||
"isCurrentRoute(operation)",
|
||||
"revealRuntimeTrustKey",
|
||||
"revokeFingerprintConfirmation.trim() !== trust.fingerprint",
|
||||
"await reloadAuthority()",
|
||||
"busyAction !== null",
|
||||
"Workdirs",
|
||||
@@ -135,6 +181,15 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
|
||||
) {
|
||||
assert(page.includes(token), `Runtime detail should include ${token}`);
|
||||
}
|
||||
for (
|
||||
const token of [
|
||||
"fingerprintConfirmation",
|
||||
"revokeFingerprintConfirmation",
|
||||
"Confirm current fingerprint",
|
||||
]
|
||||
) {
|
||||
assert(!page.includes(token), `Runtime detail must not require ${token}`);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime detail uses flat sections instead of nested cards", async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ declare const Deno: {
|
||||
|
||||
import {
|
||||
createRemoteRuntime,
|
||||
deleteRemoteRuntime,
|
||||
parseRuntimeTrustConflict,
|
||||
parseRuntimeTrustKeyRevealResponse,
|
||||
parseWorkspaceRuntimeDetail,
|
||||
@@ -272,6 +273,24 @@ Deno.test("mismatched revoke fingerprint never sends a request", async () => {
|
||||
assert(requests === 0, "mismatched fingerprint sent a revoke request");
|
||||
});
|
||||
|
||||
Deno.test("Runtime registration delete uses the Workspace-scoped resource route", async () => {
|
||||
let requestedUrl = "";
|
||||
let requestedMethod = "";
|
||||
const fetchImpl = ((input: string | URL | Request, init?: RequestInit) => {
|
||||
requestedUrl = String(input);
|
||||
requestedMethod = init?.method ?? "GET";
|
||||
return Promise.resolve(new Response(null, { status: 204 }));
|
||||
}) as typeof fetch;
|
||||
|
||||
await deleteRemoteRuntime("workspace a", "runtime/a", fetchImpl);
|
||||
|
||||
assert(
|
||||
requestedUrl === "/api/w/workspace%20a/runtimes/runtime%2Fa",
|
||||
`unexpected delete URL: ${requestedUrl}`,
|
||||
);
|
||||
assert(requestedMethod === "DELETE", "Runtime delete must use DELETE");
|
||||
});
|
||||
|
||||
Deno.test("Runtime route fence rejects a delayed reveal from the prior Runtime", async () => {
|
||||
const fence = new RuntimeTrustRouteFence();
|
||||
fence.enter("runtime-a");
|
||||
|
||||
Reference in New Issue
Block a user