fix: separate workspace settings display and editing

This commit is contained in:
2026-10-01 15:14:57 +09:00
parent 8e0dad98e8
commit d066f3963f
11 changed files with 1551 additions and 401 deletions
+10
View File
@@ -198,6 +198,16 @@ Home browser regression (`browser-tests/workspace_home_shell_test.ts`, included
also checks 320px, both themes, keyboard record navigation, bounded loading, independent slow/error
feeds, explicit refresh, and absence of Host requests.
## Workspace Identity settings fixture
`scenarios/workspace-settings.json` uses the same owned fixture server with synthetic metadata,
public identity, and deletion-preflight responses. It covers owner, member, long name, pending
provisioning, and intentional signing-identity HTTP503 states at 1440px, 768px, and 390px.
Build `web/workspace` before capture. No real keys, accounts, or deletion requests are used.
`browser-tests/workspace_settings_shell_test.ts` also checks 320px and both themes, Edit/Save/Cancel,
keyboard disclosure/focus return, full public bundle access, scoped errors, and exact-name deletion
confirmation. Interaction screenshots are saved under `target/web-ux/settings-interactions`.
## Adding a scenario
1. Name the concrete user task and expected data state; do not write “looks correct”.
@@ -1,9 +1,14 @@
import { extname, join, normalize } from "jsr:@std/path@1.1.4";
import { dashboardFixture } from "../../../web/workspace/src/lib/workspace/home/dashboard.test-fixtures.ts";
import { settingsFixtureHandler } from "../../../web/workspace/src/lib/workspace/settings/identity.test-fixtures.ts";
const settingsFixture = settingsFixtureHandler();
const port = Number(Deno.args[0]);
const buildRoot = Deno.args[1];
if (!Number.isInteger(port) || !buildRoot) throw new Error("usage: server <port> <build-root>");
if (!Number.isInteger(port) || !buildRoot) {
throw new Error("usage: server <port> <build-root>");
}
const names: Record<string, string> = {
"home-owner": "Workspace Home Review",
"home-member": "Shared Workspace",
@@ -25,6 +30,8 @@ const mime: Record<string, string> = {
Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
const url = new URL(request.url);
if (url.pathname === "/health") return new Response("ok");
const settingsResponse = await settingsFixture(request);
if (settingsResponse) return settingsResponse;
if (url.pathname === "/api/workspaces") {
return json(
Object.entries(names).map(([workspace_id, display_name]) => ({
@@ -69,13 +76,26 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
extension_points: {
store: "fixture",
event_stream: { status: "ready", note: "fixture", diagnostics: [] },
host_worker_bridge: { status: "ready", note: "fixture", diagnostics: [] },
companion_console: { status: "ready", note: "fixture", diagnostics: [] },
host_worker_bridge: {
status: "ready",
note: "fixture",
diagnostics: [],
},
companion_console: {
status: "ready",
note: "fixture",
diagnostics: [],
},
},
});
}
if (path === "/repositories") {
return json({ workspace_id: workspaceId, items: [], source: "fixture", diagnostics: [] });
return json({
workspace_id: workspaceId,
items: [],
source: "fixture",
diagnostics: [],
});
}
if (path === "/working-directories") {
return json({ workspace_id: workspaceId, items: [], diagnostics: [] });
@@ -110,7 +130,9 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
record_source: "fixture",
});
}
if (path === "/protocol/ws" && request.headers.get("upgrade") === "websocket") {
if (
path === "/protocol/ws" && request.headers.get("upgrade") === "websocket"
) {
const { socket, response } = Deno.upgradeWebSocket(request);
socket.onmessage = (event) => {
const frame = JSON.parse(String(event.data));
@@ -135,12 +157,16 @@ Deno.serve({ hostname: "127.0.0.1", port }, async (request) => {
return json({ error: "Unexpected fixture API request" }, 404);
}
const relative = normalize(url.pathname.replace(/^\/+/, "") || "index.html");
if (relative.startsWith("..")) return new Response("not found", { status: 404 });
if (relative.startsWith("..")) {
return new Response("not found", { status: 404 });
}
let file = join(buildRoot, relative);
try {
if ((await Deno.stat(file)).isDirectory) file = join(file, "index.html");
return new Response(await Deno.readFile(file), {
headers: { "content-type": mime[extname(file)] ?? "application/octet-stream" },
headers: {
"content-type": mime[extname(file)] ?? "application/octet-stream",
},
});
} catch {
return new Response(await Deno.readFile(join(buildRoot, "index.html")), {
@@ -0,0 +1,301 @@
/// <reference lib="dom" />
import { assert, assertEquals } from "@std/assert";
import { dirname, fromFileUrl, join } from "@std/path";
import { chromium, type Page } from "playwright";
import { metadataFixture } from "../../../web/workspace/src/lib/workspace/settings/identity.test-fixtures.ts";
const root = join(dirname(fromFileUrl(import.meta.url)), "../../..");
const evidence = join(root, "target/web-ux/settings-interactions");
async function ready(page: Page, owner = true) {
await page.locator("[data-name-ready=true]").waitFor();
if (owner) await page.locator("[data-identity-ready=true]").waitFor();
}
async function noOverflow(page: Page) {
assertEquals(
await page.locator("main").evaluate((main) => ({
page: document.documentElement.scrollWidth > innerWidth,
main: main.scrollWidth > main.clientWidth,
})),
{ page: false, main: false },
);
}
Deno.test("production Workspace Settings separates read/edit, discloses details, and preserves keyboard and destructive safety", async () => {
const build = await new Deno.Command(Deno.execPath(), {
args: ["task", "build"],
cwd: join(root, "web/workspace"),
stdout: "piped",
stderr: "piped",
}).output();
assert(build.success, new TextDecoder().decode(build.stderr));
await Deno.mkdir(evidence, { recursive: true });
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
const port = (listener.addr as Deno.NetAddr).port;
listener.close();
const url = `http://127.0.0.1:${port}`;
const server = new Deno.Command(Deno.execPath(), {
args: [
"run",
"--allow-net",
"--allow-read",
join(root, "tools/web-ux/browser-tests/workspace_home_fixture_server.ts"),
String(port),
join(root, "web/workspace/build"),
],
stdout: "null",
stderr: "null",
}).spawn();
try {
let started = false;
for (let n = 0; n < 100; n++) {
try {
const response = await fetch(`${url}/health`);
await response.body?.cancel();
if (response.ok) {
started = true;
break;
}
} catch { /* starting */ }
await new Promise((resolve) => setTimeout(resolve, 50));
}
assert(started);
const browser = await chromium.launch({ headless: true });
try {
for (const colorScheme of ["light", "dark"] as const) {
for (const width of [1440, 768, 390, 320]) {
const context = await browser.newContext({
viewport: { width, height: 900 },
colorScheme,
reducedMotion: "reduce",
});
try {
const page = await context.newPage();
const errors: string[] = [];
const mutations: string[] = [];
page.on("pageerror", (error) => errors.push(String(error)));
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
page.on("requestfailed", (request) => errors.push(request.url()));
page.on("request", (request) => {
if (request.method() !== "GET") mutations.push(request.url());
});
for (
const workspace of ["home-owner", "home-member", "home-long"]
) {
const owner = workspace !== "home-member";
await page.goto(`${url}/w/${workspace}/settings`);
await ready(page, owner);
assertEquals(await page.locator("main h1").allTextContents(), [
"Workspace Identity",
]);
assertEquals(
await page.locator("main input:visible, main textarea:visible")
.count(),
0,
);
assertEquals(await page.locator("main details[open]").count(), 0);
assertEquals(await page.locator("main .badge").count(), 0);
assertEquals(
await page.getByRole("button", { name: "Copy bundle" }).count(),
owner ? 1 : 0,
);
const edit = page.getByRole("button", { name: "Edit name" });
assert((await edit.boundingBox())!.y < 900);
await noOverflow(page);
const saved = await page.locator(".saved-name").innerText();
await edit.focus();
await page.keyboard.press("Enter");
const input = page.getByRole("textbox", {
name: "New display name",
});
assertEquals(
await input.evaluate((node) => node === document.activeElement),
true,
);
assertEquals(await input.inputValue(), saved);
await input.fill("Discard draft");
if (workspace === "home-long") {
await page.screenshot({
path: join(evidence, `edit-${colorScheme}-${width}.png`),
});
}
await page.getByRole("button", { name: "Cancel", exact: true })
.click();
assertEquals(
await edit.evaluate((node) => node === document.activeElement),
true,
);
assertEquals(
await page.locator(".saved-name").innerText(),
saved,
);
const technical = page.getByText("Technical details", {
exact: true,
});
await technical.focus();
await page.keyboard.press("Enter");
assertEquals(
await page.getByText("Source", { exact: true }).isVisible(),
true,
);
await page.keyboard.press("Enter");
assertEquals(
await page.getByText("Source", { exact: true }).isVisible(),
false,
);
if (owner) {
const bundle = page.getByText("Public bundle and key details", {
exact: true,
});
await bundle.focus();
await page.keyboard.press("Enter");
assertEquals(
await page.getByRole("textbox", {
name: "Public identity bundle",
}).isVisible(),
true,
);
assertEquals(
await page.getByRole("textbox", {
name: "Public identity bundle",
}).getAttribute("readonly"),
"",
);
await noOverflow(page);
if (workspace === "home-owner") {
await page.screenshot({
path: join(evidence, `bundle-${colorScheme}-${width}.png`),
});
}
await bundle.click();
const danger = page.locator(".deletion > summary");
await danger.focus();
await page.keyboard.press("Enter");
await page.getByRole("button", {
name: "Review deletion impact",
}).click();
const confirmation = page.getByRole("textbox", {
name: /^Type .* to confirm$/,
});
await confirmation.waitFor();
assertEquals(
await confirmation.evaluate((node) => node === document.activeElement),
true,
);
const deletion = page.getByRole("button", {
name: "Delete Workspace",
exact: true,
});
assertEquals(await deletion.isDisabled(), true);
await confirmation.fill("wrong");
assertEquals(await deletion.isDisabled(), true);
await confirmation.fill(saved);
assertEquals(await deletion.isDisabled(), false);
await noOverflow(page);
if (workspace === "home-owner") {
await page.screenshot({
path: join(evidence, `delete-${colorScheme}-${width}.png`),
});
}
await page.getByRole("button", { name: "Cancel", exact: true })
.click();
assertEquals(
await danger.evaluate((node) => node === document.activeElement),
true,
);
assertEquals(await page.locator(".deletion[open]").count(), 0);
}
}
assertEquals(mutations, []);
assertEquals(errors, []);
await page.getByRole("button", { name: "Edit name" }).click();
const renamed = `Confirmed name ${width} ${colorScheme}`;
await page.getByRole("textbox", { name: "New display name" }).fill(
renamed,
);
await page.getByRole("button", { name: "Save name" }).click();
await page.getByText("Workspace name saved.", { exact: true })
.waitFor();
assertEquals(
await page.locator(".saved-name").innerText(),
renamed,
);
assertEquals(await page.locator("main input:visible").count(), 0);
assertEquals(mutations.length, 1);
assertEquals(errors, []);
// Restore the synthetic name so every width actually exercises long text.
const currentResponse = await context.request.get(`${url}/api/w/home-long/settings`);
const current = await currentResponse.json();
const reset = await context.request.put(`${url}/api/w/home-long/settings`, {
data: {
display_name: metadataFixture("home-long").display_name,
revision: current.revision,
},
});
assertEquals(reset.status(), 200);
} finally {
await context.close();
}
}
}
const page = await browser.newPage({
viewport: { width: 390, height: 844 },
});
try {
await page.goto(`${url}/w/home-empty/settings`);
await ready(page);
await page.getByText("Not provisioned", { exact: true }).waitFor();
await page.getByRole("button", { name: "Provision identity" }).click();
await page.getByRole("button", { name: "Copy bundle" }).waitFor();
await page.goto(`${url}/w/home-error/settings`);
await ready(page);
assertEquals(
await page.getByRole("button", { name: "Edit name" }).isVisible(),
true,
);
assertEquals(
await page.getByRole("button", { name: "Retry identity" })
.isVisible(),
true,
);
await page.goto(`${url}/w/home-owner/settings`);
await ready(page);
await page.route("**/settings", async (route) => {
if (route.request().method() === "PUT") {
await route.fulfill({
status: 409,
body: "Workspace metadata changed. Reload the saved name.",
});
} else await route.continue();
});
await page.getByRole("button", { name: "Edit name" }).click();
await page.getByRole("textbox", { name: "New display name" }).fill(
"Keep this draft",
);
await page.getByRole("button", { name: "Save name" }).click();
await page.getByRole("alert").waitFor();
assertEquals(
await page.getByRole("textbox", { name: "New display name" })
.inputValue(),
"Keep this draft",
);
await page.screenshot({
path: join(evidence, "save-error-mobile.png"),
});
await page.getByRole("button", { name: "Reload saved name" }).click();
await page.getByRole("button", { name: "Edit name" }).waitFor();
assertEquals(await page.getByRole("alert").count(), 0);
} finally {
await page.close();
}
} finally {
await browser.close();
}
} finally {
try {
server.kill("SIGTERM");
} catch (error) {
if (!(error instanceof Deno.errors.NotFound)) throw error;
}
await server.status;
}
});
+1 -1
View File
@@ -11,7 +11,7 @@
"web-ux": "deno run --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys cli.ts",
"check": "deno check cli.ts src/*.ts tests/*.ts browser-tests/*.ts",
"test": "deno test --allow-env --allow-read --allow-write --allow-run --allow-sys tests",
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts"
"test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts browser-tests/memory_document_shell_test.ts browser-tests/console_history_shell_test.ts browser-tests/workspace_home_shell_test.ts browser-tests/workspace_settings_shell_test.ts"
},
"fmt": {
"lineWidth": 100,
@@ -0,0 +1,137 @@
{
"schemaVersion": 1,
"id": "workspace-settings",
"title": "Workspace Identity settings disclosure",
"baseUrl": "http://127.0.0.1:15282",
"colorScheme": "light",
"reducedMotion": "reduce",
"personas": [
{
"id": "fixture",
"label": "Synthetic Workspace permission projections",
"auth": {
"kind": "anonymous"
}
}
],
"viewports": [
{
"label": "desktop",
"width": 1440,
"height": 900
},
{
"label": "narrow",
"width": 768,
"height": 900
},
{
"label": "mobile",
"width": 390,
"height": 844
}
],
"routes": [
{
"id": "owner",
"label": "Owner settings",
"path": "/w/home-owner/settings",
"goal": "Read saved name and identity state without permanent edit forms or public JSON.",
"dataState": "Active public identity and saved name.",
"ready": {
"kind": "selector",
"selector": "main:has(#workspace-settings-title):not(:has-text(\"Loading\"))"
},
"capturePoints": [
{
"id": "initial",
"label": "Saved settings"
}
]
},
{
"id": "member",
"label": "Member settings",
"path": "/w/home-member/settings",
"goal": "Keep permitted metadata editing without owner identity or deletion controls.",
"dataState": "All owner permissions false; metadata read/write remains available under existing Backend contract.",
"ready": {
"kind": "selector",
"selector": "main:has(#workspace-settings-title):not(:has-text(\"Loading\"))"
},
"capturePoints": [
{
"id": "initial",
"label": "Member view"
}
]
},
{
"id": "long",
"label": "Long name",
"path": "/w/home-long/settings",
"goal": "Keep controls in the viewport and preserve complete long names.",
"dataState": "Long Workspace display name and active identity.",
"ready": {
"kind": "selector",
"selector": "main:has(#workspace-settings-title):not(:has-text(\"Loading\"))"
},
"capturePoints": [
{
"id": "initial",
"label": "Long name"
}
]
},
{
"id": "pending",
"label": "Pending identity",
"path": "/w/home-empty/settings",
"goal": "Show provisioning attention without pretending the identity is active.",
"dataState": "Identity pending_provisioning; metadata available.",
"ready": {
"kind": "selector",
"selector": "main:has(#workspace-settings-title):not(:has-text(\"Loading\"))"
},
"capturePoints": [
{
"id": "initial",
"label": "Provisioning required"
}
]
},
{
"id": "error",
"label": "Identity failure",
"path": "/w/home-error/settings",
"goal": "Keep name usable when public identity fails and offer scoped recovery.",
"dataState": "Intentional signing identity HTTP503 with its expected console diagnostic.",
"ready": {
"kind": "selector",
"selector": "main:has(#workspace-settings-title):not(:has-text(\"Loading\"))"
},
"capturePoints": [
{
"id": "initial",
"label": "Scoped error"
}
]
}
],
"processes": [
{
"id": "settings-fixture",
"command": "deno",
"cwd": "..",
"args": [
"run",
"--allow-net",
"--allow-read",
"browser-tests/workspace_home_fixture_server.ts",
"15282",
"../../web/workspace/build"
],
"readyUrl": "http://127.0.0.1:15282/health"
}
]
}
+1
View File
@@ -95,6 +95,7 @@ Use semantic tokens from `app.css`. Do not introduce component-local color, spac
- `src/routes/w/[workspaceId]/+layout.svelte`: Workspace Header and Sidebar registration.
- `src/routes/w/[workspaceId]/settings/+layout.svelte`: Settings Sidebar registration.
- `src/lib/workspace/home/`: bounded, read-only work dashboard feeds and their presentation.
- `src/lib/workspace/settings/WorkspaceIdentitySettings.svelte`: Workspace-scoped name editing, public identity disclosure, and deletion confirmation.
- `src/lib/workspace/header/`: Header frame, context, and overrides.
- `src/lib/workspace/sidebar/`: Sidebar frame, scoped content, contexts, and override stack.
- `src/lib/workspace/markdown/`: shared safe Markdown parsing plus presentation-specific document renderers.
@@ -0,0 +1,396 @@
<script lang="ts">
import { invalidate, goto } from '$app/navigation';
import { onMount, onDestroy, tick } from 'svelte';
import type {
Diagnostic, WorkspaceMetadataSettingsResponse, WorkspaceSigningIdentityResponse,
WorkspaceDeletionPreflightResponse, WorkspaceDeletionOperationResponse, WorkspaceDeletionRequest,
} from '$lib/generated/legacy-server-api';
import { workspaceApiPath } from '$lib/workspace/api/http';
import { disposeWorkspaceMultiplexer } from '$lib/workspace/multiplexer';
import { disposeWorkspaceWorkersStore } from '$lib/workspace/sidebar/worker-subscription';
import { fetchWorkspaceMetadata, updateWorkspaceMetadata, fetchWorkspaceSigningIdentity, provisionWorkspaceSigningIdentity } from './profile-api';
import { preflightWorkspaceDeletion, startWorkspaceDeletion, getWorkspaceDeletion } from './workspace-deletion-api';
import BevelLine from '$lib/workspace/ui/BevelLine.svelte';
// The route keys this component by Workspace and owner permission. Requests from
// a disposed scope must not publish state, focus, or navigation into a new one.
let { workspaceId, workspaceName, canDelete }: { workspaceId: string; workspaceName: string; canDelete: boolean } = $props();
let disposed = false;
onDestroy(() => { disposed = true; });
let workspaceMetadata = $state<WorkspaceMetadataSettingsResponse | null>(null);
let loading = $state(true);
let metadataError = $state<string | null>(null);
let diagnostics = $state<Diagnostic[]>([]);
let editing = $state(false);
let displayNameDraft = $state('');
let submitting = $state(false);
let nameError = $state<string | null>(null);
let nameNotice = $state<string | null>(null);
let editButton = $state<HTMLButtonElement>();
let identityCopyButton = $state<HTMLButtonElement>();
let provisionButton = $state<HTMLButtonElement>();
let signingIdentity = $state<WorkspaceSigningIdentityResponse | null>(null);
let identityLoading = $state(true);
let identityError = $state<string | null>(null);
let provisioningIdentity = $state(false);
let copyError = $state<string | null>(null);
let identityCopied = $state(false);
let identityBundleText = $derived(signingIdentity?.public_bundle ? JSON.stringify(signingIdentity.public_bundle, null, 2) : '');
let dangerOpen = $state(false);
let dangerSummary = $state<HTMLElement>();
let deletionOpen = $state(false);
let deletionLoading = $state(false);
let deletionSubmitting = $state(false);
let deletionConfirmation = $state('');
let deletionPreflight = $state<WorkspaceDeletionPreflightResponse | null>(null);
let deletionOperation = $state<WorkspaceDeletionOperationResponse | null>(null);
let deletionRequest = $state<WorkspaceDeletionRequest | null>(null);
let deletionError = $state<string | null>(null);
const errorText = (error: unknown, fallback: string) => error instanceof Error ? error.message : fallback;
const focusInput = (element: HTMLElement) => { element.focus(); };
const deletionStorageKey = () => `yoi:workspace-deletion:${workspaceId}`;
async function loadMetadata() {
loading = true;
metadataError = null;
try {
const response = await fetchWorkspaceMetadata(workspaceId);
if (disposed) return;
workspaceMetadata = response;
displayNameDraft = response.display_name;
diagnostics = response.diagnostics;
} catch (error) {
if (!disposed) metadataError = errorText(error, 'Workspace settings unavailable.');
} finally {
if (!disposed) loading = false;
}
}
async function loadIdentity() {
if (!canDelete) return;
identityLoading = true;
identityError = null;
try {
const response = await fetchWorkspaceSigningIdentity(workspaceId);
if (!disposed) signingIdentity = response;
} catch (error) {
if (!disposed) identityError = errorText(error, 'Public identity unavailable.');
} finally {
if (!disposed) identityLoading = false;
}
}
async function retryMetadata() {
await loadMetadata();
await tick();
if (!disposed && !metadataError) editButton?.focus();
}
async function retryIdentity() {
await loadIdentity();
await tick();
if (!disposed && !identityError) (identityCopyButton ?? provisionButton)?.focus();
}
function editName() {
if (!workspaceMetadata) return;
displayNameDraft = workspaceMetadata.display_name;
nameError = nameNotice = null;
editing = true;
}
async function cancelName() {
editing = false;
nameError = null;
displayNameDraft = workspaceMetadata?.display_name ?? '';
await tick();
if (!disposed) editButton?.focus();
}
async function reloadName() {
await loadMetadata();
if (!disposed && !metadataError) await cancelName();
}
async function submitWorkspaceName() {
if (!workspaceMetadata || submitting || !displayNameDraft.trim()) return;
submitting = true;
nameError = nameNotice = null;
try {
const response = await updateWorkspaceMetadata(workspaceId, {
display_name: displayNameDraft, revision: workspaceMetadata.revision,
});
if (disposed) return;
workspaceMetadata = response.workspace;
diagnostics = response.diagnostics.concat(response.workspace.diagnostics);
nameNotice = 'Workspace name saved.';
await cancelName();
if (disposed) return;
try { await invalidate(workspaceApiPath(workspaceId, '/workspace')); }
catch { if (!disposed) nameNotice = 'Workspace name saved. Reload the page to update navigation.'; }
} catch (error) {
if (!disposed) nameError = errorText(error, 'Workspace name could not be saved.');
} finally {
if (!disposed) submitting = false;
}
}
async function provisionIdentity() {
if (!canDelete || provisioningIdentity) return;
provisioningIdentity = true;
identityError = null;
try {
const response = await provisionWorkspaceSigningIdentity(workspaceId);
if (disposed) return;
signingIdentity = response;
await tick();
if (!disposed) identityCopyButton?.focus();
} catch (error) {
if (!disposed) identityError = errorText(error, 'Identity provisioning failed.');
} finally {
if (!disposed) provisioningIdentity = false;
}
}
async function copyIdentityBundle() {
if (!identityBundleText) return;
copyError = null;
identityCopied = false;
try {
await navigator.clipboard.writeText(identityBundleText);
if (!disposed) identityCopied = true;
} catch {
if (!disposed) copyError = 'Could not copy. Open Public bundle and key details to select the bundle manually.';
}
}
async function openDeletionConfirmation() {
if (!canDelete || editing || submitting || provisioningIdentity || deletionLoading || deletionSubmitting) return;
deletionOpen = true;
deletionLoading = true;
deletionError = null;
deletionPreflight = deletionOperation = deletionRequest = null;
deletionConfirmation = '';
try {
sessionStorage.removeItem(deletionStorageKey());
const response = await preflightWorkspaceDeletion(workspaceId);
if (!disposed) deletionPreflight = response;
} catch (error) {
if (!disposed) deletionError = errorText(error, 'Workspace deletion preflight failed.');
} finally {
if (!disposed) deletionLoading = false;
}
}
async function cancelDeletion() {
deletionOpen = dangerOpen = false;
await tick();
if (!disposed) dangerSummary?.focus();
}
async function trackDeletion(operationId: string) {
let operation = await getWorkspaceDeletion(operationId);
if (disposed) return;
deletionOperation = operation;
while (operation.state === 'queued' || operation.state === 'running') {
await new Promise((resolve) => setTimeout(resolve, 500));
if (disposed) return;
operation = await getWorkspaceDeletion(operation.operation_id);
if (disposed) return;
deletionOperation = operation;
}
if (operation.state === 'succeeded') {
sessionStorage.removeItem(deletionStorageKey());
disposeWorkspaceMultiplexer(workspaceId);
disposeWorkspaceWorkersStore(workspaceId);
await goto('/');
}
}
function storedDeletionRequest(): WorkspaceDeletionRequest | null {
try {
const value: unknown = JSON.parse(sessionStorage.getItem(deletionStorageKey()) ?? 'null');
if (typeof value !== 'object' || value === null) return null;
const record = value as Record<string, unknown>;
if (
Object.keys(record).sort().join(',') !== 'confirmation,expected_revision,operation_id' ||
typeof record.operation_id !== 'string' || record.operation_id.length === 0 || record.operation_id.length > 128 ||
!/^[A-Za-z0-9_-]+$/.test(record.operation_id) ||
typeof record.expected_revision !== 'string' || record.expected_revision.length > 128 ||
typeof record.confirmation !== 'string' || record.confirmation !== workspaceName || record.confirmation.length > 256
) return null;
return { operation_id: record.operation_id, expected_revision: record.expected_revision, confirmation: record.confirmation };
} catch { return null; }
}
onMount(() => {
void loadMetadata();
void loadIdentity();
if (!canDelete) return;
const request = storedDeletionRequest();
if (!request) return;
deletionRequest = request;
deletionConfirmation = request.confirmation;
dangerOpen = deletionOpen = deletionSubmitting = true;
void trackDeletion(request.operation_id)
.catch((error) => { if (!disposed) deletionError = errorText(error, 'Workspace deletion status failed.'); })
.finally(() => { if (!disposed) deletionSubmitting = false; });
});
async function deleteWorkspace() {
if (!canDelete || deletionSubmitting || (!deletionPreflight && !deletionRequest)) return;
if (!deletionRequest && (!deletionPreflight?.can_delete || deletionConfirmation !== deletionPreflight.display_name)) return;
deletionSubmitting = true;
deletionError = null;
try {
const request = deletionRequest ?? {
operation_id: crypto.randomUUID(), expected_revision: deletionPreflight!.expected_revision, confirmation: deletionConfirmation,
};
deletionRequest = request;
sessionStorage.setItem(deletionStorageKey(), JSON.stringify(request));
const operation = await startWorkspaceDeletion(workspaceId, request);
if (disposed) return;
deletionOperation = operation;
await trackDeletion(operation.operation_id);
} catch (error) {
if (!disposed) deletionError = errorText(error, 'Workspace deletion failed.');
} finally {
if (!disposed) deletionSubmitting = false;
}
}
</script>
<div class="identity-settings">
<h1 id="workspace-settings-title">Workspace Identity</h1>
<section aria-labelledby="workspace-name-title" data-name-ready={!loading}>
<div class="section-heading">
<h2 id="workspace-name-title">Display name</h2>
{#if workspaceMetadata && !editing}
<button bind:this={editButton} type="button" disabled={deletionOpen || deletionSubmitting} onclick={editName}>Edit name</button>
{/if}
</div>
{#if loading}<p class="muted" role="status">Loading workspace settings…</p>{/if}
{#if metadataError}
<p class="error" role="alert">{metadataError}</p>
<button type="button" onclick={retryMetadata} disabled={loading}>Retry name</button>
{/if}
{#if workspaceMetadata}
{#if editing}
<form onsubmit={(event) => { event.preventDefault(); void submitWorkspaceName(); }} aria-label="Edit Workspace name">
<label>New display name<input use:focusInput bind:value={displayNameDraft} required autocomplete="off" disabled={submitting || loading} aria-invalid={Boolean(nameError)} /></label>
<p class="muted">Current: {workspaceMetadata.display_name}</p>
{#if nameError}<p class="error" role="alert">{nameError}</p>{/if}
<div class="actions">
<button class="primary" type="submit" disabled={submitting || loading || !displayNameDraft.trim() || displayNameDraft === workspaceMetadata.display_name}>{submitting ? 'Saving…' : 'Save name'}</button>
<button type="button" disabled={submitting || loading} onclick={cancelName}>Cancel</button>
{#if nameError}<button type="button" disabled={submitting || loading} onclick={reloadName}>Reload saved name</button>{/if}
</div>
</form>
{:else}
<p class="saved-name">{workspaceMetadata.display_name}</p>
{/if}
{#if nameNotice}<p role="status">{nameNotice}</p>{/if}
{#each diagnostics.filter((item) => item.severity !== 'info') as diagnostic}
<p class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'} role="alert">{diagnostic.message}</p>
{/each}
<details class="technical">
<summary>Technical details</summary>
<dl>
<div><dt>Source</dt><dd><code>{workspaceMetadata.source}</code></dd></div>
<div><dt>Revision</dt><dd><code>{workspaceMetadata.revision}</code></dd></div>
</dl>
{#each diagnostics.filter((item) => item.severity === 'info') as diagnostic}<p>{diagnostic.message}</p>{/each}
</details>
{/if}
</section>
{#if canDelete}
<BevelLine decorative />
<section aria-labelledby="workspace-identity-title" data-identity-ready={!identityLoading}>
<div class="section-heading">
<h2 id="workspace-identity-title">Public identity</h2>
{#if signingIdentity?.public_bundle}<button bind:this={identityCopyButton} type="button" onclick={copyIdentityBundle}>Copy bundle</button>{/if}
</div>
{#if identityLoading}<p class="muted" role="status">Loading identity…</p>{/if}
{#if identityError}<p class="error" role="alert">{identityError}</p>{/if}
{#if !identityLoading && !signingIdentity}<button type="button" onclick={retryIdentity}>Retry identity</button>{/if}
{#if signingIdentity?.identity.state === 'pending_provisioning'}
<p class="warning">Not provisioned</p>
<p class="muted">Provision a signing identity before connecting a Runtime.</p>
<button bind:this={provisionButton} type="button" disabled={provisioningIdentity || deletionOpen || deletionSubmitting} onclick={provisionIdentity}>{provisioningIdentity ? 'Provisioning…' : 'Provision identity'}</button>
{:else if signingIdentity?.public_bundle}
<p>Active</p>
{#if identityCopied}<p role="status">Public bundle copied.</p>{/if}
{#if copyError}<p class="error" role="alert">{copyError}</p>{/if}
<details class="technical">
<summary>Public bundle and key details</summary>
<p>Use this public bundle when connecting a Runtime. It contains no private key material.</p>
<dl>
<div><dt>Key</dt><dd><code>{signingIdentity.identity.key_id}</code></dd></div>
<div><dt>Fingerprint</dt><dd><code>{signingIdentity.identity.public_key_fingerprint}</code></dd></div>
<div><dt>Revision</dt><dd><code>{signingIdentity.identity.revision}</code></dd></div>
</dl>
<label>Public identity bundle<textarea readonly rows="9" value={identityBundleText} spellcheck="false"></textarea></label>
</details>
{/if}
</section>
<BevelLine decorative />
<details class="deletion" bind:open={dangerOpen} ontoggle={(event) => { if (!event.currentTarget.open && !deletionSubmitting) deletionOpen = false; }}>
<summary bind:this={dangerSummary}>{deletionSubmitting ? 'Deleting Workspace…' : 'Delete Workspace'}</summary>
<p>Deleting this Workspace permanently removes its Workers, Workdirs, repositories, configuration, Memory, Tickets, and audit data. This cannot be undone.</p>
{#if !deletionOpen}
<button type="button" disabled={editing || submitting || provisioningIdentity} onclick={openDeletionConfirmation}>Review deletion impact</button>
{:else}
<section aria-labelledby="delete-workspace-title">
<h2 id="delete-workspace-title">Delete {deletionPreflight?.display_name ?? deletionRequest?.confirmation ?? workspaceMetadata?.display_name ?? workspaceName}?</h2>
{#if deletionLoading}<p role="status">Loading deletion impact…</p>{/if}
{#if deletionPreflight}
<ul>
<li>{deletionPreflight.resources.workers} Workers</li><li>{deletionPreflight.resources.workdirs} Workdirs</li>
<li>{deletionPreflight.resources.repositories} repositories</li><li>{deletionPreflight.resources.runtime_bindings} Runtime bindings</li>
<li>{deletionPreflight.resources.secrets} secret records</li><li>{deletionPreflight.resources.artifacts} artifacts</li>
</ul>
{#each deletionPreflight.blockers as blocker}<p class="error" role="alert">{blocker.message}</p>{/each}
{#if !deletionRequest}
<label>Type {deletionPreflight.display_name} to confirm<input use:focusInput bind:value={deletionConfirmation} autocomplete="off" disabled={deletionSubmitting || !deletionPreflight.can_delete} /></label>
{/if}
{/if}
{#if deletionOperation}
<p role="status">Deletion state: {deletionOperation.state}</p>
{#each deletionOperation.blockers as blocker}<p class="error" role="alert">{blocker.message}</p>{/each}
{/if}
{#if deletionError}<p class="error" role="alert">{deletionError}</p>{/if}
<div class="actions">
<button type="button" onclick={cancelDeletion} disabled={deletionSubmitting}>Cancel</button>
{#if deletionPreflight || deletionRequest}
<button class="danger" type="button" onclick={deleteWorkspace} disabled={deletionSubmitting || (!deletionRequest && (!deletionPreflight?.can_delete || deletionConfirmation !== deletionPreflight.display_name))}>{deletionSubmitting ? 'Deleting…' : deletionRequest ? 'Retry deletion' : 'Delete Workspace'}</button>
{:else if !deletionLoading}
<button type="button" onclick={openDeletionConfirmation}>Retry deletion check</button>
{/if}
</div>
</section>
{/if}
</details>
{/if}
</div>
<style>
.identity-settings { display: grid; gap: var(--space-5); max-width: 52rem; min-width: 0; container-type: inline-size; }
h1 { margin: 0; font-size: var(--font-size-title); line-height: var(--line-height-title); color: var(--text-strong); }
h2 { margin: 0; font-size: var(--font-size-body); line-height: var(--line-height-body); color: var(--text-strong); }
section, form { display: grid; gap: var(--space-3); min-width: 0; }
.section-heading { display: flex; justify-content: space-between; align-items: center; gap: var(--space-3); }
p { margin: 0; overflow-wrap: anywhere; }
.muted, .technical { color: var(--text-muted); }
.error { color: var(--danger); }
.warning { color: var(--warning); }
label { display: grid; gap: var(--space-1); min-width: 0; }
input, textarea { width: 100%; min-width: 0; border: 1px solid var(--line); border-radius: var(--radius-soft); background: var(--bg-raised); color: var(--text-strong); padding: var(--space-2); font: inherit; }
input[aria-invalid="true"] { border-color: var(--danger); }
textarea { resize: vertical; font-family: var(--font-mono); font-size: var(--font-size-compact); line-height: var(--line-height-compact); }
code { font-family: var(--font-mono); }
.actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
button { width: fit-content; max-width: 100%; border: 1px solid var(--line); border-radius: var(--radius-soft); padding: var(--space-2) var(--space-3); color: var(--text); background: transparent; cursor: pointer; font: inherit; }
button.primary { background: var(--text-strong); color: var(--bg); border-color: var(--text-strong); }
button.danger { color: var(--danger); border-color: var(--danger); }
button:hover:not(:disabled), summary:hover { background: var(--interactive-hover); color: var(--text-strong); }
button:active:not(:disabled), summary:active { background: var(--interactive-selected); color: var(--text-strong); }
button:focus-visible, summary:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:disabled, input:disabled { opacity: 0.6; cursor: not-allowed; }
summary { cursor: pointer; width: fit-content; max-width: 100%; color: var(--text); padding-block: var(--space-2); }
details > :not(summary) { margin-top: var(--space-3); }
dl { display: grid; gap: var(--space-2); margin: 0; font-size: var(--font-size-compact); line-height: var(--line-height-compact); }
dl div { display: grid; grid-template-columns: 7rem minmax(0, 1fr); gap: var(--space-3); }
dd { margin: 0; overflow-wrap: anywhere; }
.deletion > summary { color: var(--danger); }
ul { margin: 0; padding-left: var(--space-5); }
@container (max-width: 28rem) { dl div { grid-template-columns: 1fr; gap: var(--space-1); } }
</style>
@@ -0,0 +1,125 @@
// Synthetic public data only; shared by component tests and production browser fixtures.
export function metadataFixture(workspaceId = "home-owner") {
return {
workspace_id: workspaceId,
display_name: workspaceId === "home-long"
? "Workspace with a long name — international documentation and distributed development"
: "Workspace Settings Review",
created_at: "2026-01-01T00:00:00Z",
revision: "2026-01-02T00:00:00Z",
source: "server_db",
diagnostics: [],
};
}
export function identityFixture(workspaceId = "home-owner", pending = false) {
const identity = {
workspace_id: workspaceId,
key_id: "fixture-workspace-key",
algorithm: "ed25519",
revision: 1,
state: pending ? "pending_provisioning" : "active",
created_at: "2026-01-01T00:00:00Z",
};
if (pending) return { identity };
const publicKey = "yoi-ed25519-pub:v1:synthetic-public-key";
const fingerprint = "SHA256:synthetic-public-fingerprint-for-ui-review";
return {
identity: {
...identity,
public_key: publicKey,
public_key_fingerprint: fingerprint,
provisioned_at: identity.created_at,
},
public_bundle: {
workspace_id: workspaceId,
backend_url: "https://workspace.example.test",
key_id: identity.key_id,
algorithm: identity.algorithm,
public_key: publicKey,
public_key_fingerprint: fingerprint,
revision: 1,
},
};
}
export function deletionFixture(workspaceId = "home-owner") {
return {
workspace_id: workspaceId,
display_name: metadataFixture(workspaceId).display_name,
expected_revision: metadataFixture(workspaceId).revision,
can_delete: true,
blockers: [],
resources: {
workers: 2,
workdirs: 1,
repositories: 1,
runtime_bindings: 1,
secrets: 0,
artifacts: 0,
},
};
}
export function settingsFixtureHandler() {
const names = new Map<string, ReturnType<typeof metadataFixture>>();
const provisioned = new Set<string>();
return async (request: Request): Promise<Response | null> => {
const url = new URL(request.url);
const match = url.pathname.match(/^\/api\/w\/([^/]+)\/settings(.*)$/);
if (match) {
const [, id, path] = match;
if (!path) {
const current = names.get(id) ?? metadataFixture(id);
if (request.method === "PUT") {
const body = await request.json();
if (body.revision !== current.revision) {
return new Response(
"Workspace metadata changed. Reload the saved name.",
{ status: 409 },
);
}
const next = {
...current,
display_name: String(body.display_name).trim(),
revision: String(Number.parseInt(current.revision, 10) + 1),
};
names.set(id, next);
return Response.json({ workspace: next, diagnostics: [] });
}
return Response.json(current);
}
if (path === "/signing-identity/provision") {
provisioned.add(id);
return Response.json(identityFixture(id));
}
if (path === "/signing-identity") {
if (id === "home-error") {
return new Response("Public identity is temporarily unavailable.", {
status: 503,
});
}
return Response.json(
identityFixture(id, id === "home-empty" && !provisioned.has(id)),
);
}
}
const deletion = url.pathname.match(
/^\/api\/workspaces\/([^/]+)\/deletion$/,
);
if (deletion) {
if (request.method !== "GET") {
return new Response("Fixture does not delete Workspaces.", {
status: 409,
});
}
return Response.json({
...deletionFixture(deletion[1]),
...(names.has(deletion[1])
? {
display_name: names.get(deletion[1])!.display_name,
expected_revision: names.get(deletion[1])!.revision,
}
: {}),
});
}
return null;
};
}
@@ -1,395 +1,17 @@
<script lang="ts">
import type {
Diagnostic,
WorkspaceDeletionOperationResponse,
WorkspaceDeletionPreflightResponse,
WorkspaceDeletionRequest,
WorkspaceMetadataSettingsResponse,
WorkspaceSigningIdentityResponse,
} from '$lib/generated/legacy-server-api';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { disposeWorkspaceMultiplexer } from '$lib/workspace/multiplexer';
import { disposeWorkspaceWorkersStore } from '$lib/workspace/sidebar/worker-subscription';
import {
getWorkspaceDeletion,
preflightWorkspaceDeletion,
startWorkspaceDeletion,
} from '$lib/workspace/settings/workspace-deletion-api';
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import {
fetchWorkspaceMetadata,
fetchWorkspaceSigningIdentity,
provisionWorkspaceSigningIdentity,
updateWorkspaceMetadata,
} from '$lib/workspace/settings/profile-api';
import WorkspaceIdentitySettings from '$lib/workspace/settings/WorkspaceIdentitySettings.svelte';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let workspaceId = $derived(data.workspace?.workspace_id ?? '');
let workspaceMetadata = $state<WorkspaceMetadataSettingsResponse | null>(null);
let signingIdentity = $state<WorkspaceSigningIdentityResponse | null>(null);
let identityLoading = $state(true);
let identityError = $state<string | null>(null);
let provisioningIdentity = $state(false);
let identityCopied = $state(false);
let identityBundleText = $derived(
signingIdentity?.public_bundle ? JSON.stringify(signingIdentity.public_bundle, null, 2) : ''
);
let displayNameDraft = $state('');
let loading = $state(true);
let submitting = $state(false);
let message = $state<string | null>(null);
let diagnostics = $state<Diagnostic[]>([]);
let deletionOpen = $state(false);
let deletionLoading = $state(false);
let deletionSubmitting = $state(false);
let deletionConfirmation = $state('');
let deletionPreflight = $state<WorkspaceDeletionPreflightResponse | null>(null);
let deletionOperation = $state<WorkspaceDeletionOperationResponse | null>(null);
let deletionRequest = $state<WorkspaceDeletionRequest | null>(null);
let deletionError = $state<string | null>(null);
function deletionStorageKey(): string {
return `yoi:workspace-deletion:${workspaceId}`;
}
$effect(() => {
if (!workspaceId) {
loading = false;
return;
}
let cancelled = false;
async function load() {
loading = true;
message = null;
try {
const response = await fetchWorkspaceMetadata(workspaceId);
if (!cancelled) {
workspaceMetadata = response;
displayNameDraft = response.display_name;
diagnostics = response.diagnostics;
if (data.workspace?.permissions.delete_workspace) {
try {
signingIdentity = await fetchWorkspaceSigningIdentity(workspaceId);
} catch (err) {
identityError = err instanceof Error ? err.message : 'Workspace identity request failed';
} finally {
identityLoading = false;
}
} else {
identityLoading = false;
}
}
} catch (err) {
if (!cancelled) {
message = err instanceof Error ? err.message : 'workspace settings request failed';
}
} finally {
if (!cancelled) loading = false;
}
}
load();
return () => {
cancelled = true;
};
});
async function submitWorkspaceName() {
if (!workspaceMetadata) return;
submitting = true;
message = null;
try {
const response = await updateWorkspaceMetadata(workspaceId, {
display_name: displayNameDraft,
revision: workspaceMetadata.revision
});
workspaceMetadata = response.workspace;
displayNameDraft = response.workspace.display_name;
diagnostics = response.diagnostics.concat(response.workspace.diagnostics);
message = 'Workspace display name updated.';
} catch (err) {
message = err instanceof Error ? err.message : 'workspace update failed';
} finally {
submitting = false;
}
}
async function provisionIdentity() {
provisioningIdentity = true;
identityError = null;
try {
signingIdentity = await provisionWorkspaceSigningIdentity(workspaceId);
} catch (err) {
identityError = err instanceof Error ? err.message : 'Workspace identity provisioning failed';
} finally {
provisioningIdentity = false;
}
}
async function copyIdentityBundle() {
const bundle = signingIdentity?.public_bundle;
if (!bundle) return;
identityCopied = false;
try {
await navigator.clipboard.writeText(JSON.stringify(bundle, null, 2));
identityCopied = true;
} catch (err) {
identityError = err instanceof Error ? err.message : 'Workspace identity bundle copy failed';
}
}
async function openDeletionConfirmation() {
deletionOpen = true;
deletionLoading = true;
deletionError = null;
deletionOperation = null;
deletionRequest = null;
sessionStorage.removeItem(deletionStorageKey());
deletionConfirmation = '';
try {
deletionPreflight = await preflightWorkspaceDeletion(workspaceId);
} catch (err) {
deletionError = err instanceof Error ? err.message : 'Workspace deletion preflight failed';
} finally {
deletionLoading = false;
}
}
async function trackDeletion(operationId: string) {
let operation = await getWorkspaceDeletion(operationId);
deletionOperation = operation;
while (operation.state === 'queued' || operation.state === 'running') {
await new Promise((resolve) => setTimeout(resolve, 500));
operation = await getWorkspaceDeletion(operation.operation_id);
deletionOperation = operation;
}
if (operation.state === 'succeeded') {
sessionStorage.removeItem(deletionStorageKey());
disposeWorkspaceMultiplexer(workspaceId);
disposeWorkspaceWorkersStore(workspaceId);
await goto('/');
}
}
function storedDeletionRequest(): WorkspaceDeletionRequest | null {
try {
const value: unknown = JSON.parse(sessionStorage.getItem(deletionStorageKey()) ?? 'null');
if (typeof value !== 'object' || value === null) return null;
const record = value as Record<string, unknown>;
if (
Object.keys(record).sort().join(',') !== 'confirmation,expected_revision,operation_id' ||
typeof record.operation_id !== 'string' || record.operation_id.length === 0 || record.operation_id.length > 128 ||
!/^[A-Za-z0-9_-]+$/.test(record.operation_id) ||
typeof record.expected_revision !== 'string' || record.expected_revision.length > 128 ||
typeof record.confirmation !== 'string' || record.confirmation !== data.workspace?.display_name || record.confirmation.length > 256
) return null;
return {
operation_id: record.operation_id,
expected_revision: record.expected_revision,
confirmation: record.confirmation,
};
} catch {
return null;
}
}
onMount(() => {
if (!data.workspace?.permissions.delete_workspace) return;
const request = storedDeletionRequest();
if (!request) return;
deletionRequest = request;
deletionConfirmation = request.confirmation;
deletionOpen = true;
deletionSubmitting = true;
void trackDeletion(request.operation_id)
.catch((err) => {
deletionError = err instanceof Error ? err.message : 'Workspace deletion status failed';
})
.finally(() => {
deletionSubmitting = false;
});
});
async function deleteWorkspace() {
if (!deletionPreflight && !deletionRequest) return;
deletionSubmitting = true;
deletionError = null;
try {
const request = deletionRequest ?? {
operation_id: crypto.randomUUID(),
expected_revision: deletionPreflight!.expected_revision,
confirmation: deletionConfirmation,
};
deletionRequest = request;
sessionStorage.setItem(deletionStorageKey(), JSON.stringify(request));
const operation = await startWorkspaceDeletion(workspaceId, request);
deletionOperation = operation;
await trackDeletion(operation.operation_id);
} catch (err) {
deletionError = err instanceof Error ? err.message : 'Workspace deletion failed';
} finally {
deletionSubmitting = false;
}
}
</script>
<svelte:head>
<title>Workspace settings · Yoi Workspace</title>
</svelte:head>
<svelte:head><title>Workspace Identity · Settings · Yoi</title></svelte:head>
<section class="card settings-section" aria-labelledby="workspace-settings-title">
<header class="settings-section-header">
<div>
<p class="eyebrow">editable</p>
<h2 id="workspace-settings-title">Workspace Identity</h2>
</div>
<span class="badge success">Backend scoped</span>
</header>
{#if loading}
<p class="status-message">Loading workspace settings…</p>
{:else}
<form class="settings-form" onsubmit={(event) => { event.preventDefault(); void submitWorkspaceName(); }}>
<label>
<span>Display name</span>
<input bind:value={displayNameDraft} autocomplete="off" />
</label>
<p class="settings-note">Workspace id: <code>{workspaceMetadata?.workspace_id ?? workspaceId}</code></p>
<button type="submit" disabled={submitting || !workspaceMetadata}>{submitting ? 'Saving…' : 'Save workspace name'}</button>
</form>
<dl class="settings-identity-list">
<div>
<dt>Source</dt>
<dd>{workspaceMetadata?.source ?? 'unknown'}</dd>
</div>
<div>
<dt>Revision</dt>
<dd><code>{workspaceMetadata?.revision ?? 'unknown'}</code></dd>
</div>
</dl>
{#if data.workspace}
{#key `${data.workspace.workspace_id}:${data.workspace.permissions.delete_workspace}`}
<WorkspaceIdentitySettings
workspaceId={data.workspace.workspace_id}
workspaceName={data.workspace.display_name}
canDelete={data.workspace.permissions.delete_workspace}
/>
{/key}
{/if}
{#if message}
<p class="status-message" class:error={message.includes('failed')}>{message}</p>
{/if}
<DiagnosticsList {diagnostics} />
</section>
{#if data.workspace?.permissions.delete_workspace}
<section class="settings-section" aria-labelledby="workspace-identity-title">
<div class="section-heading">
<div>
<h2 id="workspace-identity-title">Workspace public identity</h2>
<p>Use this public bundle when connecting a Runtime to this Workspace.</p>
</div>
{#if signingIdentity?.public_bundle}
<button type="button" onclick={() => void copyIdentityBundle()}>
{identityCopied ? 'Copied' : 'Copy bundle'}
</button>
{/if}
</div>
{#if identityError}
<p class="status-message error">{identityError}</p>
{/if}
{#if identityLoading}
<p>Loading identity…</p>
{:else if signingIdentity?.identity.state === 'pending_provisioning'}
<p>This existing Workspace needs one explicit signing identity provisioning operation.</p>
<button
type="button"
disabled={provisioningIdentity}
onclick={() => void provisionIdentity()}
>{provisioningIdentity ? 'Provisioning…' : 'Provision identity'}</button>
{:else if signingIdentity?.public_bundle}
<dl class="metadata-list">
<div>
<dt>Key</dt>
<dd><code>{signingIdentity.identity.key_id}</code></dd>
</div>
<div>
<dt>Fingerprint</dt>
<dd><code>{signingIdentity.identity.public_key_fingerprint}</code></dd>
</div>
<div>
<dt>Revision</dt>
<dd><code>{signingIdentity.identity.revision}</code></dd>
</div>
</dl>
<label class="identity-bundle">
<span>Public identity bundle</span>
<textarea readonly rows="9" value={identityBundleText}></textarea>
</label>
{/if}
</section>
<section class="settings-section danger-zone" aria-labelledby="workspace-danger-title">
<div>
<h2 id="workspace-danger-title">Danger zone</h2>
<p>Deleting this Workspace permanently removes its Workers, Workdirs, repositories, configuration, Memory, Tickets, and audit data.</p>
</div>
<button class="danger-button" type="button" onclick={() => void openDeletionConfirmation()}>Delete Workspace</button>
</section>
{/if}
{#if deletionOpen}
<div class="modal-backdrop" role="presentation">
<div class="deletion-dialog" role="dialog" aria-modal="true" aria-labelledby="delete-workspace-title">
<h2 id="delete-workspace-title">Delete {deletionPreflight?.display_name ?? deletionRequest?.confirmation ?? 'Workspace'}?</h2>
{#if deletionLoading}
<p>Loading deletion impact…</p>
{:else if deletionPreflight}
<p>This operation cannot be undone. It will remove:</p>
<ul>
<li>{deletionPreflight.resources.workers} Workers</li>
<li>{deletionPreflight.resources.workdirs} Workdirs</li>
<li>{deletionPreflight.resources.repositories} repositories</li>
<li>{deletionPreflight.resources.runtime_bindings} Runtime bindings</li>
<li>{deletionPreflight.resources.secrets} secret records</li>
<li>{deletionPreflight.resources.artifacts} artifacts</li>
</ul>
{#each deletionPreflight.blockers as blocker}
<p class="status-message error">{blocker.message}</p>
{/each}
<label>
<span>Type <strong>{deletionPreflight.display_name}</strong> to confirm</span>
<input bind:value={deletionConfirmation} autocomplete="off" />
</label>
{/if}
{#if deletionOperation}
<p class="status-message">Deletion state: {deletionOperation.state}</p>
{#each deletionOperation.blockers as blocker}
<p class="status-message error">{blocker.message}</p>
{/each}
{/if}
{#if deletionError}<p class="status-message error">{deletionError}</p>{/if}
<div class="dialog-actions">
<button type="button" onclick={() => { deletionOpen = false; }} disabled={deletionSubmitting}>Cancel</button>
<button
class="danger-button"
type="button"
onclick={() => void deleteWorkspace()}
disabled={deletionSubmitting || (!deletionRequest && !deletionPreflight?.can_delete) || deletionConfirmation !== (deletionPreflight?.display_name ?? deletionRequest?.confirmation ?? '')}
>{deletionSubmitting ? 'Deleting…' : 'Delete Workspace'}</button>
</div>
</div>
</div>
{/if}
<style>
.section-heading { display: flex; justify-content: space-between; align-items: start; gap: var(--space-4); }
.section-heading p { margin-block: var(--space-1) 0; }
.metadata-list { display: grid; gap: var(--space-2); }
.metadata-list div { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: var(--space-3); }
.metadata-list dd { margin: 0; overflow-wrap: anywhere; }
.identity-bundle { display: grid; gap: var(--space-2); margin-top: var(--space-4); }
.identity-bundle textarea { width: 100%; resize: vertical; font-family: var(--font-mono); font-size: var(--font-size-compact); }
.danger-zone { display: flex; justify-content: space-between; align-items: start; gap: var(--space-4); border-top: 1px solid var(--color-danger, #b42318); }
.danger-zone p { max-width: 68ch; }
.danger-button { color: white; background: var(--color-danger, #b42318); border-color: var(--color-danger, #b42318); }
.modal-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: var(--space-4); background: rgb(0 0 0 / 0.55); }
.deletion-dialog { width: min(34rem, 100%); max-height: calc(100vh - 2rem); overflow: auto; padding: var(--space-5); background: var(--color-surface, white); border: 1px solid var(--color-border); }
.deletion-dialog label { display: grid; gap: var(--space-2); margin-block: var(--space-4); }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-5); }
</style>
@@ -0,0 +1,532 @@
// @vitest-environment happy-dom
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/svelte";
import { afterEach, expect, test, vi } from "vitest";
import type { ComponentProps } from "svelte";
import { goto, invalidate } from "$app/navigation";
import Settings from "./+page.svelte";
import {
deletionFixture,
identityFixture,
metadataFixture,
settingsFixtureHandler,
} from "$lib/workspace/settings/identity.test-fixtures";
vi.mock(
"$app/navigation",
() => ({ goto: vi.fn(async () => {}), invalidate: vi.fn(async () => {}) }),
);
vi.mock(
"$lib/workspace/multiplexer",
() => ({ disposeWorkspaceMultiplexer: vi.fn() }),
);
vi.mock(
"$lib/workspace/sidebar/worker-subscription",
() => ({ disposeWorkspaceWorkersStore: vi.fn() }),
);
type Data = ComponentProps<typeof Settings>["data"];
function data(id = "home-owner", owner = true): Data {
return {
workspace: {
workspace_id: id,
display_name: metadataFixture(id).display_name,
permissions: { delete_workspace: owner },
},
} as Data;
}
function mount(value = data()) {
return render(Settings, {
props: {
data: value,
params: { workspaceId: value.workspace!.workspace_id },
},
});
}
function mockApi(
override?: (request: Request) => Promise<Response | null> | Response | null,
) {
const handler = settingsFixtureHandler();
const mock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(
new URL(String(input), "https://fixture.test"),
init,
);
return await override?.(request) ?? await handler(request) ??
new Response("Unexpected fixture request", { status: 404 });
});
vi.stubGlobal("fetch", mock);
return mock;
}
async function openName() {
await fireEvent.click(
await screen.findByRole("button", { name: "Edit name" }),
);
return screen.getByLabelText("New display name") as HTMLInputElement;
}
async function openDeletion() {
await screen.findByText("Active", { exact: true });
await fireEvent.click(
screen.getByText("Delete Workspace", { selector: "summary" }),
);
await fireEvent.click(
screen.getByRole("button", { name: "Review deletion impact" }),
);
return await screen.findByLabelText(
`Type ${metadataFixture().display_name} to confirm`,
) as HTMLInputElement;
}
function operation(state = "succeeded", operationId = "op-test") {
return {
operation_id: operationId,
workspace_id: "home-owner",
display_name: metadataFixture().display_name,
state,
resources: deletionFixture().resources,
child_operation_ids: [],
blockers: [],
failure_category: null,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
completed_at: null,
};
}
afterEach(() => {
cleanup();
sessionStorage.clear();
vi.unstubAllGlobals();
vi.clearAllMocks();
});
test("saved settings have no editable form, badges or raw data expanded; each technical scope is disclosed", async () => {
const api = mockApi();
mount();
await screen.findByText("Active", { exact: true });
expect(screen.getByText(metadataFixture().display_name)).toBeTruthy();
expect(document.querySelector("form")).toBeNull();
expect(document.querySelector("input, textarea:not([readonly])")).toBeNull();
// happy-dom does not implement native details visibility; Chromium verifies it.
expect(document.querySelector("textarea")?.closest("details")?.open).toBe(
false,
);
expect(document.querySelectorAll("details[open]")).toHaveLength(0);
expect(document.querySelector(".badge")).toBeNull();
expect(api.mock.calls).toHaveLength(2);
expect(api.mock.calls.some(([path]) => String(path).includes("deletion")))
.toBe(false);
});
test("Edit focuses the field; Cancel discards the draft and returns focus without mutation", async () => {
const api = mockApi();
mount();
const input = await openName();
expect(document.activeElement).toBe(input);
await fireEvent.input(input, { target: { value: "Discard this" } });
await fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(document.activeElement).toBe(
screen.getByRole("button", { name: "Edit name" }),
);
expect((await openName()).value).toBe(metadataFixture().display_name);
expect(api.mock.calls.every(([, init]) => !init?.method)).toBe(true);
});
test("save uses the current revision, publishes confirmed name and invalidates the Workspace header", async () => {
const api = mockApi();
mount();
await fireEvent.input(await openName(), {
target: { value: "Renamed Workspace" },
});
await fireEvent.click(screen.getByRole("button", { name: "Save name" }));
await screen.findByText("Workspace name saved.");
expect(screen.getByText("Renamed Workspace")).toBeTruthy();
expect(document.querySelector("form")).toBeNull();
expect(document.activeElement).toBe(
screen.getByRole("button", { name: "Edit name" }),
);
expect(
JSON.parse(String(
api.mock.calls.find(([, init]) => init?.method === "PUT")![1]!.body,
)),
).toEqual({
display_name: "Renamed Workspace",
revision: metadataFixture().revision,
});
expect(invalidate).toHaveBeenCalledWith("/api/w/home-owner/workspace");
});
test("failed save preserves the draft and saved name; Reload saved name recovers from revision conflicts", async () => {
mockApi((request) =>
request.method === "PUT"
? new Response("Revision conflict", { status: 409 })
: null
);
mount();
const input = await openName();
await fireEvent.input(input, { target: { value: "Keep draft" } });
await fireEvent.click(screen.getByRole("button", { name: "Save name" }));
expect((await screen.findByRole("alert")).textContent).toBe(
"Revision conflict",
);
expect(input.value).toBe("Keep draft");
expect(screen.getByText(`Current: ${metadataFixture().display_name}`))
.toBeTruthy();
await fireEvent.click(
screen.getByRole("button", { name: "Reload saved name" }),
);
await screen.findByRole("button", { name: "Edit name" });
expect(screen.queryByRole("alert")).toBeNull();
expect((await openName()).value).toBe(metadataFixture().display_name);
});
test("pending save disables duplicate submit and cancel while preserving the current value", async () => {
let release!: (response: Response) => void;
const held = new Promise<Response>((resolve) => release = resolve);
const api = mockApi((request) => request.method === "PUT" ? held : null);
mount();
await fireEvent.input(await openName(), { target: { value: "New name" } });
await fireEvent.click(screen.getByRole("button", { name: "Save name" }));
expect(
(screen.getByRole("button", { name: "Saving…" }) as HTMLButtonElement)
.disabled,
).toBe(true);
expect(
(screen.getByRole("button", { name: "Cancel" }) as HTMLButtonElement)
.disabled,
).toBe(true);
await fireEvent.submit(document.querySelector("form")!);
expect(api.mock.calls.filter(([, init]) => init?.method === "PUT"))
.toHaveLength(1);
release(
Response.json({
workspace: { ...metadataFixture(), display_name: "New name" },
diagnostics: [],
}),
);
await screen.findByText("Workspace name saved.");
});
test("metadata and public identity load independently and offer scoped retries", async () => {
let failName = true;
mockApi((request) =>
failName && new URL(request.url).pathname.endsWith("/settings")
? new Response("Name unavailable", { status: 503 })
: null
);
mount();
await screen.findByText("Active", { exact: true });
await screen.findByText("Name unavailable");
expect(screen.queryByRole("button", { name: "Edit name" })).toBeNull();
failName = false;
await fireEvent.click(screen.getByRole("button", { name: "Retry name" }));
await screen.findByRole("button", { name: "Edit name" });
});
test("identity failure does not block name editing or masquerade as pending provisioning", async () => {
let fail = true;
mockApi((request) =>
fail && request.url.endsWith("/signing-identity")
? new Response("Identity unavailable", { status: 503 })
: null
);
mount();
await screen.findByRole("button", { name: "Edit name" });
await screen.findByText("Identity unavailable");
expect(screen.queryByRole("button", { name: "Provision identity" }))
.toBeNull();
fail = false;
await fireEvent.click(screen.getByRole("button", { name: "Retry identity" }));
await screen.findByText("Active", { exact: true });
});
test("non-owner retains metadata editing but never fetches or shows owner-only identity/deletion", async () => {
const api = mockApi();
mount(data("home-member", false));
await screen.findByRole("button", { name: "Edit name" });
expect(screen.queryByRole("heading", { name: "Public identity" })).toBeNull();
expect(screen.queryByText("Delete Workspace")).toBeNull();
expect(api).toHaveBeenCalledTimes(1);
});
test("pending provisioning is explicit, only provisions on action and focuses the resulting copy action", async () => {
const api = mockApi();
mount(data("home-empty"));
await screen.findByText("Not provisioned");
expect(api.mock.calls.every(([, init]) => !init?.method)).toBe(true);
await fireEvent.click(
screen.getByRole("button", { name: "Provision identity" }),
);
const copy = await screen.findByRole("button", { name: "Copy bundle" });
expect(document.activeElement).toBe(copy);
expect(screen.getByText("Active", { exact: true })).toBeTruthy();
});
test("copy uses the exact public bundle; clipboard failure offers manual selection rather than losing data", async () => {
mockApi();
const writeText = vi.fn().mockResolvedValueOnce(undefined)
.mockRejectedValueOnce(new Error("Denied"));
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
mount();
const button = await screen.findByRole("button", { name: "Copy bundle" });
await fireEvent.click(button);
await screen.findByText("Public bundle copied.");
expect(writeText).toHaveBeenCalledWith(
JSON.stringify(identityFixture().public_bundle, null, 2),
);
await fireEvent.click(button);
expect((await screen.findByRole("alert")).textContent).toContain(
"select the bundle manually",
);
expect(document.querySelector("textarea")!.value).toBe(
JSON.stringify(identityFixture().public_bundle, null, 2),
);
});
test("deletion is deferred, requires exact current name, and Cancel restores disclosure focus", async () => {
const api = mockApi();
mount();
const input = await openDeletion();
expect(document.activeElement).toBe(input);
const confirm = screen.getByRole("button", {
name: "Delete Workspace",
}) as HTMLButtonElement;
expect(confirm.disabled).toBe(true);
await fireEvent.input(input, { target: { value: "Wrong name" } });
expect(confirm.disabled).toBe(true);
await fireEvent.input(input, {
target: { value: metadataFixture().display_name },
});
expect(confirm.disabled).toBe(false);
await fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(document.activeElement?.textContent).toBe("Delete Workspace");
expect(api.mock.calls.every(([, init]) => !init?.method)).toBe(true);
});
test("deletion blockers and preflight failure do not permit a destructive request", async () => {
let fail = true;
const api = mockApi((request) =>
request.url.endsWith("/deletion")
? fail
? new Response("Preflight unavailable", { status: 503 })
: Response.json({
...deletionFixture(),
can_delete: false,
blockers: [{
kind: "worker_removal_blocked",
resource_kind: "worker",
resource_key: "W-7",
message: "Worker W-7 is running.",
}],
})
: null
);
mount();
await screen.findByText("Active", { exact: true });
await fireEvent.click(
screen.getByText("Delete Workspace", { selector: "summary" }),
);
await fireEvent.click(
screen.getByRole("button", { name: "Review deletion impact" }),
);
await screen.findByRole("button", { name: "Retry deletion check" });
expect(screen.queryByRole("button", { name: "Delete Workspace" })).toBeNull();
fail = false;
await fireEvent.click(
screen.getByRole("button", { name: "Retry deletion check" }),
);
await screen.findByText("Worker W-7 is running.");
expect(
(screen.getByRole("button", {
name: "Delete Workspace",
}) as HTMLButtonElement).disabled,
).toBe(true);
expect(api.mock.calls.every(([, init]) => !init?.method)).toBe(true);
});
test("deletion retry reuses the persisted operation id and confirmation, then redirects only after success", async () => {
let attempts = 0;
const api = mockApi(async (request) => {
if (request.method === "POST" && request.url.endsWith("/deletion")) {
const body = await request.json();
return ++attempts === 1
? new Response("Connection lost", { status: 503 })
: Response.json(operation("queued", body.operation_id));
}
if (request.url.includes("/workspace-deletions/")) {
return Response.json(
operation("succeeded", request.url.split("/").pop()),
);
}
return null;
});
mount();
await fireEvent.input(await openDeletion(), {
target: { value: metadataFixture().display_name },
});
await fireEvent.click(
screen.getByRole("button", { name: "Delete Workspace" }),
);
await screen.findByRole("button", { name: "Retry deletion" });
expect(goto).not.toHaveBeenCalled();
const stored = sessionStorage.getItem("yoi:workspace-deletion:home-owner");
expect(stored).toBeTruthy();
await fireEvent.click(screen.getByRole("button", { name: "Retry deletion" }));
await waitFor(() => expect(goto).toHaveBeenCalledWith("/"));
const requests = api.mock.calls.filter(([, init]) => init?.method === "POST");
expect(requests[0][1]!.body).toBe(requests[1][1]!.body);
expect(sessionStorage.getItem("yoi:workspace-deletion:home-owner"))
.toBeNull();
});
test("reload resumes persisted deletion without sending another POST", async () => {
sessionStorage.setItem(
"yoi:workspace-deletion:home-owner",
JSON.stringify({
operation_id: "op-test",
expected_revision: metadataFixture().revision,
confirmation: metadataFixture().display_name,
}),
);
const api = mockApi((request) =>
request.url.includes("/workspace-deletions/")
? Response.json(operation("failed"))
: null
);
mount();
await screen.findByText("Deletion state: failed");
expect(document.querySelector("details.deletion")?.hasAttribute("open")).toBe(
true,
);
expect(api.mock.calls.every(([, init]) => !init?.method)).toBe(true);
});
test("Workspace switch resets drafts and ignores old load/mutation completions", async () => {
let release!: (response: Response) => void;
const held = new Promise<Response>((resolve) => release = resolve);
mockApi((request) => request.method === "PUT" ? held : null);
const view = mount();
await fireEvent.input(await openName(), {
target: { value: "Old scope draft" },
});
await fireEvent.click(screen.getByRole("button", { name: "Save name" }));
await view.rerender({
data: data("home-long"),
params: { workspaceId: "home-long" },
});
await screen.findByText(metadataFixture("home-long").display_name);
release(
Response.json({
workspace: { ...metadataFixture(), display_name: "Old scope draft" },
diagnostics: [],
}),
);
await new Promise((resolve) => setTimeout(resolve, 20));
expect(screen.queryByText("Old scope draft")).toBeNull();
expect(invalidate).not.toHaveBeenCalled();
expect(goto).not.toHaveBeenCalled();
});
test("slow identity keeps the name usable and old identity responses cannot leak across Workspaces", async () => {
let release!: (response: Response) => void;
const held = new Promise<Response>((resolve) => release = resolve);
mockApi((request) =>
request.url.endsWith("/home-owner/settings/signing-identity") ? held : null
);
const view = mount();
await screen.findByRole("button", { name: "Edit name" });
expect(screen.getByText("Loading identity…")).toBeTruthy();
await view.rerender({
data: data("home-empty"),
params: { workspaceId: "home-empty" },
});
await screen.findByText("Not provisioned");
release(Response.json(identityFixture()));
await new Promise((resolve) => setTimeout(resolve, 20));
expect(screen.queryByText("Active", { exact: true })).toBeNull();
expect(screen.getByText("Not provisioned")).toBeTruthy();
});
test("a completed deletion request from an unmounted Workspace cannot navigate away from the new scope", async () => {
const request = {
operation_id: "op-test",
expected_revision: metadataFixture().revision,
confirmation: metadataFixture().display_name,
};
sessionStorage.setItem(
"yoi:workspace-deletion:home-owner",
JSON.stringify(request),
);
let release!: (response: Response) => void;
const held = new Promise<Response>((resolve) => release = resolve);
mockApi((request) =>
request.url.includes("/workspace-deletions/") ? held : null
);
const view = mount();
await screen.findByText("Deleting…");
await view.rerender({
data: data("home-member", false),
params: { workspaceId: "home-member" },
});
await screen.findByRole("button", { name: "Edit name" });
release(Response.json(operation()));
await new Promise((resolve) => setTimeout(resolve, 20));
expect(goto).not.toHaveBeenCalled();
expect(sessionStorage.getItem("yoi:workspace-deletion:home-owner")).not
.toBeNull();
});
test("name editing and deletion confirmation cannot overlap", async () => {
mockApi();
mount();
await openName();
await fireEvent.click(
screen.getByText("Delete Workspace", { selector: "summary" }),
);
expect(
(screen.getByRole("button", {
name: "Review deletion impact",
}) as HTMLButtonElement).disabled,
).toBe(true);
await fireEvent.click(
screen.getByRole("button", { name: "Cancel" }),
);
await fireEvent.click(
screen.getByRole("button", { name: "Review deletion impact" }),
);
await screen.findByLabelText(
`Type ${metadataFixture().display_name} to confirm`,
);
expect(
(screen.getByRole("button", { name: "Edit name" }) as HTMLButtonElement)
.disabled,
).toBe(true);
await fireEvent.click(
screen.getByRole("button", { name: "Cancel" }),
);
expect(
(screen.getByRole("button", { name: "Edit name" }) as HTMLButtonElement)
.disabled,
).toBe(false);
});
test("untrusted names and errors remain text, not interpreted HTML", async () => {
const name = "<img src=x onerror=alert(1)>";
mockApi((request) =>
request.url.endsWith("/settings")
? Response.json({ ...metadataFixture(), display_name: name })
: request.url.endsWith("/signing-identity")
? new Response("<script>bad()</script>", { status: 503 })
: null
);
mount();
await screen.findByText(name);
expect((await screen.findByRole("alert")).textContent).toBe(
"<script>bad()</script>",
);
expect(document.querySelector("img, script")).toBeNull();
});
+4 -4
View File
@@ -313,11 +313,11 @@ Deno.test("Workspace deletion DTOs fail closed and preserve durable operation st
});
Deno.test("Workspace settings exposes owner-gated typed destructive confirmation", async () => {
const source = await Deno.readTextFile(
new URL(
const source = (await Promise.all([
"../src/routes/w/[workspaceId]/settings/+page.svelte",
import.meta.url,
),
"../src/lib/workspace/settings/WorkspaceIdentitySettings.svelte",
].map((path) => Deno.readTextFile(new URL(path, import.meta.url))))).join(
"\n",
);
for (
const token of [