feat: migrate workspace configuration domains to ServerApi

This commit is contained in:
2026-09-23 04:27:52 +09:00
parent c8766e9fb9
commit 6cbc3a6988
34 changed files with 10679 additions and 454 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts",
"test:component": "deno run -A npm:vitest@3.2.4 run --config vitest.config.ts",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview",
@@ -1,140 +1,565 @@
// This file is generated by `cargo run -p server-api --features typescript --example generate_legacy_typescript | deno fmt -`.
// Do not edit this file directly.
import type { RepositoryObservedStatus, RepositorySource } from "./repository-api";
import type {
RepositoryObservedStatus,
RepositorySource,
} from "./repository-api";
export type WorkspaceSummary = { workspace_id: string, owner_account_id: string, display_name: string, state: string, created_at: string, updated_at: string, };
export type WorkspaceSummary = {
workspace_id: string;
owner_account_id: string;
display_name: string;
state: string;
created_at: string;
updated_at: string;
};
export type WorkspaceCatalogListResponse = Array<WorkspaceSummary>;
export type WorkspaceRepositoryRecord = { workspace_id: string, repository_key: string, kind: string, provider: string | null, source: RepositorySource, default_ref: string | null, source_revision: number, source_fingerprint: string, observed_status: RepositoryObservedStatus, observed_at: string | null, created_at: string, updated_at: string, };
export type WorkspaceRepositoryRecord = {
workspace_id: string;
repository_key: string;
kind: string;
provider: string | null;
source: RepositorySource;
default_ref: string | null;
source_revision: number;
source_fingerprint: string;
observed_status: RepositoryObservedStatus;
observed_at: string | null;
created_at: string;
updated_at: string;
};
export type WorkspaceCreateResponse = { workspace: WorkspaceSummary, repository: WorkspaceRepositoryRecord, config_revision: number, request_fingerprint: string, replayed: boolean, };
export type WorkspaceCreateResponse = {
workspace: WorkspaceSummary;
repository: WorkspaceRepositoryRecord;
config_revision: number;
request_fingerprint: string;
replayed: boolean;
};
export type WorkspaceAuthConfig = { "Passkey": { rp_id: string, origin: string, public_base_url: string, cookie_name: string, } };
export type WorkspaceAuthConfig = {
"Passkey": {
rp_id: string;
origin: string;
public_base_url: string;
cookie_name: string;
};
};
export type WorkspacePermissionSummary = { manage_repositories: boolean, manage_secrets: boolean, manage_runtimes: boolean, delete_workspace: boolean, };
export type WorkspacePermissionSummary = {
manage_repositories: boolean;
manage_secrets: boolean;
manage_runtimes: boolean;
delete_workspace: boolean;
};
export type WorkspaceDeletionState = "queued" | "running" | "blocked" | "failed" | "succeeded";
export type WorkspaceDeletionState =
| "queued"
| "running"
| "blocked"
| "failed"
| "succeeded";
export type WorkspaceDeletionBlockerKind = "last_accessible_workspace" | "revision_conflict" | "dirty_workdir" | "worker_removal_blocked" | "workdir_removal_blocked" | "retention_hold" | "cleanup_unavailable";
export type WorkspaceDeletionBlockerKind =
| "last_accessible_workspace"
| "revision_conflict"
| "dirty_workdir"
| "worker_removal_blocked"
| "workdir_removal_blocked"
| "retention_hold"
| "cleanup_unavailable";
export type WorkspaceDeletionBlocker = { kind: WorkspaceDeletionBlockerKind, resource_kind: string | null, resource_key: string | null, message: string, };
export type WorkspaceDeletionBlocker = {
kind: WorkspaceDeletionBlockerKind;
resource_kind: string | null;
resource_key: string | null;
message: string;
};
export type WorkspaceDeletionResourceCounts = { workers: number, workdirs: number, repositories: number, runtime_bindings: number, secrets: number, artifacts: number, };
export type WorkspaceDeletionResourceCounts = {
workers: number;
workdirs: number;
repositories: number;
runtime_bindings: number;
secrets: number;
artifacts: number;
};
export type WorkspaceDeletionPreflightResponse = { workspace_id: string, display_name: string,
/**
* Opaque persisted Workspace metadata revision used as a CAS fence.
*/
expected_revision: string, can_delete: boolean, resources: WorkspaceDeletionResourceCounts, blockers: Array<WorkspaceDeletionBlocker>, };
export type WorkspaceDeletionPreflightResponse = {
workspace_id: string;
display_name: string;
/**
* Opaque persisted Workspace metadata revision used as a CAS fence.
*/
expected_revision: string;
can_delete: boolean;
resources: WorkspaceDeletionResourceCounts;
blockers: Array<WorkspaceDeletionBlocker>;
};
export type WorkspaceDeletionRequest = { operation_id: string, expected_revision: string, confirmation: string, };
export type WorkspaceDeletionRequest = {
operation_id: string;
expected_revision: string;
confirmation: string;
};
export type WorkspaceDeletionOperationResponse = { operation_id: string, workspace_id: string, display_name: string, state: WorkspaceDeletionState, resources: WorkspaceDeletionResourceCounts, child_operation_ids: Array<string>, blockers: Array<WorkspaceDeletionBlocker>, failure_category: string | null, created_at: string, updated_at: string, completed_at: string | null, };
export type WorkspaceDeletionOperationResponse = {
operation_id: string;
workspace_id: string;
display_name: string;
state: WorkspaceDeletionState;
resources: WorkspaceDeletionResourceCounts;
child_operation_ids: Array<string>;
blockers: Array<WorkspaceDeletionBlocker>;
failure_category: string | null;
created_at: string;
updated_at: string;
completed_at: string | null;
};
export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
export type WorkspaceExtensionPointState = { status: string, note: string, diagnostics: Array<Diagnostic>, };
export type WorkspaceExtensionPointState = {
status: string;
note: string;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceExtensionPoints = { store: string, event_stream: WorkspaceExtensionPointState, host_worker_bridge: WorkspaceExtensionPointState, companion_console: WorkspaceExtensionPointState, };
export type WorkspaceExtensionPoints = {
store: string;
event_stream: WorkspaceExtensionPointState;
host_worker_bridge: WorkspaceExtensionPointState;
companion_console: WorkspaceExtensionPointState;
};
export type WorkspaceResponse = { workspace_id: string, display_name: string, record_authority: string, schema_version: number, auth: WorkspaceAuthConfig, permissions: WorkspacePermissionSummary, extension_points: WorkspaceExtensionPoints, };
export type WorkspaceResponse = {
workspace_id: string;
display_name: string;
record_authority: string;
schema_version: number;
auth: WorkspaceAuthConfig;
permissions: WorkspacePermissionSummary;
extension_points: WorkspaceExtensionPoints;
};
export type WorkspaceMetadataSettingsResponse = { workspace_id: string, display_name: string, created_at: string, revision: string, source: string, diagnostics: Array<Diagnostic>, };
export type WorkspaceMetadataSettingsResponse = {
workspace_id: string;
display_name: string;
created_at: string;
revision: string;
source: string;
diagnostics: Array<Diagnostic>;
};
export type UpdateWorkspaceMetadataRequest = { display_name: string, revision: string, };
export type UpdateWorkspaceMetadataRequest = {
display_name: string;
revision: string;
};
export type WorkspaceMetadataMutationResponse = { workspace: WorkspaceMetadataSettingsResponse, diagnostics: Array<Diagnostic>, };
export type WorkspaceMetadataMutationResponse = {
workspace: WorkspaceMetadataSettingsResponse;
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 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 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 WorkspaceSigningIdentityResponse = {
identity: WorkspaceSigningIdentityPublic;
public_bundle?: WorkspacePublicIdentityBundle;
};
export type ProfileSettingsResponse = { workspace_id: string, registry_revision: string, config_revision?: number | null, tree_digest?: string | null, projection_digest?: string | null, default_profile?: string | null, profiles: Array<WorkspaceProfileSummary>, sources: Array<WorkspaceProfileSourceSummary>, diagnostics: Array<Diagnostic>, };
export type ConfigContentType = "decodal" | "text";
export type WorkspaceProfileSummary = { profile_id: string, selector: string, label: string, source_kind: string, profile_source_id?: string | null, description?: string | null, editable: boolean, is_default: boolean, diagnostics: Array<Diagnostic>, };
export type ConfigEntry = {
path: string;
content_type: ConfigContentType;
content: string;
content_digest: string;
};
export type WorkspaceProfileSourceSummary = { profile_source_id: string, display_path: string, kind: string, content_type: string, content_digest: string, provenance: WorkspaceProfileSourceProvenance, editable: boolean, revision: string, size_bytes: number, diagnostics: Array<Diagnostic>, };
export type ConfigTreeSnapshot = {
revision: number;
digest: string;
entries: { [key in string]: ConfigEntry };
};
export type ConfigTreeChange =
| {
"kind": "create";
path: string;
content_type: ConfigContentType;
content: string;
}
| { "kind": "update"; path: string; expected_digest: string; content: string }
| { "kind": "rename"; from: string; to: string; expected_digest: string }
| { "kind": "delete"; path: string; expected_digest: string };
export type ConfigProjectionValidator = {
"kind": "static_template_catalog";
namespace: string;
key_aliases?: { [key in string]: string };
};
export type ConfigSchemaContribution = {
provider_id: string;
namespace: string;
version: string;
source: string;
projection_validator?: ConfigProjectionValidator;
source_digest: string;
};
export type WorkspaceConfigSchemaBundle = {
contributions: Array<ConfigSchemaContribution>;
source: string;
fingerprint: string;
};
export type ToolchainContract = {
contract_version: number;
decodal_version: string;
schema_version: number;
entrypoints: Array<string>;
import_policy_version: number;
schema_bundle: WorkspaceConfigSchemaBundle;
fingerprint: string;
};
export type WorkspaceConfigTreeResponse = {
snapshot: ConfigTreeSnapshot;
contract: ToolchainContract;
projection_digest: string;
};
export type ConfigCommitRequest = {
base_revision: number;
base_digest: string;
changes: Array<ConfigTreeChange>;
entrypoints: Array<string>;
};
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
config_revision?: number | null;
tree_digest?: string | null;
projection_digest?: string | null;
default_profile?: string | null;
profiles: Array<WorkspaceProfileSummary>;
sources: Array<WorkspaceProfileSourceSummary>;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceProfileSummary = {
profile_id: string;
selector: string;
label: string;
source_kind: string;
profile_source_id?: string | null;
description?: string | null;
editable: boolean;
is_default: boolean;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceProfileSourceSummary = {
profile_source_id: string;
display_path: string;
kind: string;
content_type: string;
content_digest: string;
provenance: WorkspaceProfileSourceProvenance;
editable: boolean;
revision: string;
size_bytes: number;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceProfileSourceProvenance = "project_profile_source_tree";
export type GitCommitSummary = { hash: string, short_hash: string, summary: string, author_name: string, author_email: string, author_date: string, parents: Array<string>, refs: Array<string>, };
export type GitCommitSummary = {
hash: string;
short_hash: string;
summary: string;
author_name: string;
author_email: string;
author_date: string;
parents: Array<string>;
refs: Array<string>;
};
export type RepositorySshConnectionProbeRequest = { runtime_id: string, };
export type RepositorySshConnectionProbeRequest = { runtime_id: string };
export type RepositorySshHostKeyCandidate = { algorithm: string, host_key: string, fingerprint: string, };
export type RepositorySshHostKeyCandidate = {
algorithm: string;
host_key: string;
fingerprint: string;
};
export type RepositorySshConnectionTrustState = "untrusted" | "verified" | "changed";
export type RepositorySshConnectionTrustState =
| "untrusted"
| "verified"
| "changed";
export type RepositorySshConnectionProbeResponse = { workspace_id: string, repository_key: string, runtime_id: string, hostname: string, port: number, trust_state: RepositorySshConnectionTrustState, host_trust_id: string, expected_host_trust_revision: number | null, candidates: Array<RepositorySshHostKeyCandidate>, };
export type RepositorySshConnectionProbeResponse = {
workspace_id: string;
repository_key: string;
runtime_id: string;
hostname: string;
port: number;
trust_state: RepositorySshConnectionTrustState;
host_trust_id: string;
expected_host_trust_revision: number | null;
candidates: Array<RepositorySshHostKeyCandidate>;
};
export type ConfirmRepositorySshHostTrustRequest = { operation_id: string, runtime_id: string, host_key: string, expected_host_trust_revision: number | null, };
export type ConfirmRepositorySshHostTrustRequest = {
operation_id: string;
runtime_id: string;
host_key: string;
expected_host_trust_revision: number | null;
};
export type RepositoryLogResponse = { workspace_id: string, repository_key: string, default_selector?: string | null, limit: number, items: Array<GitCommitSummary>, diagnostics: Array<Diagnostic>, };
export type RepositoryLogResponse = {
workspace_id: string;
repository_key: string;
default_selector?: string | null;
limit: number;
items: Array<GitCommitSummary>;
diagnostics: Array<Diagnostic>;
};
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
export type RuntimeSourceStatus = "active" | "reserved";
export type RuntimeIdentityAuthority = "runtime_registry_projection" | "server_runtime_configuration";
export type RuntimeIdentityAuthority =
| "runtime_registry_projection"
| "server_runtime_configuration";
export type RuntimeSourceSummary = { kind: RuntimeSourceKind, status: RuntimeSourceStatus, identity_authority: RuntimeIdentityAuthority, note: string, };
export type RuntimeSourceSummary = {
kind: RuntimeSourceKind;
status: RuntimeSourceStatus;
identity_authority: RuntimeIdentityAuthority;
note: string;
};
export type RuntimeSummary = { runtime_id: string, label: string, kind: string, status: string, source: RuntimeSourceSummary, host_ids: Array<string>, worker_creation_available: boolean, os: string, arch: string, diagnostics: Array<Diagnostic>, };
export type RuntimeSummary = {
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked";
export type WorkspaceRuntimeBindingState =
| "configured"
| "verified"
| "revoked";
export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavailable" | "revoked";
export type RuntimeConnectionDisplayState =
| "configured"
| "verified"
| "unavailable"
| "revoked";
export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed";
export type RuntimeVerificationOutcome =
| "verified"
| "challenge_issued"
| "verification_failed"
| "connectivity_failed";
export type RuntimeVerificationEvidenceSummary = { verified_at: string | null, last_checked_at: string, last_outcome: RuntimeVerificationOutcome, binding_revision: number, workspace_key_id: string, workspace_identity_revision: number, workspace_trust_generation: number, runtime_public_key_fingerprint: string, runtime_identity_revision: number, };
export type RuntimeVerificationEvidenceSummary = {
verified_at: string | null;
last_checked_at: string;
last_outcome: RuntimeVerificationOutcome;
binding_revision: number;
workspace_key_id: string;
workspace_identity_revision: number;
workspace_trust_generation: number;
runtime_public_key_fingerprint: string;
runtime_identity_revision: number;
};
export type WorkspaceRuntimeBindingSummary = { state: WorkspaceRuntimeBindingState, connection_state: RuntimeConnectionDisplayState, revision: number, workspace_key_id?: string | null, workspace_key_generation?: number | null, verification?: RuntimeVerificationEvidenceSummary | null, };
export type WorkspaceRuntimeBindingSummary = {
state: WorkspaceRuntimeBindingState;
connection_state: RuntimeConnectionDisplayState;
revision: number;
workspace_key_id?: string | null;
workspace_key_generation?: number | null;
verification?: RuntimeVerificationEvidenceSummary | null;
};
export type RuntimeManagementSummary = { built_in: boolean, config_managed: boolean, removable: boolean, endpoint_configured: boolean, token_ref_configured: boolean, binding?: WorkspaceRuntimeBindingSummary | null, };
export type RuntimeManagementSummary = {
built_in: boolean;
config_managed: boolean;
removable: boolean;
endpoint_configured: boolean;
token_ref_configured: boolean;
binding?: WorkspaceRuntimeBindingSummary | null;
};
export type WorkspaceRuntimeResource = { management: RuntimeManagementSummary, runtime_id: string, label: string, kind: string, status: string, source: RuntimeSourceSummary, host_ids: Array<string>, worker_creation_available: boolean, os: string, arch: string, diagnostics: Array<Diagnostic>, };
export type WorkspaceRuntimeResource = {
management: RuntimeManagementSummary;
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
export type RuntimeTrustKeyState = { status: RuntimeTrustKeyStatus, fingerprint?: string | null, revision?: number | null, created_at?: string | null, updated_at?: string | null, revoked_at?: string | null, };
export type RuntimeTrustKeyState = {
status: RuntimeTrustKeyStatus;
fingerprint?: string | null;
revision?: number | null;
created_at?: string | null;
updated_at?: string | null;
revoked_at?: string | null;
};
export type RuntimeTrustAuditAction = "created" | "replaced" | "reactivated" | "revoked";
export type RuntimeTrustAuditAction =
| "created"
| "replaced"
| "reactivated"
| "revoked";
export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction, actor_account_id: string, old_fingerprint?: string | null, new_fingerprint?: string | null, revision: number, at: string, };
export type RuntimeTrustAuditEntry = {
action: RuntimeTrustAuditAction;
actor_account_id: string;
old_fingerprint?: string | null;
new_fingerprint?: string | null;
revision: number;
at: string;
};
export type WorkspaceRuntimeDetail = { workspace_id: string, runtime: WorkspaceRuntimeResource, endpoint?: string | null, trust_key: RuntimeTrustKeyState, recent_audit: Array<RuntimeTrustAuditEntry>, };
export type WorkspaceRuntimeDetail = {
workspace_id: string;
runtime: WorkspaceRuntimeResource;
endpoint?: string | null;
trust_key: RuntimeTrustKeyState;
recent_audit: Array<RuntimeTrustAuditEntry>;
};
export type RuntimeTrustKeyRevealResponse = { public_key: string, };
export type RuntimeTrustKeyRevealResponse = { public_key: string };
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number, };
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number };
export type RemoveRuntimeRequest = { operation_id: string, expected_binding_revision: number, };
export type RemoveRuntimeRequest = {
operation_id: string;
expected_binding_revision: number;
};
export type RuntimeRemovalOperationState = "pending" | "cleanup_pending" | "succeeded" | "failed";
export type RuntimeRemovalOperationState =
| "pending"
| "cleanup_pending"
| "succeeded"
| "failed";
export type RuntimeRemovalOperationResponse = { operation_id: string, workspace_id: string, runtime_id: string, state: RuntimeRemovalOperationState, binding_removed: boolean, runtime_registration_removed: boolean | null, failure_category?: string | null, created_at: string, updated_at: string, completed_at?: string | null, };
export type RuntimeRemovalOperationResponse = {
operation_id: string;
workspace_id: string;
runtime_id: string;
state: RuntimeRemovalOperationState;
binding_removed: boolean;
runtime_registration_removed: boolean | null;
failure_category?: string | null;
created_at: string;
updated_at: string;
completed_at?: string | null;
};
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
export type RuntimeTrustConflictResponse = { error: RuntimeTrustConflictKind, message: string, current_revision?: number, current_fingerprint?: string | null, };
export type RuntimeTrustConflictResponse = {
error: RuntimeTrustConflictKind;
message: string;
current_revision?: number;
current_fingerprint?: string | null;
};
export type RuntimePublicIdentityBundle = { identity_id: string, public_key: string, };
export type RuntimePublicIdentityBundle = {
identity_id: string;
public_key: string;
};
export type CreateRemoteRuntimeRequest = { public_bundle: RuntimePublicIdentityBundle, display_name?: string | null, endpoint: string, expected_revision?: number | null, };
export type CreateRemoteRuntimeRequest = {
public_bundle: RuntimePublicIdentityBundle;
display_name?: string | null;
endpoint: string;
expected_revision?: number | null;
};
export type UpdateRemoteRuntimeRequest = { display_name?: string | null, endpoint: string, };
export type UpdateRemoteRuntimeRequest = {
display_name?: string | null;
endpoint: string;
};
export type RuntimeConnectionTestStatus = "compatible" | "failed";
export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration";
export type RuntimeConnectionTestFailureKind =
| "authentication"
| "authorization"
| "network_unreachable"
| "timeout"
| "tls_or_transport"
| "malformed_response"
| "protocol_version_mismatch"
| "runtime_identity_mismatch"
| "configuration";
export type RuntimeConnectionTestResponse = { workspace_id: string, runtime_id: string, binding_revision: number, connection_state: RuntimeConnectionDisplayState, verification: RuntimeVerificationEvidenceSummary | null, checked_at: string, status: RuntimeConnectionTestStatus, failure_kind: RuntimeConnectionTestFailureKind | null, expected_protocol_version: number, actual_protocol_version: number | null, diagnostics: Array<Diagnostic>, };
export type RuntimeConnectionTestResponse = {
workspace_id: string;
runtime_id: string;
binding_revision: number;
connection_state: RuntimeConnectionDisplayState;
verification: RuntimeVerificationEvidenceSummary | null;
checked_at: string;
status: RuntimeConnectionTestStatus;
failure_kind: RuntimeConnectionTestFailureKind | null;
expected_protocol_version: number;
actual_protocol_version: number | null;
diagnostics: Array<Diagnostic>;
};
+93 -11
View File
@@ -1,26 +1,108 @@
// Generated from server-api. Do not edit by hand.
// Regenerate: cargo run -q -p server-api --features typescript --example generate_memory_api_types > web/workspace/src/lib/generated/memory-api.ts
export const MEMORY_API_LIMITS = {
maxResponseBytes: 8388608,
maxDocumentBytes: 4194304,
maxCollectionItems: 500,
maxStringBytes: 1048576,
maxIdentifierBytes: 512,
} as const;
export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
export type MemoryDocumentResponse = { body_md: string, created_at: string, updated_at: string, bytes: number, record_source: string, };
export type MemoryDocumentResponse = {
body_md: string;
created_at: string;
updated_at: string;
bytes: number;
record_source: string;
};
export type MemoryCandidateKind = "preference" | "working_assumption" | "constraint" | "decision" | "open_question" | "lesson";
export type MemoryCandidateKind =
| "preference"
| "working_assumption"
| "constraint"
| "decision"
| "open_question"
| "lesson";
export type MemoryEvidenceOriginKind = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown";
export type MemoryEvidenceOriginKind =
| "human_input"
| "worker_input"
| "flow_instruction"
| "backend_instruction"
| "model_output"
| "tool_output"
| "derived_summary"
| "legacy_unknown";
export type MemoryEvidenceOrigin = { kind: MemoryEvidenceOriginKind, account_id?: string | null, workspace_id?: string | null, runtime_id?: string | null, worker_id?: string | null, flow_selector?: string | null, flow_definition_id?: string | null, flow_definition_revision?: number | null, };
export type MemoryEvidenceOrigin = {
kind: MemoryEvidenceOriginKind;
account_id?: string | null;
workspace_id?: string | null;
runtime_id?: string | null;
worker_id?: string | null;
flow_selector?: string | null;
flow_definition_id?: string | null;
flow_definition_revision?: number | null;
};
export type MemorySourceRef = { segment_id: string, range: [number, number], };
export type MemorySourceRef = { segment_id: string; range: [number, number] };
export type MemoryStagingEvidence = { id: string, kind: string, entry_range: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt: string | null, summary: string | null, };
export type MemoryStagingEvidence = {
id: string;
kind: string;
entry_range: [number, number] | null;
origin?: MemoryEvidenceOrigin | null;
excerpt: string | null;
summary: string | null;
};
export type MemorySourceEvidenceRef = { session_id: string | null, segment_id: string | null, entry_range: [number, number] | null, evidence_id: string | null, origin?: MemoryEvidenceOrigin | null, evidence_kind: string | null, label: string | null, summary: string | null, };
export type MemorySourceEvidenceRef = {
session_id: string | null;
segment_id: string | null;
entry_range: [number, number] | null;
evidence_id: string | null;
origin?: MemoryEvidenceOrigin | null;
evidence_kind: string | null;
label: string | null;
summary: string | null;
};
export type MemoryStagingRecord = { schema_version: number, id: string, extract_run_id: string, source: MemorySourceRef, kind: MemoryCandidateKind, claim: string, why_useful: string, staleness: string | null, evidence: Array<MemoryStagingEvidence>, source_refs: Array<MemorySourceEvidenceRef>, };
export type MemoryStagingRecord = {
schema_version: number;
id: string;
extract_run_id: string;
source: MemorySourceRef;
kind: MemoryCandidateKind;
claim: string;
why_useful: string;
staleness: string | null;
evidence: Array<MemoryStagingEvidence>;
source_refs: Array<MemorySourceEvidenceRef>;
};
export type MemoryStagingEntry = { id: string, byte_len: number, record: MemoryStagingRecord, };
export type MemoryStagingEntry = {
id: string;
byte_len: number;
record: MemoryStagingRecord;
};
export type MemoryStagingListResponse = { limit: number, returned_count: number, total_valid_count: number, invalid_count: number, truncated: boolean, order: string, record_authority: string, items: Array<MemoryStagingEntry>, diagnostics: Array<Diagnostic>, };
export type MemoryStagingListResponse = {
limit: number;
returned_count: number;
total_valid_count: number;
invalid_count: number;
truncated: boolean;
order: string;
record_authority: string;
items: Array<MemoryStagingEntry>;
diagnostics: Array<Diagnostic>;
};
@@ -1,26 +1,93 @@
// Generated from server-api. Do not edit by hand.
// Regenerate: cargo run -q -p server-api --features typescript --example generate_repository_access_types > web/workspace/src/lib/generated/repository-access-api.ts
export type RepositorySshCredential = { credential_id: string, workspace_id: string, name: string, public_key_algorithm: string, public_key_fingerprint: string, current_revision: number, status: string, created_at: string, rotated_at: string | null, referenced_repositories: Array<string>, };
export type RepositorySshCredential = {
credential_id: string;
workspace_id: string;
name: string;
public_key_algorithm: string;
public_key_fingerprint: string;
current_revision: number;
status: string;
created_at: string;
rotated_at: string | null;
referenced_repositories: Array<string>;
};
export type CreateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, private_key: string, passphrase: string | null, };
export type CreateRepositorySshCredentialRequest = {
operation_id: string;
credential_id: string;
name: string;
private_key: string;
passphrase: string | null;
};
export type GenerateRepositorySshCredentialRequest = { operation_id: string, credential_id: string, name: string, };
export type GenerateRepositorySshCredentialRequest = {
operation_id: string;
credential_id: string;
name: string;
};
export type RepositorySshPublicKey = { credential_id: string, current_revision: number, public_key_algorithm: string, public_key_fingerprint: string, public_key: string, };
export type RepositorySshPublicKey = {
credential_id: string;
current_revision: number;
public_key_algorithm: string;
public_key_fingerprint: string;
public_key: string;
};
export type RotateRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, private_key: string, passphrase: string | null, };
export type RotateRepositorySshCredentialRequest = {
operation_id: string;
expected_revision: number;
private_key: string;
passphrase: string | null;
};
export type DeleteRepositorySshCredentialRequest = { operation_id: string, expected_revision: number, };
export type DeleteRepositorySshCredentialRequest = {
operation_id: string;
expected_revision: number;
};
export type RepositorySshHostTrust = { host_trust_id: string, workspace_id: string, hostname: string, port: number, key_algorithm: string, host_key: string, fingerprint: string, current_revision: number, created_at: string, updated_at: string, referenced_repositories: Array<string>, };
export type RepositorySshHostTrust = {
host_trust_id: string;
workspace_id: string;
hostname: string;
port: number;
key_algorithm: string;
host_key: string;
fingerprint: string;
current_revision: number;
created_at: string;
updated_at: string;
referenced_repositories: Array<string>;
};
export type PutRepositorySshHostTrustRequest = { operation_id: string, host_trust_id: string, hostname: string, port: number, host_key: string, expected_revision: number | null, };
export type PutRepositorySshHostTrustRequest = {
operation_id: string;
host_trust_id: string;
hostname: string;
port: number;
host_key: string;
expected_revision: number | null;
};
export type DeleteRepositorySshHostTrustRequest = { operation_id: string, expected_revision: number, };
export type DeleteRepositorySshHostTrustRequest = {
operation_id: string;
expected_revision: number;
};
export type RepositoryAccessMode = "read_only" | "read_write";
export type RepositorySshAccessBinding = { repository_key: string, credential_id: string, host_trust_id: string, access: RepositoryAccessMode, };
export type RepositorySshAccessBinding = {
repository_key: string;
credential_id: string;
host_trust_id: string;
access: RepositoryAccessMode;
};
export type RepositoryAccessProjection = { workspace_id: string, config_revision: number, projection_digest: string, bindings: Array<RepositorySshAccessBinding>, };
export type RepositoryAccessProjection = {
workspace_id: string;
config_revision: number;
projection_digest: string;
bindings: Array<RepositorySshAccessBinding>;
};
@@ -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:b478a5e12b43d9ac1261ba31358a5b80bb31e398bd57eccbd797f5e92c6f48b7)
// Input contract: 0.1.0 (sha256:386c56c830b49766e9f523f08c5bd4e24de490ba18c99c3d04063021d6b33352)
// 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
+61 -7
View File
@@ -20,22 +20,76 @@ export const SKILL_API_LIMITS = {
export type SkillDiagnosticSeverity = "error" | "warning";
export type SkillDiagnostic = { severity: SkillDiagnosticSeverity, code: string, message: string, source?: string, };
export type SkillDiagnostic = {
severity: SkillDiagnosticSeverity;
code: string;
message: string;
source?: string;
};
export type SkillSourceKind = "builtin" | "workspace";
export type SkillProvenance = { kind: SkillSourceKind, id: string, virtual_path?: string, revision?: number, source_digest?: string, tree_digest?: string, };
export type SkillProvenance = {
kind: SkillSourceKind;
id: string;
virtual_path?: string;
revision?: number;
source_digest?: string;
tree_digest?: string;
};
export type SkillActivationStatus = "active" | "inactive";
export type SkillProjectionStatus = "valid" | "invalid";
export type SkillProjectionIdentity = { config_revision: number, tree_digest: string, };
export type SkillProjectionIdentity = {
config_revision: number;
tree_digest: string;
};
export type SkillResourceRef = { kind: string, name: string, supported: boolean, diagnostic?: string, };
export type SkillResourceRef = {
kind: string;
name: string;
supported: boolean;
diagnostic?: string;
};
export type SkillCatalogEntry = { name: string, description: string, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, provenance: SkillProvenance, overrides: Array<SkillProvenance>, diagnostics: Array<SkillDiagnostic>, };
export type SkillCatalogEntry = {
name: string;
description: string;
activation_status: SkillActivationStatus;
projection_status: SkillProjectionStatus;
provenance: SkillProvenance;
overrides: Array<SkillProvenance>;
diagnostics: Array<SkillDiagnostic>;
};
export type SkillCatalogResponse = { authority: string, projection: SkillProjectionIdentity, entries: Array<SkillCatalogEntry>, diagnostics: Array<SkillDiagnostic>, };
export type SkillCatalogResponse = {
authority: string;
projection: SkillProjectionIdentity;
entries: Array<SkillCatalogEntry>;
diagnostics: Array<SkillDiagnostic>;
};
export type SkillDetailResponse = { authority: string, projection: SkillProjectionIdentity, name: string, description: string, provenance: SkillProvenance, overrides: Array<SkillProvenance>, diagnostics: Array<SkillDiagnostic>, activation_status: SkillActivationStatus, projection_status: SkillProjectionStatus, body: string, allowed_tools: Array<string>, allowed_tools_status: string, resources: Array<SkillResourceRef>, };
export type SkillDetailResponse = {
authority: string;
projection: SkillProjectionIdentity;
name: string;
description: string;
provenance: SkillProvenance;
overrides: Array<SkillProvenance>;
diagnostics: Array<SkillDiagnostic>;
activation_status: SkillActivationStatus;
projection_status: SkillProjectionStatus;
body: string;
allowed_tools: Array<string>;
allowed_tools_status: string;
resources: Array<SkillResourceRef>;
};
export type SkillActivationResponse = {
name: string;
provenance: SkillProvenance;
diagnostics: Array<SkillDiagnostic>;
body: string;
};
@@ -1,5 +1,9 @@
import { error } from "@sveltejs/kit";
import { RepositoryAccessSchemaError } from "./repository-access.ts";
import { readBoundedJson } from "./http.ts";
import {
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
RepositoryAccessSchemaError,
} from "./repository-access.ts";
export async function loadRepositoryAccessJson<T>(
fetcher: typeof fetch,
@@ -27,7 +31,10 @@ export async function loadRepositoryAccessJson<T>(
let payload: unknown;
try {
payload = await response.json();
payload = await readBoundedJson(
response,
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
);
} catch {
error(502, {
message: "Repository Access returned an invalid JSON response.",
@@ -5,6 +5,10 @@ import type {
RepositorySshPublicKey,
} from "../../generated/repository-access-api.ts";
export const REPOSITORY_ACCESS_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
const MAX_COLLECTION_ITEMS = 1000;
const MAX_STRING_BYTES = 1024 * 1024;
export class RepositoryAccessSchemaError extends Error {
constructor(path: string, expected: string) {
super(
@@ -165,7 +169,7 @@ function readRecord(
);
if (unknownKey !== undefined) {
throw new RepositoryAccessSchemaError(
`${path}.${unknownKey}`,
`${path}.${unknownKey.slice(0, 128)}`,
"no unknown field",
);
}
@@ -173,7 +177,7 @@ function readRecord(
}
function readArray(value: unknown, path: string): unknown[] {
if (!Array.isArray(value)) {
if (!Array.isArray(value) || value.length > MAX_COLLECTION_ITEMS) {
throw new RepositoryAccessSchemaError(path, "an array");
}
return value;
@@ -185,8 +189,11 @@ function readString(
path: string,
): string {
const value = record[key];
if (typeof value !== "string") {
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a string");
if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES
) {
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a bounded string");
}
return value;
}
@@ -197,8 +204,15 @@ function readNullableString(
path: string,
): string | null {
const value = record[key];
if (value !== null && typeof value !== "string") {
throw new RepositoryAccessSchemaError(`${path}.${key}`, "a string or null");
if (
value !== null &&
(typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES)
) {
throw new RepositoryAccessSchemaError(
`${path}.${key}`,
"a bounded string or null",
);
}
return value;
}
@@ -210,10 +224,13 @@ function readStringArray(
): string[] {
const values = readArray(record[key], `${path}.${key}`);
values.forEach((value, index) => {
if (typeof value !== "string") {
if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > MAX_STRING_BYTES
) {
throw new RepositoryAccessSchemaError(
`${path}.${key}[${index}]`,
"a string",
"a bounded string",
);
}
});
@@ -395,6 +395,27 @@ function workspaceRepositoryRecord(
};
}
function workspaceString(value: unknown, path: string): string {
const parsed = string(value, path);
if (parsed.length > REPOSITORY_API_LIMITS.maxStringCodeUnits) {
throw new Error(`${path} exceeds the Workspace API string limit`);
}
return parsed;
}
function workspaceDiagnostics(value: unknown, path: string): Diagnostic[] {
const entries = array(value, path);
if (entries.length > 100) {
throw new Error(`${path} exceeds the Workspace API collection limit`);
}
return entries.map((entry, index) => {
const parsed = diagnostic(entry, `${path}[${index}]`);
workspaceString(parsed.code, `${path}[${index}].code`);
workspaceString(parsed.message, `${path}[${index}].message`);
return parsed;
});
}
function extensionPoint(
value: unknown,
path: string,
@@ -402,12 +423,9 @@ function extensionPoint(
const item = object(value, path);
exactKeys(item, ["status", "note", "diagnostics"], path);
return {
status: string(item.status, `${path}.status`),
note: string(item.note, `${path}.note`),
diagnostics: array(item.diagnostics, `${path}.diagnostics`).map((
entry,
index,
) => diagnostic(entry, `${path}.diagnostics[${index}]`)),
status: workspaceString(item.status, `${path}.status`),
note: workspaceString(item.note, `${path}.note`),
diagnostics: workspaceDiagnostics(item.diagnostics, `${path}.diagnostics`),
};
}
@@ -423,7 +441,7 @@ function extensionPoints(
"companion_console",
], path);
return {
store: string(item.store, `${path}.store`),
store: workspaceString(item.store, `${path}.store`),
event_stream: extensionPoint(item.event_stream, `${path}.event_stream`),
host_worker_bridge: extensionPoint(
item.host_worker_bridge,
@@ -447,13 +465,16 @@ function authConfig(value: unknown, path: string): WorkspaceAuthConfig {
);
return {
Passkey: {
rp_id: string(passkey.rp_id, `${path}.Passkey.rp_id`),
origin: string(passkey.origin, `${path}.Passkey.origin`),
public_base_url: string(
rp_id: workspaceString(passkey.rp_id, `${path}.Passkey.rp_id`),
origin: workspaceString(passkey.origin, `${path}.Passkey.origin`),
public_base_url: workspaceString(
passkey.public_base_url,
`${path}.Passkey.public_base_url`,
),
cookie_name: string(passkey.cookie_name, `${path}.Passkey.cookie_name`),
cookie_name: workspaceString(
passkey.cookie_name,
`${path}.Passkey.cookie_name`,
),
},
};
}
@@ -575,23 +596,27 @@ export function parseWorkspaceResponse(value: unknown): WorkspaceResponse {
],
"workspace response",
);
const schemaVersion = integer(
response.schema_version,
"workspace response.schema_version",
);
if (schemaVersion < 0) {
throw new Error("workspace response.schema_version must be non-negative");
}
return {
workspace_id: string(
workspace_id: workspaceString(
response.workspace_id,
"workspace response.workspace_id",
),
display_name: string(
display_name: workspaceString(
response.display_name,
"workspace response.display_name",
),
record_authority: string(
record_authority: workspaceString(
response.record_authority,
"workspace response.record_authority",
),
schema_version: integer(
response.schema_version,
"workspace response.schema_version",
),
schema_version: schemaVersion,
auth: authConfig(response.auth, "workspace response.auth"),
permissions: permissions(
response.permissions,
@@ -1,20 +1,267 @@
import type {
ConfigCommitRequest,
ConfigContentType,
ConfigEntry,
ConfigProjectionValidator,
ConfigSchemaContribution,
ConfigTreeSnapshot,
ToolchainContract,
WorkspaceConfigSchemaBundle,
WorkspaceConfigTreeResponse,
} from "./types.ts";
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
const MAX_ERROR_BYTES = 4 * 1024;
const MAX_ENTRY_COUNT = 256;
const MAX_ENTRY_BYTES = 256 * 1024;
const MAX_PATH_BYTES = 512;
const MAX_STRING_BYTES = 4 * 1024;
const MAX_DIGEST_BYTES = 256;
const MAX_COLLECTION_ITEMS = 256;
export class ConfigSourceApiError extends Error {
constructor(
message: string,
readonly status: number,
) {
super(message);
this.name = "ConfigSourceApiError";
}
}
type JsonRecord = Record<string, unknown>;
type Parser<T> = (value: unknown) => T;
function sourceTreeUrl(workspaceId: string): string {
return `/api/w/${encodeURIComponent(workspaceId)}/config/source-tree`;
}
async function readJson<T>(response: Response): Promise<T> {
if (!response.ok) {
const body = await response.text();
throw new Error(body || `${response.status} ${response.statusText}`);
async function readJson<T>(
response: Response,
parser: Parser<T>,
): Promise<T> {
const limit = response.ok ? MAX_RESPONSE_BYTES : MAX_ERROR_BYTES;
const declared = response.headers.get("content-length");
if (declared !== null) {
const bytes = Number(declared);
if (!Number.isSafeInteger(bytes) || bytes < 0 || bytes > limit) {
throw new ConfigSourceApiError(
response.ok
? "Workspace configuration response is too large."
: "Workspace configuration request failed.",
response.ok ? 502 : response.status,
);
}
}
return await response.json() as T;
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > limit) {
throw new ConfigSourceApiError(
response.ok
? "Workspace configuration response is too large."
: "Workspace configuration request failed.",
response.ok ? 502 : response.status,
);
}
const text = new TextDecoder().decode(bytes);
if (!response.ok) {
throw new ConfigSourceApiError(
text || `${response.status} ${response.statusText}`,
response.status,
);
}
let value: unknown;
try {
value = JSON.parse(text) as unknown;
} catch {
throw schemaError();
}
return parser(value);
}
function schemaError(): ConfigSourceApiError {
return new ConfigSourceApiError(
"Workspace configuration returned an invalid response.",
502,
);
}
function objectValue(value: unknown): JsonRecord {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw schemaError();
}
return value as JsonRecord;
}
function record(
value: unknown,
required: readonly string[],
optional: readonly string[] = [],
): JsonRecord {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw schemaError();
}
const result = value as JsonRecord;
const allowed = new Set([...required, ...optional]);
if (
required.some((key) => !(key in result)) ||
Object.keys(result).some((key) => !allowed.has(key))
) {
throw schemaError();
}
return result;
}
function boundedString(value: unknown, maxBytes = MAX_STRING_BYTES): string {
if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > maxBytes
) {
throw schemaError();
}
return value;
}
function safeInteger(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw schemaError();
}
return value as number;
}
function boundedArray(value: unknown): unknown[] {
if (!Array.isArray(value) || value.length > MAX_COLLECTION_ITEMS) {
throw schemaError();
}
return value;
}
function parseContentType(value: unknown): ConfigContentType {
if (value !== "decodal" && value !== "text") throw schemaError();
return value;
}
export function parseConfigEntry(value: unknown): ConfigEntry {
const item = record(value, [
"path",
"content_type",
"content",
"content_digest",
]);
return {
path: boundedString(item.path, MAX_PATH_BYTES),
content_type: parseContentType(item.content_type),
content: boundedString(item.content, MAX_ENTRY_BYTES),
content_digest: boundedString(item.content_digest, MAX_DIGEST_BYTES),
};
}
function parseStringMap(value: unknown): Record<string, string> {
const item = objectValue(value);
const entries = Object.entries(item);
if (entries.length > MAX_COLLECTION_ITEMS) throw schemaError();
const result: Record<string, string> = {};
for (const [key, entry] of entries) {
const boundedKey = boundedString(key, MAX_PATH_BYTES);
result[boundedKey] = boundedString(entry, MAX_PATH_BYTES);
}
return result;
}
function parseProjectionValidator(value: unknown): ConfigProjectionValidator {
const item = record(value, ["kind", "namespace"], ["key_aliases"]);
if (item.kind !== "static_template_catalog") throw schemaError();
return {
kind: "static_template_catalog",
namespace: boundedString(item.namespace, MAX_PATH_BYTES),
key_aliases: item.key_aliases === undefined
? {}
: parseStringMap(item.key_aliases),
};
}
function parseContribution(value: unknown): ConfigSchemaContribution {
const item = record(
value,
["provider_id", "namespace", "version", "source", "source_digest"],
["projection_validator"],
);
const projection = item.projection_validator === undefined
? undefined
: parseProjectionValidator(item.projection_validator);
return {
provider_id: boundedString(item.provider_id),
namespace: boundedString(item.namespace),
version: boundedString(item.version),
source: boundedString(item.source),
...(projection === undefined ? {} : { projection_validator: projection }),
source_digest: boundedString(item.source_digest, MAX_DIGEST_BYTES),
};
}
function parseSchemaBundle(value: unknown): WorkspaceConfigSchemaBundle {
const item = record(value, ["contributions", "source", "fingerprint"]);
return {
contributions: boundedArray(item.contributions).map(parseContribution),
source: boundedString(item.source),
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
};
}
function parseToolchainContract(value: unknown): ToolchainContract {
const item = record(value, [
"contract_version",
"decodal_version",
"schema_version",
"entrypoints",
"import_policy_version",
"schema_bundle",
"fingerprint",
]);
return {
contract_version: safeInteger(item.contract_version),
decodal_version: boundedString(item.decodal_version),
schema_version: safeInteger(item.schema_version),
entrypoints: boundedArray(item.entrypoints).map((entry) =>
boundedString(entry, MAX_PATH_BYTES)
),
import_policy_version: safeInteger(item.import_policy_version),
schema_bundle: parseSchemaBundle(item.schema_bundle),
fingerprint: boundedString(item.fingerprint, MAX_DIGEST_BYTES),
};
}
export function parseConfigTreeSnapshot(value: unknown): ConfigTreeSnapshot {
const item = record(value, ["revision", "digest", "entries"]);
const entriesRecord = objectValue(item.entries);
const entries = Object.entries(entriesRecord);
if (entries.length > MAX_ENTRY_COUNT) throw schemaError();
const parsedEntries: Record<string, ConfigEntry> = {};
for (const [path, entry] of entries) {
const boundedPath = boundedString(path, MAX_PATH_BYTES);
const parsed = parseConfigEntry(entry);
if (parsed.path !== boundedPath) throw schemaError();
parsedEntries[boundedPath] = parsed;
}
return {
revision: safeInteger(item.revision),
digest: boundedString(item.digest, MAX_DIGEST_BYTES),
entries: parsedEntries,
};
}
export function parseWorkspaceConfigTreeResponse(
value: unknown,
): WorkspaceConfigTreeResponse {
const item = record(value, ["snapshot", "contract", "projection_digest"]);
return {
snapshot: parseConfigTreeSnapshot(item.snapshot),
contract: parseToolchainContract(item.contract),
projection_digest: boundedString(
item.projection_digest,
MAX_DIGEST_BYTES,
),
};
}
export async function fetchConfigTree(
@@ -25,6 +272,7 @@ export async function fetchConfigTree(
await fetcher(sourceTreeUrl(workspaceId), {
headers: { accept: "application/json" },
}),
parseWorkspaceConfigTreeResponse,
);
}
@@ -38,6 +286,7 @@ export async function fetchConfigEntry(
`${sourceTreeUrl(workspaceId)}/entries/${encodeURIComponent(path)}`,
{ headers: { accept: "application/json" } },
),
parseConfigEntry,
);
}
@@ -50,6 +299,7 @@ export async function fetchConfigRevision(
await fetcher(`${sourceTreeUrl(workspaceId)}/revisions/${revision}`, {
headers: { accept: "application/json" },
}),
parseConfigTreeSnapshot,
);
}
@@ -64,5 +314,6 @@ export async function commitConfigTree(
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
}),
parseWorkspaceConfigTreeResponse,
);
}
@@ -1,16 +1,21 @@
// Core and HTTP DTOs are generated from Rust with ts-rs.
export type { ConfigContentType } from "./generated/types/ConfigContentType.ts";
// Workspace REST DTOs are generated from the server-api Rust authority.
export type {
ConfigCommitRequest,
ConfigContentType,
ConfigEntry,
ConfigProjectionValidator,
ConfigSchemaContribution,
ConfigTreeChange,
ConfigTreeSnapshot,
ToolchainContract,
WorkspaceConfigSchemaBundle,
WorkspaceConfigTreeResponse,
} from "$lib/generated/legacy-server-api.ts";
// Browser-local Decodal/WASM projections remain owned by config-source.
export type { ConfigDiagnostic } from "./generated/types/ConfigDiagnostic.ts";
export type { ConfigDiagnosticLabel } from "./generated/types/ConfigDiagnosticLabel.ts";
export type { ConfigEntry } from "./generated/types/ConfigEntry.ts";
export type { ConfigSpan } from "./generated/types/ConfigSpan.ts";
export type { ConfigTreeChange } from "./generated/types/ConfigTreeChange.ts";
export type { ConfigSchemaContribution } from "./generated/types/ConfigSchemaContribution.ts";
export type { WorkspaceConfigSchemaBundle } from "./generated/types/WorkspaceConfigSchemaBundle.ts";
export type { ConfigTreeSnapshot } from "./generated/types/ConfigTreeSnapshot.ts";
export type { EvaluatedProjection } from "./generated/types/EvaluatedProjection.ts";
export type { EvaluationResult } from "./generated/types/EvaluationResult.ts";
export type { ToolchainContract } from "./generated/types/ToolchainContract.ts";
export type { VirtualPath } from "./generated/types/VirtualPath.ts";
export type { ConfigCommitRequest } from "./generated/types/ConfigCommitRequest.ts";
export type { WorkspaceConfigState as WorkspaceConfigTreeResponse } from "./generated/types/WorkspaceConfigState.ts";
+33 -12
View File
@@ -1,3 +1,4 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import type {
Diagnostic,
DiagnosticSeverity,
@@ -13,10 +14,10 @@ import type {
MemoryStagingRecord,
} from "$lib/generated/memory-api";
const MAX_STAGING_ITEMS = 500;
const MAX_EVIDENCE_PER_RECORD = 500;
const MAX_SOURCE_REFS_PER_RECORD = 500;
const MAX_ORIGIN_VALUE_LENGTH = 512;
const MAX_STAGING_ITEMS = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_EVIDENCE_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_SOURCE_REFS_PER_RECORD = MEMORY_API_LIMITS.maxCollectionItems;
const MAX_ORIGIN_VALUE_LENGTH = MEMORY_API_LIMITS.maxIdentifierBytes;
const candidateKinds = new Set<MemoryCandidateKind>([
"preference",
@@ -51,7 +52,11 @@ export function parseMemoryDocumentResponse(
"Memory document response",
);
return {
body_md: requiredString(record, "body_md"),
body_md: requiredString(
record,
"body_md",
MEMORY_API_LIMITS.maxDocumentBytes,
),
created_at: requiredString(record, "created_at"),
updated_at: requiredString(record, "updated_at"),
bytes: requiredNonNegativeInteger(record, "bytes"),
@@ -258,7 +263,10 @@ function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin {
) {
if (key in record) {
const text = nullableString(record, key);
if (text !== null && text.length > MAX_ORIGIN_VALUE_LENGTH) {
if (
text !== null &&
new TextEncoder().encode(text).byteLength > MAX_ORIGIN_VALUE_LENGTH
) {
invalid(`${key} exceeds the Memory origin limit`);
}
result[key] = text;
@@ -328,11 +336,19 @@ function boundedArray(
return value;
}
function requiredString(record: Record<string, unknown>, key: string): string {
if (typeof record[key] !== "string") {
invalid(`${key} must be a string`);
function requiredString(
record: Record<string, unknown>,
key: string,
maxBytes: number = MEMORY_API_LIMITS.maxStringBytes,
): string {
const value = record[key];
if (
typeof value !== "string" ||
new TextEncoder().encode(value).byteLength > maxBytes
) {
invalid(`${key} must be a bounded string`);
}
return record[key];
return value;
}
function nullableString(
@@ -340,8 +356,13 @@ function nullableString(
key: string,
): string | null {
const value = record[key];
if (value !== null && typeof value !== "string") {
invalid(`${key} must be a string or null`);
if (
value !== null &&
(typeof value !== "string" ||
new TextEncoder().encode(value).byteLength >
MEMORY_API_LIMITS.maxStringBytes)
) {
invalid(`${key} must be a bounded string or null`);
}
return value;
}
@@ -13,6 +13,12 @@ import type {
WorkspaceSigningIdentityResponse,
WorkspaceSigningIdentityState,
} from "$lib/generated/legacy-server-api";
import { readBoundedJson } from "$lib/workspace/api/http.ts";
const MAX_PROFILE_RESPONSE_BYTES = 4 * 1024 * 1024;
const MAX_PROFILE_ERROR_BYTES = 4 * 1024;
const MAX_PROFILE_STRING_BYTES = 1024 * 1024;
const MAX_PROFILE_COLLECTION_ITEMS = 1000;
export class ProfileApiError extends Error {
constructor(
@@ -49,10 +55,7 @@ function exactKeys(
}
function stringValue(value: unknown, context: string): string {
if (typeof value !== "string") {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value;
return boundedStringValue(value, context, MAX_PROFILE_STRING_BYTES);
}
function boundedStringValue(
@@ -60,7 +63,10 @@ function boundedStringValue(
context: string,
maxBytes: number,
): string {
const text = stringValue(value, context);
if (typeof value !== "string") {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
const text = value;
if (new TextEncoder().encode(text).byteLength > maxBytes) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
@@ -107,7 +113,7 @@ function arrayValue<T>(
parser: (item: unknown) => T,
context: string,
): T[] {
if (!Array.isArray(value)) {
if (!Array.isArray(value) || value.length > MAX_PROFILE_COLLECTION_ITEMS) {
throw new ProfileApiError(`${context} returned an invalid response.`, 502);
}
return value.map(parser);
@@ -502,17 +508,23 @@ export function parseWorkspaceSigningIdentityResponse(
};
}
async function boundedErrorText(response: Response): Promise<string> {
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > MAX_PROFILE_ERROR_BYTES) return "";
return new TextDecoder().decode(bytes);
}
async function parseResponse<T>(
response: Response,
parser: (value: unknown) => T,
): Promise<T> {
if (!response.ok) {
throw new ProfileApiError(
(await response.text()) || response.statusText,
(await boundedErrorText(response)) || response.statusText,
response.status,
);
}
return parser(await response.json() as unknown);
return parser(await readBoundedJson(response, MAX_PROFILE_RESPONSE_BYTES));
}
export async function fetchWorkspaceMetadata(
@@ -7,21 +7,18 @@ import type { LayoutLoad } from "./$types";
export const load: LayoutLoad = async ({ fetch, params }) => {
const workspaceId = params.workspaceId;
const [workspaceResult, repositoryResult] = await Promise.all([
loadJson<unknown>(fetch, workspaceApiPath(workspaceId, "/workspace")),
loadJson(
fetch,
workspaceApiPath(workspaceId, "/workspace"),
undefined,
parseWorkspaceResponse,
{ diagnosticLabel: "Workspace API", maxResponseBytes: 1024 * 1024 },
),
loadWorkspaceRepositoryList(fetch, workspaceId),
]);
let workspace = null;
let workspaceError = workspaceResult.error;
if (workspaceResult.data !== null) {
try {
workspace = parseWorkspaceResponse(workspaceResult.data);
} catch (cause) {
workspaceError = cause instanceof Error
? cause.message
: "invalid workspace response";
}
}
const workspace = workspaceResult.data;
const workspaceError = workspaceResult.error;
if (!workspace) {
error(404, {
message: workspaceError ?? `Workspace ${workspaceId} is unavailable`,
@@ -1,3 +1,4 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseMemoryDocumentResponse } from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types";
@@ -10,6 +11,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
workspaceApiPath(params.workspaceId, "/memory"),
undefined,
parseMemoryDocumentResponse,
{
diagnosticLabel: "Memory API",
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
},
),
};
};
@@ -1,3 +1,4 @@
import { MEMORY_API_LIMITS } from "$lib/generated/memory-api";
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseMemoryStagingListResponse } from "$lib/workspace/memory/api";
import type { PageLoad } from "./$types";
@@ -10,6 +11,10 @@ export const load: PageLoad = async ({ fetch, params }) => {
`${workspaceApiPath(params.workspaceId, "/memory/staging")}?limit=200`,
undefined,
parseMemoryStagingListResponse,
{
diagnosticLabel: "Memory API",
maxResponseBytes: MEMORY_API_LIMITS.maxResponseBytes,
},
),
};
};
@@ -12,10 +12,12 @@
RotateRepositorySshCredentialRequest,
} from '$lib/generated/repository-access-api';
import {
REPOSITORY_ACCESS_MAX_RESPONSE_BYTES,
parseRepositorySshCredential,
parseRepositorySshHostTrust,
parseRepositorySshPublicKey,
} from '$lib/workspace/api/repository-access';
import { readBoundedJson } from '$lib/workspace/api/http';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
@@ -71,13 +73,21 @@
body: JSON.stringify(body)
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`Repository Access request failed with status ${response.status}.`);
}
if (response.status === 204) return undefined;
const payload: unknown = await response.json();
if (parse === null) {
throw new Error('Repository Access returned an unexpected response body.');
if (response.status !== 204) {
await response.body?.cancel();
throw new Error('Repository Access returned an unexpected response body.');
}
await response.body?.cancel();
return undefined;
}
if (response.status === 204) {
throw new Error('Repository Access returned an unexpected empty response.');
}
const payload = await readBoundedJson(response, REPOSITORY_ACCESS_MAX_RESPONSE_BYTES);
return parse(payload);
}
@@ -86,10 +96,11 @@
`${base}/credentials/${encodeURIComponent(credentialId)}/public-key`,
{ headers: { accept: 'application/json' } }
);
const body = await response.json();
if (!response.ok) {
await response.body?.cancel();
throw new Error(`Repository Access request failed with status ${response.status}.`);
}
const body = await readBoundedJson(response, REPOSITORY_ACCESS_MAX_RESPONSE_BYTES);
return parseRepositorySshPublicKey(body);
}
+99 -4
View File
@@ -1,11 +1,18 @@
/// <reference lib="deno.ns" />
import { assert, assertEquals } from "jsr:@std/assert";
import {
assert,
assertEquals,
assertRejects,
assertThrows,
} from "jsr:@std/assert";
import {
commitConfigTree,
ConfigSourceApiError,
fetchConfigEntry,
fetchConfigRevision,
fetchConfigTree,
parseWorkspaceConfigTreeResponse,
} from "../../src/lib/workspace/config-source/api.ts";
function response(body: unknown, status = 200): Response {
@@ -15,11 +22,43 @@ function response(body: unknown, status = 200): Response {
});
}
const entry = {
path: "profiles/main.dcdl",
content_type: "decodal",
content: "profile = {}",
content_digest: "sha256:entry",
};
const snapshot = {
revision: 7,
digest: "sha256:tree",
entries: { "profiles/main.dcdl": entry },
};
const tree = {
snapshot,
contract: {
contract_version: 1,
decodal_version: "0.4.0",
schema_version: 1,
entrypoints: ["main.dcdl"],
import_policy_version: 1,
schema_bundle: {
contributions: [],
source: "builtin",
fingerprint: "sha256:schema",
},
fingerprint: "sha256:toolchain",
},
projection_digest: "sha256:projection",
};
Deno.test("config source API commits directly through the workspace scope", async () => {
const calls: Array<{ url: string; init?: RequestInit }> = [];
const fetcher = ((input: string | URL | Request, init?: RequestInit) => {
calls.push({ url: String(input), init });
return Promise.resolve(response({ ok: true }));
const url = String(input);
calls.push({ url, init });
if (url.includes("/entries/")) return Promise.resolve(response(entry));
if (url.includes("/revisions/")) return Promise.resolve(response(snapshot));
return Promise.resolve(response(tree));
}) as typeof fetch;
await fetchConfigTree("w/one", fetcher);
@@ -44,7 +83,63 @@ Deno.test("config source API commits directly through the workspace scope", asyn
);
});
Deno.test("config source API surfaces failed evaluation instead of treating it as a successful commit", async () => {
Deno.test("config source API rejects unknown response fields", async () => {
const fetcher =
(() =>
Promise.resolve(response({ ...tree, unexpected: true }))) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API rejects mismatched entry map paths", async () => {
const invalid = {
...tree,
snapshot: {
...snapshot,
entries: { "profiles/other.dcdl": entry },
},
};
const fetcher = (() => Promise.resolve(response(invalid))) as typeof fetch;
await assertRejects(
() => fetchConfigTree("w", fetcher),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API rejects unsafe revisions and oversized entry content", () => {
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: { ...snapshot, revision: Number.MAX_SAFE_INTEGER + 1 },
}),
ConfigSourceApiError,
"invalid response",
);
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: {
...snapshot,
entries: {
"profiles/main.dcdl": {
...entry,
content: "x".repeat(256 * 1024 + 1),
},
},
},
}),
ConfigSourceApiError,
"invalid response",
);
});
Deno.test("config source API surfaces bounded failed evaluation details", async () => {
const fetcher = (() =>
Promise.resolve(
new Response("structured diagnostics", { status: 422 }),