feat: integrate Workspace signing identity authority

This commit is contained in:
2026-09-08 00:48:45 +09:00
12 changed files with 2691 additions and 35 deletions
@@ -165,6 +165,35 @@ export type WorkspaceMetadataMutationResponse = {
diagnostics: Array<Diagnostic>;
};
export type WorkspaceSigningIdentityState = "pending_provisioning" | "active";
export type WorkspaceSigningIdentityPublic = {
workspace_id: string;
key_id: string;
algorithm: string;
public_key?: string;
public_key_fingerprint?: string;
revision: number;
state: WorkspaceSigningIdentityState;
created_at: string;
provisioned_at?: string;
};
export type WorkspacePublicIdentityBundle = {
workspace_id: string;
backend_url: string;
key_id: string;
algorithm: string;
public_key: string;
public_key_fingerprint: string;
revision: number;
};
export type WorkspaceSigningIdentityResponse = {
identity: WorkspaceSigningIdentityPublic;
public_bundle?: WorkspacePublicIdentityBundle;
};
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
@@ -8,6 +8,10 @@ import type {
WorkspaceProfileSourceProvenance,
WorkspaceProfileSourceSummary,
WorkspaceProfileSummary,
WorkspacePublicIdentityBundle,
WorkspaceSigningIdentityPublic,
WorkspaceSigningIdentityResponse,
WorkspaceSigningIdentityState,
} from "$lib/generated/workspace-api";
export class ProfileApiError extends Error {
@@ -51,6 +55,18 @@ function stringValue(value: unknown, context: string): string {
return value;
}
function boundedStringValue(
value: unknown,
context: string,
maxBytes: number,
): string {
const text = stringValue(value, context);
if (new TextEncoder().encode(text).byteLength > maxBytes) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return text;
}
function booleanValue(value: unknown, context: string): boolean {
if (typeof value !== "boolean") {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
@@ -66,6 +82,15 @@ function optionalString(
return stringValue(value, context);
}
function optionalBoundedString(
value: unknown,
context: string,
maxBytes: number,
): string | null | undefined {
if (value === undefined || value === null) return value;
return boundedStringValue(value, context, maxBytes);
}
function optionalRevision(
value: unknown,
context: string,
@@ -286,6 +311,197 @@ export function parseProfileSettingsResponse(
};
}
export function parseWorkspaceSigningIdentityResponse(
value: unknown,
): WorkspaceSigningIdentityResponse {
const item = record(value, "Workspace signing identity");
exactKeys(
item,
["identity"],
["public_bundle"],
"Workspace signing identity",
);
const identityItem = record(item.identity, "Workspace signing identity");
exactKeys(
identityItem,
["workspace_id", "key_id", "algorithm", "revision", "state", "created_at"],
["public_key", "public_key_fingerprint", "provisioned_at"],
"Workspace signing identity",
);
const state = boundedStringValue(
identityItem.state,
"Workspace signing identity",
32,
);
if (state !== "pending_provisioning" && state !== "active") {
throw new ProfileApiError(
"Workspace signing identity returned an invalid response.",
502,
);
}
const revision = optionalRevision(
identityItem.revision,
"Workspace signing identity",
);
if (revision === undefined || revision === null || revision < 1) {
throw new ProfileApiError(
"Workspace signing identity returned an invalid response.",
502,
);
}
const publicKey = optionalBoundedString(
identityItem.public_key,
"Workspace signing identity",
256,
);
const fingerprint = optionalBoundedString(
identityItem.public_key_fingerprint,
"Workspace signing identity",
128,
);
const provisionedAt = optionalBoundedString(
identityItem.provisioned_at,
"Workspace signing identity",
128,
);
const identity: WorkspaceSigningIdentityPublic = {
workspace_id: boundedStringValue(
identityItem.workspace_id,
"Workspace signing identity",
128,
),
key_id: boundedStringValue(
identityItem.key_id,
"Workspace signing identity",
128,
),
algorithm: boundedStringValue(
identityItem.algorithm,
"Workspace signing identity",
32,
),
...(publicKey === undefined || publicKey === null
? {}
: { public_key: publicKey }),
...(fingerprint === undefined || fingerprint === null
? {}
: { public_key_fingerprint: fingerprint }),
revision,
state: state as WorkspaceSigningIdentityState,
created_at: boundedStringValue(
identityItem.created_at,
"Workspace signing identity",
128,
),
...(provisionedAt === undefined || provisionedAt === null
? {}
: { provisioned_at: provisionedAt }),
};
let publicBundle: WorkspacePublicIdentityBundle | undefined;
if (item.public_bundle !== undefined) {
const bundle = record(
item.public_bundle,
"Workspace public identity bundle",
);
exactKeys(
bundle,
[
"workspace_id",
"backend_url",
"key_id",
"algorithm",
"public_key",
"public_key_fingerprint",
"revision",
],
[],
"Workspace public identity bundle",
);
const bundleRevision = optionalRevision(
bundle.revision,
"Workspace public identity bundle",
);
if (
bundleRevision === undefined || bundleRevision === null ||
bundleRevision < 1
) {
throw new ProfileApiError(
"Workspace public identity bundle returned an invalid response.",
502,
);
}
publicBundle = {
workspace_id: boundedStringValue(
bundle.workspace_id,
"Workspace public identity bundle",
128,
),
backend_url: boundedStringValue(
bundle.backend_url,
"Workspace public identity bundle",
2048,
),
key_id: boundedStringValue(
bundle.key_id,
"Workspace public identity bundle",
128,
),
algorithm: boundedStringValue(
bundle.algorithm,
"Workspace public identity bundle",
32,
),
public_key: boundedStringValue(
bundle.public_key,
"Workspace public identity bundle",
256,
),
public_key_fingerprint: boundedStringValue(
bundle.public_key_fingerprint,
"Workspace public identity bundle",
128,
),
revision: bundleRevision,
};
}
if (
publicBundle !== undefined &&
(
publicBundle.workspace_id !== identity.workspace_id ||
publicBundle.key_id !== identity.key_id ||
publicBundle.algorithm !== identity.algorithm ||
publicBundle.public_key !== identity.public_key ||
publicBundle.public_key_fingerprint !== identity.public_key_fingerprint ||
publicBundle.revision !== identity.revision
)
) {
throw new ProfileApiError(
"Workspace public identity bundle does not match identity metadata.",
502,
);
}
if (
(state === "active" &&
(publicBundle === undefined || identity.public_key === undefined ||
identity.public_key_fingerprint === undefined ||
identity.provisioned_at === undefined)) ||
(state === "pending_provisioning" &&
(publicBundle !== undefined || identity.public_key !== undefined ||
identity.public_key_fingerprint !== undefined ||
identity.provisioned_at !== undefined))
) {
throw new ProfileApiError(
"Workspace signing identity returned an invalid response.",
502,
);
}
return {
identity,
...(publicBundle === undefined ? {} : { public_bundle: publicBundle }),
};
}
async function parseResponse<T>(
response: Response,
parser: (value: unknown) => T,
@@ -325,6 +541,33 @@ export async function updateWorkspaceMetadata(
);
}
export async function fetchWorkspaceSigningIdentity(
workspaceId: string,
): Promise<WorkspaceSigningIdentityResponse> {
return await parseResponse(
await fetch(
`/api/w/${
encodeURIComponent(workspaceId)
}/settings/workspace/signing-identity`,
),
parseWorkspaceSigningIdentityResponse,
);
}
export async function provisionWorkspaceSigningIdentity(
workspaceId: string,
): Promise<WorkspaceSigningIdentityResponse> {
return await parseResponse(
await fetch(
`/api/w/${
encodeURIComponent(workspaceId)
}/settings/workspace/signing-identity/provision`,
{ method: "POST" },
),
parseWorkspaceSigningIdentityResponse,
);
}
export async function fetchProfileSettings(
workspaceId: string,
): Promise<ProfileSettingsResponse> {
@@ -5,6 +5,7 @@
WorkspaceDeletionPreflightResponse,
WorkspaceDeletionRequest,
WorkspaceMetadataSettingsResponse,
WorkspaceSigningIdentityResponse,
} from '$lib/generated/workspace-api';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
@@ -18,6 +19,8 @@
import DiagnosticsList from '$lib/workspace/settings/DiagnosticsList.svelte';
import {
fetchWorkspaceMetadata,
fetchWorkspaceSigningIdentity,
provisionWorkspaceSigningIdentity,
updateWorkspaceMetadata,
} from '$lib/workspace/settings/profile-api';
import type { PageProps } from './$types';
@@ -26,6 +29,14 @@
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);
@@ -58,6 +69,17 @@
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) {
@@ -93,6 +115,30 @@
}
}
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;
@@ -232,6 +278,52 @@
</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>
@@ -286,6 +378,13 @@
{/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: 0.75rem; }
.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); }
+73
View File
@@ -28,6 +28,7 @@ import {
fetchWorkspaceMetadata,
parseProfileSettingsResponse,
parseWorkspaceMetadataSettingsResponse,
parseWorkspaceSigningIdentityResponse,
ProfileApiError,
updateWorkspaceMetadata,
} from "../src/lib/workspace/settings/profile-api.ts";
@@ -170,6 +171,78 @@ Deno.test("profile settings parser rejects missing, mistyped, stale, and invalid
);
});
Deno.test("Workspace signing identity parser validates active and pending public contracts", () => {
const active = {
identity: {
workspace_id: "workspace-1",
key_id: "WK-1",
algorithm: "ed25519",
public_key: "public-key",
public_key_fingerprint: "sha256:fingerprint",
revision: 1,
state: "active",
created_at: "2026-01-01T00:00:00Z",
provisioned_at: "2026-01-01T00:00:00Z",
},
public_bundle: {
workspace_id: "workspace-1",
backend_url: "https://backend.example.test",
key_id: "WK-1",
algorithm: "ed25519",
public_key: "public-key",
public_key_fingerprint: "sha256:fingerprint",
revision: 1,
},
};
assertEquals(
parseWorkspaceSigningIdentityResponse(active).public_bundle?.key_id,
"WK-1",
);
assertEquals(
parseWorkspaceSigningIdentityResponse({
identity: {
workspace_id: "workspace-1",
key_id: "WK-1",
algorithm: "ed25519",
revision: 1,
state: "pending_provisioning",
created_at: "2026-01-01T00:00:00Z",
},
}).public_bundle,
undefined,
);
for (
const mutate of [
(value: Record<string, unknown>) => {
value.private_material_ref = "must-not-be-accepted";
},
(value: Record<string, unknown>) => {
(value.identity as Record<string, unknown>).revision =
Number.MAX_SAFE_INTEGER + 1;
},
(value: Record<string, unknown>) => {
(value.identity as Record<string, unknown>).public_key = "x".repeat(
17_000,
);
},
(value: Record<string, unknown>) => {
(value.public_bundle as Record<string, unknown>).key_id = "WK-other";
},
(value: Record<string, unknown>) => {
delete value.public_bundle;
},
]
) {
const value = structuredClone(active);
mutate(value);
assertThrows(
() => parseWorkspaceSigningIdentityResponse(value),
ProfileApiError,
);
}
});
Deno.test("workspace metadata parser rejects incomplete or stale response fields", () => {
assertThrows(
() =>