merge: compose current auth migration with Ticket case schema 91
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:6f5ab2a613fca9996680d3a08962dc580563457811e68679574ff4325c86ec4e)
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/companion-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:6f5ab2a613fca9996680d3a08962dc580563457811e68679574ff4325c86ec4e)
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:6f5ab2a613fca9996680d3a08962dc580563457811e68679574ff4325c86ec4e)
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||
@@ -25,7 +25,7 @@ export type CompletionEntry = { description?: string | null; invocation?: Featur
|
||||
|
||||
export type CompletionKind = "file" | "feature" | "feature_argument";
|
||||
|
||||
export type CreateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; expected_binding_id?: string | null; public_bundle: RuntimePublicIdentityBundle; workspace_trust_id: string; };
|
||||
export type CreateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; expected_binding_id?: string | null; public_bundle: RuntimePublicIdentityBundle; };
|
||||
|
||||
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||
|
||||
@@ -77,11 +77,9 @@ export type RuntimeConfigBundleResult = { availability?: unknown; diagnostics?:
|
||||
|
||||
export type RuntimeConfigBundleSyncRequest = { bundle: unknown; };
|
||||
|
||||
export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavailable" | "revoked";
|
||||
|
||||
export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration";
|
||||
|
||||
export type RuntimeConnectionTestResponse = { actual_protocol_version?: number | null; binding_id: string; checked_at: string; connection_state: RuntimeConnectionDisplayState; diagnostics?: Array<Diagnostic>; expected_protocol_version: number; failure_kind?: RuntimeConnectionTestFailureKind | null; runtime_id: string; status: RuntimeConnectionTestStatus; verification?: RuntimeVerificationEvidenceSummary | null; workspace_id: string; };
|
||||
export type RuntimeConnectionTestResponse = { actual_protocol_version?: number | null; binding_id: string; checked_at: string; diagnostics?: Array<Diagnostic>; expected_protocol_version: number; failure_kind?: RuntimeConnectionTestFailureKind | null; runtime_id: string; status: RuntimeConnectionTestStatus; workspace_id: string; };
|
||||
|
||||
export type RuntimeConnectionTestStatus = "compatible" | "failed";
|
||||
|
||||
@@ -121,10 +119,6 @@ export type RuntimeTrustKeyState = { binding_id?: string | null; created_at?: st
|
||||
|
||||
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
|
||||
|
||||
export type RuntimeVerificationEvidenceSummary = { binding_id: string; last_checked_at: string; last_outcome: RuntimeVerificationOutcome; runtime_public_key_fingerprint: string; verified_at?: string | null; workspace_key_id: string; workspace_public_key_fingerprint: string; workspace_trust_id: string; };
|
||||
|
||||
export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed";
|
||||
|
||||
export type RuntimeWorkerCompletionsRequest = { context?: CompletionContext | null; kind: CompletionKind; prefix?: string; request_id?: string | null; };
|
||||
|
||||
export type RuntimeWorkerCompletionsResult = { context?: CompletionContext | null; diagnostics?: Array<Diagnostic>; entries: Array<CompletionEntry>; kind: CompletionKind; prefix: string; request_id?: string | null; runtime_id: string; worker_id: string; };
|
||||
@@ -217,9 +211,7 @@ export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_tar
|
||||
|
||||
export type WorkspaceConfigAccess = "read_only" | "read_write";
|
||||
|
||||
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked";
|
||||
|
||||
export type WorkspaceRuntimeBindingSummary = { binding_id: string; connection_state: RuntimeConnectionDisplayState; state: WorkspaceRuntimeBindingState; verification?: RuntimeVerificationEvidenceSummary | null; workspace_key_id?: string | null; workspace_trust_id?: string | null; };
|
||||
export type WorkspaceRuntimeBindingSummary = { binding_id: string; revoked_at?: string | null; };
|
||||
|
||||
export type WorkspaceRuntimeDetail = { endpoint?: string | null; recent_audit?: Array<RuntimeTrustAuditEntry>; runtime: WorkspaceRuntimeResource; trust_key: RuntimeTrustKeyState; workspace_id: string; };
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:6f5ab2a613fca9996680d3a08962dc580563457811e68679574ff4325c86ec4e)
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/ticket-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:6f5ab2a613fca9996680d3a08962dc580563457811e68679574ff4325c86ec4e)
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
|
||||
// Input: openapi/server-api.json
|
||||
// Input contract: 0.1.0 (sha256:6f5ab2a613fca9996680d3a08962dc580563457811e68679574ff4325c86ec4e)
|
||||
// Input contract: 0.1.0 (sha256:2e8dcd75d5d7b70c5d820999afcabf3d1377896732c7a3e8c3839e73a994974c)
|
||||
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
|
||||
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
|
||||
|
||||
@@ -2,15 +2,12 @@ import type {
|
||||
Diagnostic,
|
||||
RuntimeConnectionTestFailureKind,
|
||||
RuntimeConnectionTestResponse,
|
||||
RuntimeVerificationEvidenceSummary,
|
||||
} from "#lib/generated/runtime-api.ts";
|
||||
|
||||
const RESPONSE_KEYS = [
|
||||
"workspace_id",
|
||||
"runtime_id",
|
||||
"binding_id",
|
||||
"connection_state",
|
||||
"verification",
|
||||
"checked_at",
|
||||
"status",
|
||||
"failure_kind",
|
||||
@@ -108,27 +105,11 @@ export function parseRuntimeConnectionTestResponse(
|
||||
return null;
|
||||
}
|
||||
const bindingId = value.binding_id;
|
||||
const connectionState = parseConnectionState(value.connection_state);
|
||||
const verification = parseVerificationEvidence(value.verification);
|
||||
if (
|
||||
!isBoundedString(bindingId) ||
|
||||
connectionState === null ||
|
||||
(value.verification !== null && verification === null) ||
|
||||
(verification !== null &&
|
||||
verification.binding_id !== bindingId) ||
|
||||
(connectionState === "verified" && value.status !== "compatible") ||
|
||||
(connectionState === "verified" && verification !== null &&
|
||||
(verification.last_outcome !== "verified" ||
|
||||
verification.verified_at === null))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!isBoundedString(bindingId, 256)) return null;
|
||||
return {
|
||||
workspace_id: value.workspace_id,
|
||||
runtime_id: value.runtime_id,
|
||||
binding_id: bindingId,
|
||||
connection_state: connectionState,
|
||||
verification,
|
||||
checked_at: value.checked_at,
|
||||
status: value.status,
|
||||
failure_kind: failureKind as RuntimeConnectionTestFailureKind | null,
|
||||
@@ -138,60 +119,6 @@ export function parseRuntimeConnectionTestResponse(
|
||||
};
|
||||
}
|
||||
|
||||
function parseConnectionState(
|
||||
value: unknown,
|
||||
): "configured" | "verified" | "unavailable" | "revoked" | null {
|
||||
return value === "configured" || value === "verified" ||
|
||||
value === "unavailable" || value === "revoked"
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
function parseVerificationEvidence(
|
||||
value: unknown,
|
||||
): RuntimeVerificationEvidenceSummary | null {
|
||||
if (value === null) return null;
|
||||
const keys = [
|
||||
"verified_at",
|
||||
"last_checked_at",
|
||||
"last_outcome",
|
||||
"binding_id",
|
||||
"workspace_key_id",
|
||||
"workspace_public_key_fingerprint",
|
||||
"workspace_trust_id",
|
||||
"runtime_public_key_fingerprint",
|
||||
] as const;
|
||||
if (!isRecord(value) || !hasExactKeys(value, keys)) return null;
|
||||
if (
|
||||
(value.verified_at !== null &&
|
||||
(!isBoundedString(value.verified_at, 128) ||
|
||||
Number.isNaN(Date.parse(value.verified_at)))) ||
|
||||
!isBoundedString(value.last_checked_at, 128) ||
|
||||
Number.isNaN(Date.parse(value.last_checked_at)) ||
|
||||
(value.last_outcome !== "verified" &&
|
||||
value.last_outcome !== "challenge_issued" &&
|
||||
value.last_outcome !== "verification_failed" &&
|
||||
value.last_outcome !== "connectivity_failed") ||
|
||||
!isBoundedString(value.binding_id) ||
|
||||
!isBoundedString(value.workspace_key_id, 128) ||
|
||||
!isBoundedString(value.workspace_public_key_fingerprint) ||
|
||||
!isBoundedString(value.workspace_trust_id) ||
|
||||
!isBoundedString(value.runtime_public_key_fingerprint, 128)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
verified_at: value.verified_at,
|
||||
last_checked_at: value.last_checked_at,
|
||||
last_outcome: value.last_outcome,
|
||||
binding_id: value.binding_id,
|
||||
workspace_key_id: value.workspace_key_id,
|
||||
workspace_public_key_fingerprint: value.workspace_public_key_fingerprint,
|
||||
workspace_trust_id: value.workspace_trust_id,
|
||||
runtime_public_key_fingerprint: value.runtime_public_key_fingerprint,
|
||||
};
|
||||
}
|
||||
|
||||
export async function testRuntimeConnection(
|
||||
workspaceId: string,
|
||||
runtimeId: string,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
Diagnostic,
|
||||
RemoveRuntimeRequest,
|
||||
RevokeRuntimeTrustKeyRequest,
|
||||
RuntimeConnectionDisplayState,
|
||||
RuntimeIdentityAuthority,
|
||||
RuntimeManagementApiError,
|
||||
RuntimeManagementSummary,
|
||||
@@ -17,14 +16,13 @@ import type {
|
||||
RuntimeTrustKeyRevealResponse,
|
||||
RuntimeTrustKeyState,
|
||||
RuntimeTrustKeyStatus,
|
||||
RuntimeVerificationEvidenceSummary,
|
||||
UpdateRemoteRuntimeRequest,
|
||||
WorkspaceRuntimeBindingState,
|
||||
WorkspaceRuntimeBindingSummary,
|
||||
WorkspaceRuntimeDetail,
|
||||
WorkspaceRuntimeListResponse,
|
||||
WorkspaceRuntimeResource,
|
||||
} from "#lib/generated/runtime-api.ts";
|
||||
import type { RepositoryApiError } from "#lib/generated/repository-api.ts";
|
||||
import { workspaceApiPath } from "./http.ts";
|
||||
|
||||
export type WorkspaceRuntimeList =
|
||||
@@ -90,17 +88,6 @@ const CONFLICT_KINDS = new Set<RuntimeTrustConflictKind>([
|
||||
"stale_binding",
|
||||
"fingerprint_in_use",
|
||||
]);
|
||||
const BINDING_STATES = new Set<WorkspaceRuntimeBindingState>([
|
||||
"configured",
|
||||
"verified",
|
||||
"revoked",
|
||||
]);
|
||||
const CONNECTION_STATES = new Set<RuntimeConnectionDisplayState>([
|
||||
"configured",
|
||||
"verified",
|
||||
"unavailable",
|
||||
"revoked",
|
||||
]);
|
||||
const REMOVAL_OPERATION_STATES = new Set<RuntimeRemovalOperationState>([
|
||||
"pending",
|
||||
"cleanup_pending",
|
||||
@@ -326,140 +313,16 @@ function runtimeSource(value: unknown, path: string): RuntimeSourceSummary {
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeVerification(
|
||||
value: unknown,
|
||||
path: string,
|
||||
): RuntimeVerificationEvidenceSummary {
|
||||
const item = object(value, path);
|
||||
exactKeys(
|
||||
item,
|
||||
[
|
||||
"verified_at",
|
||||
"last_checked_at",
|
||||
"last_outcome",
|
||||
"binding_id",
|
||||
"workspace_key_id",
|
||||
"workspace_public_key_fingerprint",
|
||||
"workspace_trust_id",
|
||||
"runtime_public_key_fingerprint",
|
||||
],
|
||||
[],
|
||||
path,
|
||||
);
|
||||
const verifiedAt = item.verified_at === null
|
||||
? null
|
||||
: boundedString(item.verified_at, `${path}.verified_at`, 128);
|
||||
const lastOutcome = enumValue(
|
||||
item.last_outcome,
|
||||
`${path}.last_outcome`,
|
||||
new Set(
|
||||
[
|
||||
"verified",
|
||||
"challenge_issued",
|
||||
"verification_failed",
|
||||
"connectivity_failed",
|
||||
] as const,
|
||||
),
|
||||
);
|
||||
return {
|
||||
verified_at: verifiedAt,
|
||||
last_checked_at: boundedString(
|
||||
item.last_checked_at,
|
||||
`${path}.last_checked_at`,
|
||||
128,
|
||||
),
|
||||
last_outcome: lastOutcome,
|
||||
binding_id: boundedIdentity(
|
||||
item.binding_id,
|
||||
`${path}.binding_id`,
|
||||
),
|
||||
workspace_key_id: boundedString(
|
||||
item.workspace_key_id,
|
||||
`${path}.workspace_key_id`,
|
||||
LIMITS.idBytes,
|
||||
),
|
||||
workspace_public_key_fingerprint: boundedIdentity(
|
||||
item.workspace_public_key_fingerprint,
|
||||
`${path}.workspace_public_key_fingerprint`,
|
||||
),
|
||||
workspace_trust_id: boundedIdentity(
|
||||
item.workspace_trust_id,
|
||||
`${path}.workspace_trust_id`,
|
||||
),
|
||||
runtime_public_key_fingerprint: boundedString(
|
||||
item.runtime_public_key_fingerprint,
|
||||
`${path}.runtime_public_key_fingerprint`,
|
||||
LIMITS.fingerprintBytes,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeBinding(
|
||||
value: unknown,
|
||||
path: string,
|
||||
requiresWorkspaceIdentity: boolean,
|
||||
): WorkspaceRuntimeBindingSummary {
|
||||
const item = object(value, path);
|
||||
exactKeys(
|
||||
item,
|
||||
["state", "connection_state", "binding_id"],
|
||||
["workspace_key_id", "workspace_trust_id", "verification"],
|
||||
path,
|
||||
);
|
||||
const workspaceKeyId = optionalNullableString(
|
||||
item.workspace_key_id,
|
||||
`${path}.workspace_key_id`,
|
||||
LIMITS.idBytes,
|
||||
);
|
||||
const workspaceTrustId = optionalNullableIdentity(
|
||||
item.workspace_trust_id,
|
||||
`${path}.workspace_trust_id`,
|
||||
);
|
||||
const state = enumValue(item.state, `${path}.state`, BINDING_STATES);
|
||||
if (
|
||||
requiresWorkspaceIdentity &&
|
||||
state !== "revoked" &&
|
||||
(workspaceKeyId == null || workspaceTrustId == null)
|
||||
) {
|
||||
return fail(path, "requires Workspace signing key identity metadata");
|
||||
}
|
||||
const connectionState = enumValue(
|
||||
item.connection_state,
|
||||
`${path}.connection_state`,
|
||||
CONNECTION_STATES,
|
||||
);
|
||||
const binding_id = boundedIdentity(item.binding_id, `${path}.binding_id`);
|
||||
const verification = item.verification === undefined
|
||||
? undefined
|
||||
: runtimeVerification(item.verification, `${path}.verification`);
|
||||
if (
|
||||
verification !== undefined && verification.binding_id !== binding_id
|
||||
) {
|
||||
return fail(path, "verification must match the current binding binding_id");
|
||||
}
|
||||
if (
|
||||
requiresWorkspaceIdentity &&
|
||||
connectionState === "verified" &&
|
||||
(verification === undefined ||
|
||||
verification.verified_at === null ||
|
||||
verification.last_outcome !== "verified")
|
||||
) {
|
||||
return fail(
|
||||
path,
|
||||
"verified Workspace identity binding requires verification evidence",
|
||||
);
|
||||
}
|
||||
exactKeys(item, ["binding_id"], ["revoked_at"], path);
|
||||
return {
|
||||
state,
|
||||
connection_state: connectionState,
|
||||
binding_id,
|
||||
...(workspaceKeyId === undefined
|
||||
? {}
|
||||
: { workspace_key_id: workspaceKeyId }),
|
||||
...(workspaceTrustId === undefined
|
||||
? {}
|
||||
: { workspace_trust_id: workspaceTrustId }),
|
||||
...(verification === undefined ? {} : { verification }),
|
||||
binding_id: boundedIdentity(item.binding_id, `${path}.binding_id`),
|
||||
revoked_at:
|
||||
optionalNullableTimestamp(item.revoked_at, `${path}.revoked_at`) ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -483,7 +346,7 @@ function runtimeManagement(
|
||||
const builtIn = boolean(item.built_in, `${path}.built_in`);
|
||||
const binding = item.binding == null
|
||||
? undefined
|
||||
: runtimeBinding(item.binding, `${path}.binding`, !builtIn);
|
||||
: runtimeBinding(item.binding, `${path}.binding`);
|
||||
return {
|
||||
built_in: builtIn,
|
||||
config_managed: boolean(item.config_managed, `${path}.config_managed`),
|
||||
@@ -763,7 +626,7 @@ export function parseRuntimeTrustConflict(
|
||||
exactKeys(
|
||||
response,
|
||||
["error", "message"],
|
||||
["current_binding_id", "current_fingerprint"],
|
||||
["current_binding_id", "current_fingerprint", "diagnostics"],
|
||||
"Runtime trust conflict",
|
||||
);
|
||||
return {
|
||||
@@ -786,6 +649,15 @@ export function parseRuntimeTrustConflict(
|
||||
"Runtime trust conflict.current_fingerprint",
|
||||
LIMITS.fingerprintBytes,
|
||||
),
|
||||
...(response.diagnostics === undefined ? {} : {
|
||||
diagnostics: array(
|
||||
response.diagnostics,
|
||||
"Runtime trust conflict.diagnostics",
|
||||
LIMITS.diagnostics,
|
||||
).map((entry, index) =>
|
||||
diagnostic(entry, `Runtime trust conflict.diagnostics[${index}]`)
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -891,6 +763,37 @@ async function readBoundedJson(response: Response): Promise<unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
function repositoryApiError(value: unknown): RepositoryApiError {
|
||||
const response = object(value, "Runtime trust error");
|
||||
exactKeys(
|
||||
response,
|
||||
["error", "message"],
|
||||
["diagnostics"],
|
||||
"Runtime trust error",
|
||||
);
|
||||
return {
|
||||
error: boundedString(
|
||||
response.error,
|
||||
"Runtime trust error.error",
|
||||
LIMITS.idBytes,
|
||||
),
|
||||
message: boundedString(
|
||||
response.message,
|
||||
"Runtime trust error.message",
|
||||
LIMITS.conflictMessageBytes,
|
||||
),
|
||||
...(response.diagnostics === undefined ? {} : {
|
||||
diagnostics: array(
|
||||
response.diagnostics,
|
||||
"Runtime trust error.diagnostics",
|
||||
LIMITS.diagnostics,
|
||||
).map((entry, index) =>
|
||||
diagnostic(entry, `Runtime trust error.diagnostics[${index}]`)
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function requestErrorFrom(
|
||||
value: unknown,
|
||||
status: number,
|
||||
@@ -917,30 +820,13 @@ function requestErrorFrom(
|
||||
),
|
||||
);
|
||||
}
|
||||
exactKeys(
|
||||
response,
|
||||
["error", "message", "diagnostics"],
|
||||
[],
|
||||
"Runtime trust error",
|
||||
);
|
||||
const diagnostics = array(
|
||||
response.diagnostics,
|
||||
"Runtime trust error.diagnostics",
|
||||
LIMITS.diagnostics,
|
||||
).map((entry, index) =>
|
||||
diagnostic(entry, `Runtime trust error.diagnostics[${index}]`)
|
||||
);
|
||||
const message = boundedString(
|
||||
response.message,
|
||||
"Runtime trust error.message",
|
||||
LIMITS.conflictMessageBytes,
|
||||
);
|
||||
const field = diagnostics.some((entry) =>
|
||||
const error = repositoryApiError(response);
|
||||
const field = (error.diagnostics ?? []).some((entry) =>
|
||||
entry.code.startsWith("runtime_public_key_")
|
||||
)
|
||||
? "public_key"
|
||||
: null;
|
||||
return new RuntimeTrustRequestError(message, field);
|
||||
return new RuntimeTrustRequestError(error.message, field);
|
||||
} catch {
|
||||
return new RuntimeTrustRequestError(
|
||||
`Runtime trust request failed (${status})`,
|
||||
@@ -960,6 +846,21 @@ function throwRuntimeTrustConflict(payload: unknown): never {
|
||||
throw new RuntimeTrustConflictError(conflict);
|
||||
}
|
||||
|
||||
function throwRuntimeCreationConflict(payload: unknown): never {
|
||||
let error: RepositoryApiError;
|
||||
try {
|
||||
error = repositoryApiError(payload);
|
||||
} catch {
|
||||
throw new RuntimeTrustRequestError(
|
||||
"Runtime trust conflict response was invalid",
|
||||
);
|
||||
}
|
||||
if (CONFLICT_KINDS.has(error.error as RuntimeTrustConflictKind)) {
|
||||
throwRuntimeTrustConflict(error);
|
||||
}
|
||||
throw requestErrorFrom(error, 409);
|
||||
}
|
||||
|
||||
async function finishMutation(
|
||||
response: Response,
|
||||
workspaceId: string,
|
||||
@@ -990,17 +891,16 @@ export async function createRemoteRuntime(
|
||||
request: CreateRemoteRuntimeRequest,
|
||||
fetchImpl: typeof fetch = fetch,
|
||||
): Promise<WorkspaceRuntimeResource> {
|
||||
const enrollmentId = boundedIdentity(request.workspace_trust_id, "workspace_trust_id");
|
||||
const response = await fetchImpl(
|
||||
workspaceApiPath(workspaceId, "/runtimes"),
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...request, workspace_trust_id: enrollmentId }),
|
||||
body: JSON.stringify(request),
|
||||
},
|
||||
);
|
||||
const payload = await readBoundedJson(response);
|
||||
if (response.status === 409) throwRuntimeTrustConflict(payload);
|
||||
if (response.status === 409) throwRuntimeCreationConflict(payload);
|
||||
if (!response.ok) throw requestErrorFrom(payload, response.status);
|
||||
const runtime = runtimeResource(payload, "Runtime create response");
|
||||
if (runtime.runtime_id !== request.public_bundle.identity_id) {
|
||||
|
||||
@@ -68,6 +68,6 @@ export function repositorySshProbeRuntimes(
|
||||
runtime.management.endpoint_configured &&
|
||||
runtime.management.binding !== undefined &&
|
||||
runtime.management.binding !== null &&
|
||||
runtime.management.binding.state !== "revoked"
|
||||
runtime.management.binding.revoked_at == null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let runtimePublicBundle = $state('');
|
||||
let workspaceTrustId = $state('');
|
||||
let displayName = $state('');
|
||||
let endpoint = $state('');
|
||||
let runtimeFingerprint = $state<string | null>(null);
|
||||
@@ -36,9 +35,7 @@
|
||||
|
||||
function connectionTestSummary(result: RuntimeConnectionTestResponse): string {
|
||||
if (result.status === 'compatible') {
|
||||
return result.connection_state === 'verified'
|
||||
? `Verified · protocol v${result.actual_protocol_version}`
|
||||
: `Compatible · ${result.connection_state} · protocol v${result.actual_protocol_version}`;
|
||||
return `Signed ping succeeded · protocol v${result.actual_protocol_version}`;
|
||||
}
|
||||
switch (result.failure_kind) {
|
||||
case 'authentication': return 'Authentication failed';
|
||||
@@ -145,18 +142,16 @@
|
||||
}
|
||||
await createRemoteRuntime(data.workspaceId, {
|
||||
public_bundle: publicBundle,
|
||||
workspace_trust_id: workspaceTrustId,
|
||||
display_name: displayName || null,
|
||||
endpoint,
|
||||
expected_binding_id: null,
|
||||
});
|
||||
runtimePublicBundle = '';
|
||||
workspaceTrustId = '';
|
||||
runtimeFingerprint = null;
|
||||
displayName = '';
|
||||
endpoint = '';
|
||||
showAddRuntime = false;
|
||||
requestNotice = 'Runtime registered for this Workspace. Run Test to complete authenticated verification.';
|
||||
requestNotice = 'Runtime registered for this Workspace. Run Test to observe a signed ping.';
|
||||
await invalidateAll();
|
||||
} catch (error) {
|
||||
requestError = error instanceof RuntimeTrustRequestError || error instanceof Error
|
||||
@@ -171,7 +166,8 @@
|
||||
runtime: WorkspaceRuntimeResource,
|
||||
): RuntimeConnectionTestResponse | undefined {
|
||||
const result = testResults[runtime.runtime_id];
|
||||
return result?.binding_id === runtime.management?.binding?.binding_id
|
||||
return result?.workspace_id === data.workspaceId &&
|
||||
result?.binding_id === runtime.management?.binding?.binding_id
|
||||
? result
|
||||
: undefined;
|
||||
}
|
||||
@@ -179,12 +175,16 @@
|
||||
async function testRuntime(runtime: WorkspaceRuntimeResource): Promise<void> {
|
||||
const bindingId = runtime.management?.binding?.binding_id;
|
||||
if (typeof bindingId !== 'string') return;
|
||||
const workspaceId = data.workspaceId;
|
||||
const generation = ++connectionTestGeneration;
|
||||
requestError = null;
|
||||
const { [runtime.runtime_id]: _previousResult, ...remainingResults } = testResults;
|
||||
testResults = remainingResults;
|
||||
busyRuntimeId = runtime.runtime_id;
|
||||
try {
|
||||
const result = await testRuntimeConnection(data.workspaceId, runtime.runtime_id);
|
||||
if (
|
||||
workspaceId !== data.workspaceId ||
|
||||
generation !== connectionTestGeneration ||
|
||||
result.binding_id !== bindingId
|
||||
) {
|
||||
@@ -261,15 +261,9 @@
|
||||
</button>
|
||||
<pre>yoi-runtime trust-workspace add --bundle {workspaceBundleFilename()}</pre>
|
||||
<small>
|
||||
Copy the Runtime-issued <code>workspace_trust_id</code> from <code>trust-workspace add/show</code>.
|
||||
It identifies this enrollment, not the Workspace key fingerprint.
|
||||
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>
|
||||
<label>
|
||||
Workspace trust ID
|
||||
<input bind:value={workspaceTrustId} required autocomplete="off" spellcheck="false" />
|
||||
</label>
|
||||
{:else}
|
||||
<p class="section-state">Loading Workspace public identity…</p>
|
||||
{/if}
|
||||
@@ -323,14 +317,14 @@
|
||||
</div>
|
||||
<p>
|
||||
Registration stores this Workspace-scoped binding. After it appears in the list, run
|
||||
<strong>Test</strong> to complete authenticated verification.
|
||||
<strong>Test</strong> to observe a signed ping. Each request authenticates independently.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="settings-action-row">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={busyRuntimeId !== null || !data.signingIdentity?.public_bundle || !runtimeFingerprint || !workspaceTrustId}
|
||||
disabled={busyRuntimeId !== null || !runtimeFingerprint}
|
||||
>Register Runtime</button>
|
||||
<button type="button" disabled={busyRuntimeId !== null} onclick={() => showAddRuntime = false}>
|
||||
Cancel
|
||||
@@ -368,7 +362,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.runtimes.items as runtime}
|
||||
<tr class:inactive={runtime.status !== 'active'}>
|
||||
<tr class:inactive={Boolean(runtime.management?.binding?.revoked_at) || currentTestResult(runtime)?.status === 'failed'}>
|
||||
<td>
|
||||
<strong>
|
||||
<a class="inline-link" href={`/w/${encodeURIComponent(data.workspaceId)}/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}`}>
|
||||
@@ -379,7 +373,7 @@
|
||||
</td>
|
||||
<td>{runtime.kind}</td>
|
||||
<td>
|
||||
{runtime.management?.binding?.connection_state ?? runtime.status}
|
||||
{runtime.management?.binding?.revoked_at ? 'Revoked' : currentTestResult(runtime) ? connectionTestSummary(currentTestResult(runtime)!) : 'Not tested'}
|
||||
</td>
|
||||
<td>{runtimePlatform(runtime)}</td>
|
||||
<td>{managementLabel(runtime)}</td>
|
||||
@@ -390,16 +384,14 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="settings-action-row">
|
||||
{#if runtime.management?.config_managed && runtime.management.binding?.connection_state !== 'revoked'}
|
||||
{#if runtime.management?.config_managed && !runtime.management.binding?.revoked_at}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyRuntimeId !== null}
|
||||
onclick={() => testRuntime(runtime)}
|
||||
>Test</button>
|
||||
{/if}
|
||||
{#if runtime.management?.binding?.state === 'configured'}
|
||||
<span class="settings-muted-action">Verification required</span>
|
||||
{:else if !runtime.management?.config_managed}
|
||||
{#if !runtime.management?.config_managed}
|
||||
<span class="settings-muted-action">Test unavailable</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+53
-33
@@ -2,6 +2,7 @@
|
||||
import { goto, invalidateAll } from '$app/navigation';
|
||||
import type {
|
||||
RevokeRuntimeTrustKeyRequest,
|
||||
RuntimeConnectionTestResponse,
|
||||
RuntimeTrustKeyStatus,
|
||||
} from '#lib/generated/runtime-api.ts';
|
||||
import {
|
||||
@@ -17,6 +18,7 @@
|
||||
RuntimeTrustRequestError,
|
||||
type RuntimeTrustRouteOperation,
|
||||
} from '#lib/workspace/api/runtime-management.ts';
|
||||
import { testRuntimeConnection } from '#lib/workspace/api/runtime-connection.ts';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
type TrustAction = 'create' | 'replace' | 'reactivate';
|
||||
@@ -25,18 +27,19 @@
|
||||
let showPublicKey = $state(false);
|
||||
let revealedPublicKey = $state<string | null>(null);
|
||||
let publicKey = $state('');
|
||||
let workspaceTrustId = $state('');
|
||||
let displayName = $state('');
|
||||
let endpoint = $state('');
|
||||
let editingMetadata = $state(false);
|
||||
let deleteRuntimeConfirmation = $state('');
|
||||
let busyAction = $state<'metadata' | 'trust' | 'revoke' | 'reveal' | 'copy' | 'delete' | null>(null);
|
||||
let busyAction = $state<'metadata' | 'trust' | 'revoke' | 'reveal' | 'copy' | 'delete' | 'test' | 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);
|
||||
let replacementFingerprintError = $state<string | null>(null);
|
||||
let connectionResult = $state<RuntimeConnectionTestResponse | null>(null);
|
||||
let connectionError = $state<string | null>(null);
|
||||
let fingerprintGeneration = 0;
|
||||
const routeFence = new RuntimeTrustRouteFence();
|
||||
const runtimeRemovalAttempt = new RuntimeRemovalAttempt();
|
||||
@@ -47,10 +50,11 @@
|
||||
if (nextGeneration === routeGeneration) return;
|
||||
routeGeneration = nextGeneration;
|
||||
fingerprintGeneration += 1;
|
||||
connectionResult = null;
|
||||
connectionError = null;
|
||||
showPublicKey = false;
|
||||
revealedPublicKey = null;
|
||||
publicKey = '';
|
||||
workspaceTrustId = '';
|
||||
displayName = data.runtimeDetail?.runtime.label ?? '';
|
||||
endpoint = data.runtimeDetail?.endpoint ?? '';
|
||||
editingMetadata = false;
|
||||
@@ -109,6 +113,28 @@
|
||||
return new TextEncoder().encode(value).byteLength;
|
||||
}
|
||||
|
||||
async function testConnection(): Promise<void> {
|
||||
const bindingId = data.runtimeDetail?.runtime.management.binding?.binding_id;
|
||||
if (busyAction !== null || !bindingId) return;
|
||||
const operation = routeFence.capture(data.runtimeId);
|
||||
const workspaceId = data.workspaceId;
|
||||
busyAction = 'test';
|
||||
connectionResult = null;
|
||||
connectionError = null;
|
||||
try {
|
||||
const result = await testRuntimeConnection(workspaceId, operation.runtimeId);
|
||||
if (!isCurrentRoute(operation) || workspaceId !== data.workspaceId ||
|
||||
result.binding_id !== data.runtimeDetail?.runtime.management.binding?.binding_id) return;
|
||||
connectionResult = result;
|
||||
} catch (error) {
|
||||
if (isCurrentRoute(operation) && workspaceId === data.workspaceId) {
|
||||
connectionError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
} finally {
|
||||
if (isCurrentRoute(operation) && workspaceId === data.workspaceId) busyAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadAuthority(): Promise<void> {
|
||||
await invalidateAll();
|
||||
}
|
||||
@@ -195,7 +221,7 @@
|
||||
const binding = data.runtimeDetail.runtime.management.binding;
|
||||
if (!binding || !data.runtimeDetail.endpoint) {
|
||||
throw new RuntimeTrustRequestError(
|
||||
'The Workspace identity binding and authoritative Runtime endpoint are required.',
|
||||
'The Runtime binding and authoritative endpoint are required.',
|
||||
);
|
||||
}
|
||||
await createRemoteRuntime(data.workspaceId, {
|
||||
@@ -203,14 +229,12 @@
|
||||
identity_id: operation.runtimeId,
|
||||
public_key: key,
|
||||
},
|
||||
workspace_trust_id: workspaceTrustId,
|
||||
display_name: data.runtimeDetail.runtime.label,
|
||||
endpoint: data.runtimeDetail.endpoint,
|
||||
expected_binding_id: binding.binding_id,
|
||||
});
|
||||
if (!isCurrentRoute(operation)) return;
|
||||
publicKey = '';
|
||||
workspaceTrustId = '';
|
||||
showPublicKey = false;
|
||||
revealedPublicKey = null;
|
||||
successMessage = action === 'create'
|
||||
@@ -396,8 +420,8 @@
|
||||
{@const detail = data.runtimeDetail}
|
||||
{@const runtime = detail.runtime}
|
||||
{@const trust = detail.trust_key}
|
||||
{@const verifiedTrust = runtime.management.binding?.state === 'verified'}
|
||||
{@const currentAction = trustAction(trust.status)}
|
||||
{@const latestResult = connectionResult?.workspace_id === data.workspaceId && connectionResult?.runtime_id === data.runtimeId && connectionResult?.binding_id === runtime.management.binding?.binding_id ? connectionResult : null}
|
||||
|
||||
<section class="runtime-detail-section" aria-labelledby="runtime-identity-heading">
|
||||
<h2 id="runtime-identity-heading">Identity and binding</h2>
|
||||
@@ -405,17 +429,32 @@
|
||||
<div><dt>Runtime ID</dt><dd><code>{runtime.runtime_id}</code></dd></div>
|
||||
<div><dt>Kind</dt><dd>{runtime.kind}</dd></div>
|
||||
<div><dt>Endpoint</dt><dd>{detail.endpoint ?? 'Not configured'}</dd></div>
|
||||
<div><dt>Status</dt><dd>{runtime.status}</dd></div>
|
||||
<div><dt>Connection state</dt><dd>{runtime.management.binding?.connection_state ?? 'Not configured'}</dd></div>
|
||||
<div><dt>Workspace signing key</dt><dd><code>{runtime.management.binding?.workspace_key_id ?? '—'}</code></dd></div>
|
||||
<div><dt>Verified</dt><dd>{formatTimestamp(runtime.management.binding?.verification?.verified_at)}</dd></div>
|
||||
<div><dt>Last verification check</dt><dd>{runtime.management.binding?.verification?.last_outcome ?? '—'} · {formatTimestamp(runtime.management.binding?.verification?.last_checked_at)}</dd></div>
|
||||
<div><dt>Connection test</dt><dd>{runtime.management.binding?.revoked_at ? 'Revoked' : latestResult ? latestResult.status === 'compatible' ? 'Signed ping succeeded' : `Signed ping failed · ${latestResult.failure_kind}` : 'Not tested'}</dd></div>
|
||||
<div><dt>Binding ID</dt><dd><code>{runtime.management.binding?.binding_id ?? '—'}</code></dd></div>
|
||||
<div><dt>Runtime key status</dt><dd>{trust.status}</dd></div>
|
||||
<div><dt>Fingerprint</dt><dd><code>{trust.fingerprint ?? '—'}</code></dd></div>
|
||||
<div><dt>Created</dt><dd>{formatTimestamp(trust.created_at)}</dd></div>
|
||||
<div><dt>Updated</dt><dd>{formatTimestamp(trust.updated_at)}</dd></div>
|
||||
<div><dt>Revoked</dt><dd>{formatTimestamp(trust.revoked_at)}</dd></div>
|
||||
</dl>
|
||||
{#if runtime.management.config_managed && !runtime.management.binding?.revoked_at}
|
||||
<div class="settings-action-row">
|
||||
<button type="button" disabled={busyAction !== null} onclick={testConnection}>
|
||||
{busyAction === 'test' ? 'Testing…' : 'Test'}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if latestResult}
|
||||
<p class="section-state" class:error={latestResult.status === 'failed'}>
|
||||
Checked {formatTimestamp(latestResult.checked_at)}
|
||||
{#each latestResult.diagnostics ?? [] as diagnostic}
|
||||
<span>{diagnostic.message}</span>
|
||||
{/each}
|
||||
</p>
|
||||
{/if}
|
||||
{#if connectionError}
|
||||
<p class="section-state error" role="alert">{connectionError}</p>
|
||||
{/if}
|
||||
{#if (runtime.diagnostics ?? []).length > 0}
|
||||
<ul class="settings-diagnostics-list">
|
||||
{#each runtime.diagnostics ?? [] as diagnostic}
|
||||
@@ -495,25 +534,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if verifiedTrust}
|
||||
<div class="runtime-public-key-readonly">
|
||||
<strong>Runtime public key</strong>
|
||||
<p>
|
||||
This verified key is read-only. Revoke Workspace trust and register the Runtime again to use a different public key.
|
||||
</p>
|
||||
<code>{trust.fingerprint}</code>
|
||||
</div>
|
||||
{:else}
|
||||
<form class="runtime-trust-form" onsubmit={saveTrustKey}>
|
||||
<label for="runtime-workspace-trust-id-input">Workspace trust ID</label>
|
||||
<input
|
||||
id="runtime-workspace-trust-id-input"
|
||||
bind:value={workspaceTrustId}
|
||||
required
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p>Copy the Runtime-issued <code>workspace_trust_id</code> from <code>trust-workspace add/show</code> for the current enrollment. Do not use a key fingerprint or a revoked enrollment ID.</p>
|
||||
<form class="runtime-trust-form" onsubmit={saveTrustKey}>
|
||||
<label for="runtime-public-key-input">Runtime public key</label>
|
||||
<textarea
|
||||
id="runtime-public-key-input"
|
||||
@@ -548,8 +569,7 @@
|
||||
{busyAction === 'trust' ? 'Saving…' : actionLabel(currentAction)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<div class="runtime-revoke-row">
|
||||
<div>
|
||||
|
||||
+7
-21
@@ -10,7 +10,6 @@ import { afterEach, expect, test, vi } from "vitest";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import RuntimeSettings from "./+page.svelte";
|
||||
import RuntimeDetail from "./[runtimeId]/+page.svelte";
|
||||
import { identityFixture } from "#lib/workspace/settings/identity.test-fixtures.ts";
|
||||
|
||||
const api = vi.hoisted(() => ({ create: vi.fn(), fingerprint: vi.fn() }));
|
||||
vi.mock("$app/navigation", () => ({ invalidateAll: vi.fn(async () => {}) }));
|
||||
@@ -27,14 +26,15 @@ afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test("Runtime registration requires and preserves the Runtime-issued enrollment ID independently of the fingerprint", async () => {
|
||||
test("Runtime registration uses public bundle without requiring copied Workspace enrollment identity", async () => {
|
||||
api.fingerprint.mockResolvedValue("sha256:runtime-public-key");
|
||||
api.create.mockResolvedValue({});
|
||||
const data = {
|
||||
workspaceId: "workspace-a",
|
||||
workspace: { permissions: { manage_runtimes: true } },
|
||||
runtimes: { items: [], diagnostics: [] },
|
||||
signingIdentity: identityFixture("workspace-a"),
|
||||
signingIdentity: null,
|
||||
signingIdentityError: "Workspace public identity could not be loaded",
|
||||
} as unknown as ComponentProps<typeof RuntimeSettings>["data"];
|
||||
render(RuntimeSettings, {
|
||||
props: { data, params: { workspaceId: "workspace-a" } },
|
||||
@@ -61,11 +61,7 @@ test("Runtime registration requires and preserves the Runtime-issued enrollment
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText("sha256:runtime-public-key")).toBeTruthy()
|
||||
);
|
||||
expect(submit).toHaveProperty("disabled", true);
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
await fireEvent.input(screen.getByLabelText("Workspace trust ID"), {
|
||||
target: { value: "Runtime-issued-enrollment-A" },
|
||||
});
|
||||
expect(screen.queryByLabelText("Workspace trust ID")).toBeNull();
|
||||
expect(submit).toHaveProperty("disabled", false);
|
||||
await fireEvent.submit(submit.closest("form")!);
|
||||
await waitFor(() => expect(api.create).toHaveBeenCalledOnce());
|
||||
@@ -74,14 +70,13 @@ test("Runtime registration requires and preserves the Runtime-issued enrollment
|
||||
identity_id: "runtime-a",
|
||||
public_key: "runtime-public-key",
|
||||
},
|
||||
workspace_trust_id: "Runtime-issued-enrollment-A",
|
||||
display_name: null,
|
||||
endpoint: "https://runtime.example",
|
||||
expected_binding_id: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("Runtime reactivation submits a new enrollment ID with the observed binding instead of reusing revoked trust", async () => {
|
||||
test("Runtime reactivation uses the observed binding without copied Workspace identity", async () => {
|
||||
api.fingerprint.mockResolvedValue("sha256:replacement-key");
|
||||
api.create.mockResolvedValue({});
|
||||
const data = {
|
||||
@@ -100,10 +95,8 @@ test("Runtime reactivation submits a new enrollment ID with the observed binding
|
||||
config_managed: true,
|
||||
removable: true,
|
||||
binding: {
|
||||
state: "revoked",
|
||||
connection_state: "revoked",
|
||||
revoked_at: "2026-10-10T00:00:00Z",
|
||||
binding_id: "observed-binding",
|
||||
workspace_trust_id: "revoked-enrollment",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -121,13 +114,7 @@ test("Runtime reactivation submits a new enrollment ID with the observed binding
|
||||
params: { workspaceId: "workspace-a", runtimeId: "runtime-a" },
|
||||
},
|
||||
});
|
||||
expect(screen.getByLabelText("Workspace trust ID")).toHaveProperty(
|
||||
"value",
|
||||
"",
|
||||
);
|
||||
await fireEvent.input(screen.getByLabelText("Workspace trust ID"), {
|
||||
target: { value: "new-enrollment" },
|
||||
});
|
||||
expect(screen.queryByLabelText("Workspace trust ID")).toBeNull();
|
||||
await fireEvent.input(screen.getByLabelText("Runtime public key"), {
|
||||
target: { value: "yoi-ed25519-pub:v1:new-key" },
|
||||
});
|
||||
@@ -145,7 +132,6 @@ test("Runtime reactivation submits a new enrollment ID with the observed binding
|
||||
identity_id: "runtime-a",
|
||||
public_key: "yoi-ed25519-pub:v1:new-key",
|
||||
},
|
||||
workspace_trust_id: "new-enrollment",
|
||||
display_name: "Runtime A",
|
||||
endpoint: "https://runtime.example",
|
||||
expected_binding_id: "observed-binding",
|
||||
|
||||
@@ -54,7 +54,8 @@ Deno.test("Repository SSH probe offers configured remote Runtimes regardless of
|
||||
endpoint_configured: true,
|
||||
endpoint_display: "https://arcadia.example",
|
||||
binding: {
|
||||
state: "verified",
|
||||
binding_id: "observed-binding",
|
||||
revoked_at: null,
|
||||
},
|
||||
},
|
||||
} as unknown as WorkspaceRuntimeResource;
|
||||
@@ -68,7 +69,10 @@ Deno.test("Repository SSH probe offers configured remote Runtimes regardless of
|
||||
runtime_id: "revoked",
|
||||
management: {
|
||||
...configured.management,
|
||||
binding: { state: "revoked" },
|
||||
binding: {
|
||||
binding_id: "revoked-binding",
|
||||
revoked_at: "2026-10-10T00:00:00Z",
|
||||
},
|
||||
},
|
||||
} as unknown as WorkspaceRuntimeResource;
|
||||
const unbound = {
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { assert, assertEquals } from "jsr:@std/assert@1.0.19";
|
||||
import { chromium } from "npm:playwright@1.59.1";
|
||||
|
||||
// Production static build plus loopback fixtures only; no live accounts or keys.
|
||||
// --before retains the superseded wire shape solely for before/after visual evidence.
|
||||
Deno.test("Runtime settings keep list, editing and testing accessible after signed-ping authentication failure", async () => {
|
||||
const before = Deno.args.includes("--before");
|
||||
const root = new URL("../../../../", import.meta.url).pathname;
|
||||
const output = new URL(
|
||||
`../../.svelte-kit/runtime-corrective/${before ? "before" : "after"}/`,
|
||||
import.meta.url,
|
||||
).pathname;
|
||||
await Deno.mkdir(output, { recursive: true });
|
||||
const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 });
|
||||
const port = (listener.addr as Deno.NetAddr).port;
|
||||
listener.close();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const server = new Deno.Command(Deno.execPath(), {
|
||||
args: [
|
||||
"run",
|
||||
"--allow-net",
|
||||
"--allow-read",
|
||||
`${root}tools/web-ux/browser-tests/workspace_home_fixture_server.ts`,
|
||||
String(port),
|
||||
`${root}web/workspace/build`,
|
||||
],
|
||||
stdout: "null",
|
||||
stderr: "null",
|
||||
}).spawn();
|
||||
try {
|
||||
// Wait for an observable ready response, bounded by timeout.
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let stopped = false;
|
||||
const deadline = setTimeout(() => {
|
||||
stopped = true;
|
||||
reject(new Error("fixture readiness timed out"));
|
||||
}, 10000);
|
||||
const probe = async () => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
const response = await fetch(`${base}/health`);
|
||||
await response.body?.cancel();
|
||||
if (response.ok) {
|
||||
clearTimeout(deadline);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
} catch { /* owned server is starting */ }
|
||||
setTimeout(probe, 25);
|
||||
};
|
||||
void probe();
|
||||
});
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const evidence: unknown[] = [];
|
||||
try {
|
||||
for (const colorScheme of ["light", "dark"] as const) {
|
||||
for (const width of [1440, 768, 390]) {
|
||||
for (const owner of [true, false]) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width, height: 900 },
|
||||
colorScheme,
|
||||
reducedMotion: "reduce",
|
||||
});
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
const errors: string[] = [];
|
||||
let documentStatus: number | null = null;
|
||||
const navigate = async (url: string) => {
|
||||
documentStatus = (await page.goto(url))?.status() ?? null;
|
||||
assertEquals(documentStatus, 200);
|
||||
};
|
||||
page.on("response", (response) => {
|
||||
if (response.status() >= 400) {
|
||||
errors.push(`${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
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()));
|
||||
const workspace = owner ? "home-owner" : "home-member";
|
||||
const binding = before
|
||||
? {
|
||||
binding_id: "binding-observed",
|
||||
state: "configured",
|
||||
connection_state: "unavailable",
|
||||
workspace_key_id: "workspace-key",
|
||||
workspace_trust_id: "enrollment",
|
||||
}
|
||||
: { binding_id: "binding-observed", revoked_at: null };
|
||||
const runtime = {
|
||||
runtime_id: "fixture-runtime",
|
||||
label: "Development Runtime",
|
||||
kind: "remote",
|
||||
status: "unavailable",
|
||||
source: {
|
||||
kind: "remote_http",
|
||||
status: "active",
|
||||
identity_authority: "server_runtime_configuration",
|
||||
note: "Fixture",
|
||||
},
|
||||
host_ids: [],
|
||||
worker_creation_available: false,
|
||||
os: "linux",
|
||||
arch: "x86_64",
|
||||
diagnostics: [{
|
||||
code: "runtime_authentication_failed",
|
||||
severity: "error",
|
||||
message:
|
||||
"The signed Runtime request was not authenticated. Update the endpoint or key and retry.",
|
||||
}],
|
||||
management: {
|
||||
built_in: false,
|
||||
config_managed: true,
|
||||
removable: true,
|
||||
endpoint_configured: true,
|
||||
token_ref_configured: false,
|
||||
binding,
|
||||
},
|
||||
};
|
||||
await page.route(
|
||||
"**/api/w/*/runtimes",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
workspace_id: workspace,
|
||||
limit: 200,
|
||||
source: "fixture",
|
||||
items: [runtime],
|
||||
diagnostics: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await page.route(
|
||||
"**/api/w/*/runtimes/fixture-runtime",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
workspace_id: workspace,
|
||||
runtime,
|
||||
endpoint: "https://runtime.example.test",
|
||||
trust_key: {
|
||||
status: "active",
|
||||
binding_id: "binding-observed",
|
||||
fingerprint: "sha256:runtime-public-key",
|
||||
created_at: "2026-10-01T00:00:00Z",
|
||||
updated_at: "2026-10-01T00:00:00Z",
|
||||
revoked_at: null,
|
||||
},
|
||||
recent_audit: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await page.route(
|
||||
"**/api/w/*/runtimes/fixture-runtime/connection-tests",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
workspace_id: workspace,
|
||||
runtime_id: "fixture-runtime",
|
||||
binding_id: "binding-observed",
|
||||
...(before
|
||||
? {
|
||||
connection_state: "unavailable",
|
||||
verification: null,
|
||||
}
|
||||
: {}),
|
||||
checked_at: "2026-10-10T00:00:00Z",
|
||||
status: "failed",
|
||||
failure_kind: "authentication",
|
||||
expected_protocol_version: 1,
|
||||
actual_protocol_version: null,
|
||||
diagnostics: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const prefix = `${colorScheme}-${width}-${
|
||||
owner ? "owner" : "member"
|
||||
}`;
|
||||
const capture = async (name: string) => {
|
||||
await page.screenshot({
|
||||
path: `${output}${prefix}-${name}.png`,
|
||||
fullPage: true,
|
||||
});
|
||||
evidence.push({
|
||||
colorScheme,
|
||||
width,
|
||||
owner,
|
||||
name,
|
||||
url: page.url(),
|
||||
documentStatus,
|
||||
screenshot: `${prefix}-${name}.png`,
|
||||
errors: [...errors],
|
||||
accessibility: await page.locator("main").ariaSnapshot(),
|
||||
});
|
||||
assertEquals(
|
||||
await page.evaluate(
|
||||
"document.documentElement.scrollWidth > innerWidth",
|
||||
),
|
||||
false,
|
||||
prefix,
|
||||
);
|
||||
};
|
||||
await navigate(`${base}/w/${workspace}/settings/runtimes`);
|
||||
await page.getByRole("link", {
|
||||
name: "Development Runtime",
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await page.getByRole("button", { name: "Test", exact: true })
|
||||
.click();
|
||||
await page.getByText("Connection test: Authentication failed", {
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await capture("list-failed");
|
||||
assertEquals(
|
||||
await page.getByRole("button", { name: "Test", exact: true })
|
||||
.isEnabled(),
|
||||
true,
|
||||
);
|
||||
if (owner) {
|
||||
await page.getByRole("button", {
|
||||
name: "Add Runtime",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByLabel("Runtime public bundle", { exact: true })
|
||||
.waitFor();
|
||||
await capture("registration");
|
||||
}
|
||||
await navigate(
|
||||
`${base}/w/${workspace}/settings/runtimes/fixture-runtime`,
|
||||
);
|
||||
await page.getByRole("heading", {
|
||||
name: "Identity and binding",
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await capture("detail");
|
||||
if (!before) {
|
||||
await page.getByRole("button", { name: "Test", exact: true })
|
||||
.click();
|
||||
await page.getByText("Signed ping failed · authentication", {
|
||||
exact: true,
|
||||
}).waitFor();
|
||||
await capture("detail-failed");
|
||||
assert(
|
||||
await page.getByRole("button", { name: "Test", exact: true })
|
||||
.isEnabled(),
|
||||
);
|
||||
}
|
||||
assertEquals(
|
||||
await page.getByRole("button", {
|
||||
name: "Edit Runtime",
|
||||
exact: true,
|
||||
}).count(),
|
||||
owner ? 1 : 0,
|
||||
);
|
||||
if (owner) {
|
||||
await page.getByRole("button", {
|
||||
name: "Edit Runtime",
|
||||
exact: true,
|
||||
}).click();
|
||||
await page.getByLabel("Endpoint", { exact: true }).waitFor();
|
||||
await capture("edit");
|
||||
assert(
|
||||
await page.getByRole("button", {
|
||||
name: "Save Runtime settings",
|
||||
exact: true,
|
||||
}).isEnabled(),
|
||||
);
|
||||
if (!before) {
|
||||
assertEquals(
|
||||
await page.getByLabel("Workspace trust ID", { exact: true })
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!before) {
|
||||
Object.assign(binding, { revoked_at: "2026-10-10T00:00:00Z" });
|
||||
await page.goto(`${base}/w/${workspace}/settings/runtimes`);
|
||||
await page.getByRole("cell", { name: "Revoked", exact: true })
|
||||
.waitFor();
|
||||
assertEquals(
|
||||
await page.getByRole("button", { name: "Test", exact: true })
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
await capture("revoked");
|
||||
}
|
||||
assertEquals(errors, []);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
await Deno.writeTextFile(
|
||||
`${output}review-context.json`,
|
||||
JSON.stringify(evidence, null, 2),
|
||||
);
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
await server.status;
|
||||
}
|
||||
});
|
||||
@@ -20,8 +20,6 @@ function compatibleResponse(): Record<string, unknown> {
|
||||
workspace_id: "workspace-a",
|
||||
runtime_id: "runtime-a",
|
||||
binding_id: "binding-3",
|
||||
connection_state: "verified",
|
||||
verification: null,
|
||||
checked_at: "2026-09-01T12:00:00Z",
|
||||
status: "compatible",
|
||||
failure_kind: null,
|
||||
@@ -63,16 +61,7 @@ Deno.test("runtime connection response rejects unknown fields and incoherent com
|
||||
assertEquals(
|
||||
parseRuntimeConnectionTestResponse({
|
||||
...compatibleResponse(),
|
||||
verification: {
|
||||
verified_at: "2026-09-01T12:00:00Z",
|
||||
last_checked_at: "2026-09-01T12:00:01Z",
|
||||
last_outcome: "verified",
|
||||
binding_id: "binding-2",
|
||||
workspace_key_id: "WK-a",
|
||||
workspace_public_key_fingerprint: "sha256:workspace",
|
||||
workspace_trust_id: "trust-a",
|
||||
runtime_public_key_fingerprint: "sha256:runtime",
|
||||
},
|
||||
verification: {},
|
||||
}),
|
||||
null,
|
||||
);
|
||||
@@ -120,3 +109,26 @@ Deno.test("runtime connection request rejects a mismatched response identity", a
|
||||
"Connection test response did not match the selected Runtime",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("runtime authentication failure remains an observed result without granting approval", async () => {
|
||||
const failed = {
|
||||
...compatibleResponse(),
|
||||
status: "failed",
|
||||
failure_kind: "authentication",
|
||||
actual_protocol_version: null,
|
||||
};
|
||||
assertEquals(parseRuntimeConnectionTestResponse(failed), failed);
|
||||
const fetched = await testRuntimeConnection(
|
||||
"workspace-a",
|
||||
"runtime-a",
|
||||
(() => Promise.resolve(Response.json(failed))) as typeof fetch,
|
||||
);
|
||||
assertEquals(fetched, failed);
|
||||
assertEquals(
|
||||
parseRuntimeConnectionTestResponse({
|
||||
...failed,
|
||||
connection_state: "verified",
|
||||
}),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ Deno.test("Runtime registration presents the complete multi-Workspace trust sequ
|
||||
"yoi-runtime identity show --json",
|
||||
"3. Register the connection",
|
||||
"Register Runtime",
|
||||
"Run Test to complete authenticated verification.",
|
||||
"Run Test to observe a signed ping.",
|
||||
]
|
||||
) {
|
||||
assert(
|
||||
@@ -129,16 +129,14 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
|
||||
|
||||
const ownerGate = page.indexOf("data.workspace.permissions.manage_runtimes");
|
||||
const reveal = page.indexOf("Reveal public key");
|
||||
const verifiedKey = page.indexOf("{#if verifiedTrust}");
|
||||
const mutation = page.indexOf('id="runtime-public-key-input"');
|
||||
const metadataStart = page.indexOf("async function saveRuntimeMetadata");
|
||||
const trustStart = page.indexOf("async function saveTrustKey");
|
||||
const metadataMutation = page.slice(metadataStart, trustStart);
|
||||
assert(ownerGate >= 0, "Runtime trust controls should use manage_runtimes");
|
||||
assert(
|
||||
verifiedKey >= 0 && verifiedKey < mutation &&
|
||||
page.slice(verifiedKey, mutation).includes("{:else}"),
|
||||
"verified Runtime public key must be read-only while unverified trust keeps key input",
|
||||
!page.includes("verifiedTrust") && !page.includes("workspace_trust_id"),
|
||||
"cached verification and copied enrollment identity must not gate key editing",
|
||||
);
|
||||
assert(
|
||||
metadataMutation.includes("updateRemoteRuntime(") &&
|
||||
@@ -150,7 +148,7 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
|
||||
!metadataMutation.includes("publicKey") &&
|
||||
!metadataMutation.includes("public_bundle") &&
|
||||
!metadataMutation.includes("public_key"),
|
||||
"verified Runtime metadata updates must not carry public key authority",
|
||||
"Runtime metadata updates must not carry public key authority",
|
||||
);
|
||||
assert(
|
||||
page.includes("Current fingerprint"),
|
||||
@@ -195,7 +193,6 @@ Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", as
|
||||
"saveRuntimeMetadata",
|
||||
"Runtime settings",
|
||||
"Edit Runtime",
|
||||
"This verified key is read-only",
|
||||
"publicKey = ''",
|
||||
"requestError = null",
|
||||
"successMessage = null",
|
||||
@@ -246,3 +243,33 @@ Deno.test("Runtime detail uses flat sections instead of nested cards", async ()
|
||||
"Runtime detail hierarchy should use flat section separators",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime labels and recovery controls do not depend on persisted authentication approval", async () => {
|
||||
for (const name of ["+page.svelte", "[runtimeId]/+page.svelte"]) {
|
||||
const page = await Deno.readTextFile(
|
||||
new URL(
|
||||
"../src/routes/w/[workspaceId]/settings/runtimes/" + name,
|
||||
import.meta.url,
|
||||
),
|
||||
);
|
||||
for (
|
||||
const field of [
|
||||
"connection_state",
|
||||
"verification",
|
||||
"workspace_key_id",
|
||||
"workspace_trust_id",
|
||||
"Verification required",
|
||||
]
|
||||
) {
|
||||
assert(!page.includes(field), "Runtime page retained " + field);
|
||||
}
|
||||
assert(
|
||||
page.includes("revoked_at"),
|
||||
"explicit revocation must remain visible",
|
||||
);
|
||||
assert(
|
||||
page.includes("testRuntimeConnection("),
|
||||
"actual test must remain available",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ declare const Deno: {
|
||||
test(name: string, fn: () => void | Promise<void>): void;
|
||||
};
|
||||
|
||||
import type { RepositoryApiError } from "../src/lib/generated/repository-api.ts";
|
||||
import type { RuntimeManagementApiError } from "../src/lib/generated/runtime-api.ts";
|
||||
import {
|
||||
createRemoteRuntime,
|
||||
parseRuntimeRemovalOperationResponse,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
revokeRuntimeTrustKey,
|
||||
RuntimeRemovalAttempt,
|
||||
RuntimeTrustConflictError,
|
||||
RuntimeTrustRequestError,
|
||||
RuntimeTrustRouteFence,
|
||||
updateRemoteRuntime,
|
||||
} from "../src/lib/workspace/api/runtime-management.ts";
|
||||
@@ -44,21 +47,8 @@ function runtime() {
|
||||
endpoint_configured: true,
|
||||
token_ref_configured: false,
|
||||
binding: {
|
||||
state: "verified",
|
||||
connection_state: "verified",
|
||||
binding_id: "binding-3",
|
||||
workspace_key_id: "WK-1",
|
||||
workspace_trust_id: "trust-1",
|
||||
verification: {
|
||||
verified_at: "2026-09-01T13:00:00Z",
|
||||
last_checked_at: "2026-09-01T13:00:00Z",
|
||||
last_outcome: "verified",
|
||||
binding_id: "binding-3",
|
||||
workspace_key_id: "WK-1",
|
||||
workspace_public_key_fingerprint: "sha256:workspace-key",
|
||||
workspace_trust_id: "trust-1",
|
||||
runtime_public_key_fingerprint: "SHA256:current",
|
||||
},
|
||||
revoked_at: null,
|
||||
},
|
||||
},
|
||||
runtime_id: "arcadia",
|
||||
@@ -117,8 +107,8 @@ Deno.test("Runtime list and detail parsers return generated Runtime DTO shapes",
|
||||
);
|
||||
|
||||
assert(
|
||||
list.items[0]?.management.binding?.state === "verified",
|
||||
"binding state was not preserved",
|
||||
list.items[0]?.management.binding?.binding_id === "binding-3",
|
||||
"binding identity was not preserved",
|
||||
);
|
||||
|
||||
const parsed = parseWorkspaceRuntimeDetail(detail());
|
||||
@@ -142,9 +132,7 @@ Deno.test("Runtime list parser accepts the built-in Runtime's internal binding",
|
||||
const binding = embedded.management.binding as Partial<
|
||||
typeof embedded.management.binding
|
||||
>;
|
||||
delete binding.workspace_key_id;
|
||||
delete binding.workspace_trust_id;
|
||||
delete binding.verification;
|
||||
delete binding.revoked_at;
|
||||
|
||||
const list = parseWorkspaceRuntimeList({
|
||||
workspace_id: "workspace-a",
|
||||
@@ -154,23 +142,51 @@ Deno.test("Runtime list parser accepts the built-in Runtime's internal binding",
|
||||
diagnostics: [],
|
||||
});
|
||||
assert(
|
||||
list.items[0]?.management.binding?.connection_state === "verified",
|
||||
list.items[0]?.management.binding?.revoked_at === null,
|
||||
"built-in Runtime binding was not preserved",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime management parser rejects Workspace identity bindings without key metadata", () => {
|
||||
const payload = detail();
|
||||
const binding = payload.runtime.management.binding as Partial<
|
||||
typeof payload.runtime.management.binding
|
||||
Deno.test("Runtime list accepts migrated remote bindings without Workspace identity metadata", () => {
|
||||
const migrated = runtime();
|
||||
const binding = migrated.management.binding as Partial<
|
||||
typeof migrated.management.binding
|
||||
>;
|
||||
delete binding.workspace_key_id;
|
||||
delete binding.workspace_trust_id;
|
||||
binding.state = "configured";
|
||||
assertThrows(
|
||||
() => parseWorkspaceRuntimeDetail(payload),
|
||||
"requires Workspace signing key identity metadata",
|
||||
delete binding.revoked_at;
|
||||
const list = parseWorkspaceRuntimeList({
|
||||
workspace_id: "workspace-a",
|
||||
limit: 200,
|
||||
items: [migrated],
|
||||
source: "fixture",
|
||||
diagnostics: [],
|
||||
});
|
||||
assert(
|
||||
list.items[0]?.management.binding?.binding_id === "binding-3",
|
||||
"migrated binding was rejected",
|
||||
);
|
||||
assert(
|
||||
list.items[0]?.management.binding?.revoked_at === null,
|
||||
"missing revocation should remain unrevoked",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime binding rejects cached approval and copied Workspace identity fields", () => {
|
||||
for (
|
||||
const field of [
|
||||
"state",
|
||||
"connection_state",
|
||||
"verification",
|
||||
"workspace_key_id",
|
||||
"workspace_trust_id",
|
||||
]
|
||||
) {
|
||||
const payload = detail();
|
||||
Object.assign(payload.runtime.management.binding, { [field]: "obsolete" });
|
||||
assertThrows(
|
||||
() => parseWorkspaceRuntimeDetail(payload),
|
||||
"not part of the wire contract",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime validators reject unknown object keys and enum variants", () => {
|
||||
@@ -450,82 +466,225 @@ Deno.test("Runtime public key preview matches the Server fingerprint contract",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime registration rejects empty or oversized enrollment identities before sending", async () => {
|
||||
for (const enrollment of ["", "x".repeat(257)]) {
|
||||
Deno.test("Runtime registration sends only Runtime connection data without Workspace identity copies", async () => {
|
||||
const request = {
|
||||
public_bundle: { identity_id: "arcadia", public_key: "test-key" },
|
||||
endpoint: "https://runtime.example",
|
||||
};
|
||||
let body: unknown;
|
||||
await createRemoteRuntime(
|
||||
"workspace-a",
|
||||
request,
|
||||
((_input: string | URL | Request, init?: RequestInit) => {
|
||||
body = JSON.parse(String(init?.body));
|
||||
return Promise.resolve(Response.json(runtime()));
|
||||
}) as typeof fetch,
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(body) === JSON.stringify(request),
|
||||
"registration copied Workspace identity or approval fields",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Runtime trust replacement recognizes RepositoryApiError conflicts without retrying", async () => {
|
||||
for (const errorKind of ["stale_binding", "fingerprint_in_use"] as const) {
|
||||
for (
|
||||
const diagnostics of [undefined, [], [{
|
||||
code: errorKind,
|
||||
severity: "error" as const,
|
||||
message: "Reload Workspace trust before retrying.",
|
||||
}]]
|
||||
) {
|
||||
const request = {
|
||||
public_bundle: {
|
||||
identity_id: "runtime-a",
|
||||
public_key: "yoi-ed25519-pub:v1:test",
|
||||
},
|
||||
display_name: null,
|
||||
endpoint: "https://runtime.example",
|
||||
expected_binding_id: "observed-binding-id",
|
||||
};
|
||||
const conflict: RepositoryApiError = {
|
||||
error: errorKind,
|
||||
message: "Reload the authoritative Runtime trust.",
|
||||
...(diagnostics === undefined ? {} : { diagnostics }),
|
||||
};
|
||||
let calls = 0;
|
||||
const fetchImpl = ((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
calls++;
|
||||
assert(
|
||||
String(input) === "/api/w/workspace-a/runtimes",
|
||||
"wrong trust route",
|
||||
);
|
||||
assert(
|
||||
init?.body === JSON.stringify(request),
|
||||
"observed binding ID changed",
|
||||
);
|
||||
return Promise.resolve(Response.json(conflict, { status: 409 }));
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await createRemoteRuntime("workspace-a", request, fetchImpl);
|
||||
throw new Error("expected trust replacement to reject");
|
||||
} catch (error) {
|
||||
assert(
|
||||
error instanceof RuntimeTrustConflictError,
|
||||
`binding conflict was hidden: ${String(error)}`,
|
||||
);
|
||||
assert(
|
||||
error.message === conflict.message &&
|
||||
JSON.stringify(error.conflict) === JSON.stringify(conflict),
|
||||
"RepositoryApiError conflict was changed",
|
||||
);
|
||||
}
|
||||
assert(calls === 1, "trust replacement was retried");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime revoke preserves typed conflict binding identities and diagnostics", async () => {
|
||||
const conflict: RuntimeManagementApiError = {
|
||||
error: "stale_binding",
|
||||
message: "Reload the authoritative Runtime trust.",
|
||||
current_binding_id: "current-binding-id",
|
||||
current_fingerprint: "sha256:current-key",
|
||||
diagnostics: [],
|
||||
};
|
||||
try {
|
||||
await revokeRuntimeTrustKey(
|
||||
"workspace-a",
|
||||
"runtime-a",
|
||||
{ expected_binding_id: "observed-binding-id" },
|
||||
"sha256:observed-key",
|
||||
"sha256:observed-key",
|
||||
() => Promise.resolve(Response.json(conflict, { status: 409 })),
|
||||
);
|
||||
throw new Error("expected revoke to reject");
|
||||
} catch (error) {
|
||||
assert(
|
||||
error instanceof RuntimeTrustConflictError,
|
||||
"revoke conflict was hidden",
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(error.conflict) === JSON.stringify(conflict),
|
||||
"typed revoke conflict identity was changed",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime create surfaces other RepositoryApiError 409 messages as request errors", async () => {
|
||||
for (
|
||||
const diagnostics of [undefined, [], [{
|
||||
code: "runtime_binding_replacement_blocked",
|
||||
severity: "error" as const,
|
||||
message: "An active Worker occupies this Runtime.",
|
||||
}]]
|
||||
) {
|
||||
const payload: RepositoryApiError = {
|
||||
error: "conflict",
|
||||
message: "Runtime binding replacement is blocked by an active Worker.",
|
||||
...(diagnostics === undefined ? {} : { diagnostics }),
|
||||
};
|
||||
let calls = 0;
|
||||
try {
|
||||
await createRemoteRuntime("workspace-a", {
|
||||
public_bundle: { identity_id: "runtime-a", public_key: "test-key" },
|
||||
workspace_trust_id: enrollment,
|
||||
endpoint: "https://runtime.example",
|
||||
}, (() => { calls++; throw new Error("unexpected request"); }) as typeof fetch);
|
||||
throw new Error("expected enrollment validation to reject");
|
||||
}, () => {
|
||||
calls++;
|
||||
return Promise.resolve(Response.json(payload, { status: 409 }));
|
||||
});
|
||||
throw new Error("expected occupancy conflict to reject");
|
||||
} catch (error) {
|
||||
assert(error instanceof Error && error.message.includes("workspace_trust_id"), "enrollment validation was bypassed");
|
||||
}
|
||||
assert(calls === 0, "invalid enrollment reached the Server");
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime trust replacement preserves binding conflicts without retrying", async () => {
|
||||
for (const errorKind of ["stale_binding", "fingerprint_in_use"] as const) {
|
||||
const request = {
|
||||
public_bundle: {
|
||||
identity_id: "runtime-a",
|
||||
public_key: "yoi-ed25519-pub:v1:test",
|
||||
},
|
||||
display_name: null,
|
||||
endpoint: "https://runtime.example",
|
||||
workspace_trust_id: "runtime-issued-enrollment-id",
|
||||
expected_binding_id: "observed-binding-id",
|
||||
};
|
||||
const conflict = {
|
||||
error: errorKind,
|
||||
message: "Reload the authoritative Runtime trust.",
|
||||
current_binding_id: "current-binding-id",
|
||||
current_fingerprint: "sha256:current-key",
|
||||
};
|
||||
let calls = 0;
|
||||
const fetchImpl = ((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
calls++;
|
||||
assert(String(input) === "/api/w/workspace-a/runtimes", "wrong trust route");
|
||||
assert(init?.body === JSON.stringify(request), "observed binding ID changed");
|
||||
return Promise.resolve(Response.json(conflict, { status: 409 }));
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
await createRemoteRuntime("workspace-a", request, fetchImpl);
|
||||
throw new Error("expected trust replacement to reject");
|
||||
} catch (error) {
|
||||
assert(error instanceof RuntimeTrustConflictError, "binding conflict was hidden");
|
||||
assert(
|
||||
JSON.stringify(error.conflict) === JSON.stringify(conflict),
|
||||
"authoritative conflict identity was changed",
|
||||
error instanceof RuntimeTrustRequestError,
|
||||
"occupancy error was misclassified",
|
||||
);
|
||||
assert(error.message === payload.message, "occupancy message was hidden");
|
||||
}
|
||||
assert(calls === 1, "trust replacement was retried");
|
||||
assert(calls === 1, "occupancy conflict was retried");
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime trust replacement rejects unknown fields and missing binding identity in conflict responses", async () => {
|
||||
const fetchImpl = (() => Promise.resolve(Response.json({
|
||||
error: "stale_binding",
|
||||
message: "Stale trust",
|
||||
unexpected_field: true,
|
||||
}, { status: 409 }))) as typeof fetch;
|
||||
Deno.test("Runtime create bounds legitimate RepositoryApiError 409 messages", async () => {
|
||||
const message = "Runtime is occupied. ".repeat(30);
|
||||
try {
|
||||
await createRemoteRuntime("workspace-a", {
|
||||
public_bundle: { identity_id: "runtime-a", public_key: "test-key" },
|
||||
endpoint: "https://runtime.example",
|
||||
workspace_trust_id: "runtime-issued-enrollment-id",
|
||||
expected_binding_id: "observed-binding-id",
|
||||
}, fetchImpl);
|
||||
throw new Error("expected malformed conflict to reject");
|
||||
}, () =>
|
||||
Promise.resolve(Response.json(
|
||||
{
|
||||
error: "conflict",
|
||||
message,
|
||||
} satisfies RepositoryApiError,
|
||||
{ status: 409 },
|
||||
)));
|
||||
throw new Error("expected occupancy conflict to reject");
|
||||
} catch (error) {
|
||||
assert(!(error instanceof RuntimeTrustConflictError), "legacy conflict was accepted");
|
||||
assert(
|
||||
error instanceof Error && error.message === "Runtime trust conflict response was invalid",
|
||||
"malformed conflict did not fail closed",
|
||||
error instanceof RuntimeTrustRequestError,
|
||||
"occupancy error was misclassified",
|
||||
);
|
||||
assert(
|
||||
error.message === message.slice(0, 256),
|
||||
"bounded backend message was lost",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("Runtime create rejects malformed RepositoryApiError conflicts without exposing their message", async () => {
|
||||
const validDiagnostic = {
|
||||
code: "conflict",
|
||||
severity: "error",
|
||||
message: "detail",
|
||||
};
|
||||
for (const errorKind of ["stale_binding", "fingerprint_in_use", "conflict"]) {
|
||||
const valid = {
|
||||
error: errorKind,
|
||||
message: "Do not expose malformed payload text",
|
||||
};
|
||||
for (
|
||||
const payload of [
|
||||
{ ...valid, unexpected_field: true },
|
||||
{ ...valid, current_binding_id: "not-in-creation-contract" },
|
||||
{ ...valid, current_fingerprint: "not-in-creation-contract" },
|
||||
{ ...valid, error: 409 },
|
||||
{ ...valid, error: "" },
|
||||
{ ...valid, error: "x".repeat(257) },
|
||||
{ error: errorKind },
|
||||
{ ...valid, message: null },
|
||||
{ ...valid, message: "" },
|
||||
{ ...valid, message: "é".repeat(513) },
|
||||
{ ...valid, diagnostics: null },
|
||||
{ ...valid, diagnostics: {} },
|
||||
{ ...valid, diagnostics: Array(65).fill(validDiagnostic) },
|
||||
{
|
||||
...valid,
|
||||
diagnostics: [{ ...validDiagnostic, severity: "unknown" }],
|
||||
},
|
||||
{ ...valid, diagnostics: [{ ...validDiagnostic, code: 1 }] },
|
||||
{ ...valid, diagnostics: [{ ...validDiagnostic, extra: true }] },
|
||||
{
|
||||
...valid,
|
||||
diagnostics: [{ ...validDiagnostic, message: "x".repeat(2049) }],
|
||||
},
|
||||
]
|
||||
) {
|
||||
try {
|
||||
await createRemoteRuntime("workspace-a", {
|
||||
public_bundle: { identity_id: "runtime-a", public_key: "test-key" },
|
||||
endpoint: "https://runtime.example",
|
||||
expected_binding_id: "observed-binding-id",
|
||||
}, () => Promise.resolve(Response.json(payload, { status: 409 })));
|
||||
throw new Error("expected malformed conflict to reject");
|
||||
} catch (error) {
|
||||
assert(
|
||||
error instanceof RuntimeTrustRequestError &&
|
||||
error.message === "Runtime trust conflict response was invalid",
|
||||
`malformed conflict did not fail closed: ${JSON.stringify(payload)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -551,7 +710,6 @@ Deno.test("Runtime create surfaces bounded Settings error details", async () =>
|
||||
},
|
||||
display_name: null,
|
||||
endpoint: "https://runtime.example",
|
||||
workspace_trust_id: "runtime-issued-enrollment-id",
|
||||
expected_binding_id: null,
|
||||
},
|
||||
fetchImpl,
|
||||
|
||||
Reference in New Issue
Block a user