This commit is contained in:
2026-09-03 11:28:30 +07:00 Unverified
parent 26c2d0f015
commit 82d0fe3e0d
93 changed files with 19237 additions and 1285 deletions
+245
View File
@@ -0,0 +1,245 @@
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue =
| JsonPrimitive
| JsonValue[]
| { [key: string]: JsonValue };
export type ApiProblemDetails = {
type?: string;
title: string;
status: number;
detail?: string;
message?: string;
/** Stable machine-readable identifier, unlike title or detail. */
code: string;
requestId?: string;
operationId?: string;
errors?: Record<string, string[]>;
};
export type Page<T> = {
items: T[];
nextCursor?: string;
};
export const KUBER_API_VERSION = "kuber.astrxl.dev/v2" as const;
export type ObjectMeta = {
name: string;
uid: string;
resourceVersion: string;
creationTimestamp: string;
labels?: Record<string, string>;
annotations?: Record<string, string>;
};
export type WorkspaceSourceReference = {
uri: string;
digest: string;
revision?: string;
};
export type WorkspaceSpec = {
source: WorkspaceSourceReference;
config?: unknown;
};
export type Workspace = {
apiVersion: typeof KUBER_API_VERSION;
kind: "Workspace";
metadata: ObjectMeta;
spec: WorkspaceSpec;
status: { latestRevision: number };
};
export type CreateWorkspaceRequest = {
id: string;
source: WorkspaceSourceReference;
config?: unknown;
labels?: Record<string, string>;
annotations?: Record<string, string>;
};
export type UpdateWorkspaceRequest = Omit<CreateWorkspaceRequest, "id">;
export type CreateWorkspaceResponse = Workspace;
export type ListWorkspacesResponse = Page<Workspace>;
export type GetWorkspaceResponse = Workspace;
export type DeleteWorkspaceResponse = OperationAcceptedResponse;
export type AdoptWorkspaceResponse = {
workspaceId: string;
workspaceUid: string;
resourcesAdopted: number;
};
export type AdoptPlatformRequest = { workspaceUid: string };
export type AdoptPlatformResponse = AdoptWorkspaceResponse;
export type WorkspaceState =
| "pending"
| "ready"
| "degraded"
| "stopped"
| "deleting"
| "failed";
export type ServiceStatus = {
name: string;
state: "pending" | "running" | "stopped" | "failed" | "unknown";
readyReplicas: number;
desiredReplicas: number;
image?: string;
message?: string;
};
export type WorkspaceStatusResponse = {
workspaceId: string;
state: WorkspaceState;
services: ServiceStatus[];
observedAt: string;
};
export type LifecycleAction = "start" | "stop" | "restart" | "delete";
export type LifecycleRequest = {
action: LifecycleAction;
services?: string[];
};
export type LifecycleResponse = OperationAcceptedResponse;
export type ResourceObject = {
apiVersion: string;
kind: string;
metadata: {
name: string;
namespace?: string;
labels?: Record<string, string>;
annotations?: Record<string, string>;
};
} & Record<string, unknown>;
export type ResourceChange = {
apiVersion: string;
kind: string;
name: string;
action: "create" | "update" | "delete" | "unchanged";
};
export type ReconcileResourcesRequest = {
resources: ResourceObject[];
prune?: boolean;
dryRun?: boolean;
};
export type ReconcileResourcesResponse = {
operationId?: string;
changes: ResourceChange[];
};
export type DatabaseEngine = "postgres";
export type DatabaseClaim = {
name: string;
engine: DatabaseEngine;
database?: string;
username?: string;
};
export type ReconcileDatabasesRequest = { claims: DatabaseClaim[] };
export type DatabaseBinding = {
claim: string;
service: string;
environment: Record<string, string>;
};
export type ReconcileDatabasesResponse = {
bindings: DatabaseBinding[];
operationId?: string;
};
export type StorageKind = "s3";
export type StorageClaim = {
name: string;
kind: StorageKind;
bucket?: string;
region?: string;
};
export type ReconcileStorageRequest = { claims: StorageClaim[] };
export type StorageBinding = {
claim: string;
service: string;
environment: Record<string, string>;
};
export type ReconcileStorageResponse = {
bindings: StorageBinding[];
operationId?: string;
};
export type OperationState =
| "pending"
| "running"
| "succeeded"
| "failed"
| "cancelled";
export type OperationError = { code: string; message: string };
export type Operation = {
apiVersion: typeof KUBER_API_VERSION;
kind: "Operation";
metadata: ObjectMeta & { workspaceUid?: string };
spec: { workspaceId: string; action: string };
status: {
state: OperationState;
startedAt?: string;
finishedAt?: string;
result?: unknown;
error?: OperationError;
};
};
export type OperationAcceptedResponse = {
operationId: string;
operation?: Operation;
};
export type GetOperationResponse = Operation;
export type CancelOperationResponse = Operation;
export type OperationEvent = {
operationId: string;
sequence: number;
timestamp: string;
type: "status" | "progress" | "log" | "result" | "error";
data: JsonValue;
};
export type UserRole = "admin" | "operator" | "viewer" | (string & {});
export type User = {
username: string;
roles: UserRole[];
disabled: boolean;
createdAt?: string;
updatedAt?: string;
};
export type CreateUserRequest = {
username: string;
password: string;
roles: UserRole[];
};
export type UpdateUserRequest = {
password?: string;
roles?: UserRole[];
disabled?: boolean;
};
export type UserResponse = User;
export type ListUsersResponse = Page<User>;
export type AuditEvent = {
id: string;
timestamp: string;
actor: string;
action: string;
resourceType: string;
resourceId?: string;
workspaceId?: string;
requestId?: string;
operationId?: string;
outcome: "success" | "failure";
metadata?: Record<string, JsonValue>;
};
export type ListAuditEventsRequest = {
cursor?: string;
limit?: number;
actor?: string;
workspaceId?: string;
since?: string;
until?: string;
};
export type ListAuditEventsResponse = Page<AuditEvent>;
+226
View File
@@ -0,0 +1,226 @@
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
import {
dirname,
isAbsolute,
normalize,
relative,
resolve,
sep,
} from "node:path";
export interface ArtifactProvider {
isFile(path: string, options?: ArtifactReadOptions): boolean;
readText(path: string, options?: ArtifactReadOptions): string;
}
export type ArtifactReadOptions = {
expandHome?: boolean;
};
export type ArtifactBundle = {
version: 1;
files: Record<string, string>;
};
export type ArtifactLimits = {
maxArtifactCount?: number;
maxArtifactBytes?: number;
maxTotalBytes?: number;
};
export type LocalArtifactProviderOptions = ArtifactLimits & {
workspace: string;
strict?: boolean;
};
export type BundleArtifactProviderOptions = ArtifactLimits;
export const DEFAULT_ARTIFACT_LIMITS = {
maxArtifactCount: 100,
maxArtifactBytes: 1024 * 1024,
maxTotalBytes: 4 * 1024 * 1024,
} as const;
function artifactError(
message: string,
code?: string,
): Error & { code?: string } {
return Object.assign(new Error(message), code ? { code } : {});
}
function toWorkspacePath(path: string): string {
if (
!path ||
path.includes("\0") ||
path.includes("\\") ||
path.startsWith("~") ||
isAbsolute(path)
) {
throw artifactError(`Artifact path must be workspace-relative: ${path}`);
}
const normalized = normalize(path);
if (normalized === ".." || normalized.startsWith(`..${sep}`)) {
throw artifactError(`Artifact path escapes the workspace: ${path}`);
}
return normalized.replace(/^\.\//, "");
}
function assertWithinWorkspace(workspace: string, path: string): void {
const relation = relative(workspace, path);
if (
relation === ".." ||
relation.startsWith(`..${sep}`) ||
isAbsolute(relation)
) {
throw artifactError(`Artifact path escapes the workspace: ${path}`);
}
}
function assertRealPathWithinWorkspace(workspace: string, path: string): void {
let existingPath = path;
while (!existsSync(existingPath)) {
const parent = dirname(existingPath);
if (parent === existingPath) break;
existingPath = parent;
}
assertWithinWorkspace(workspace, realpathSync(existingPath));
}
function assertPositiveLimit(name: string, value: number | undefined): void {
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
throw new Error(`${name} must be a non-negative safe integer`);
}
}
class ArtifactBudget {
readonly #limits: ArtifactLimits;
#count = 0;
#bytes = 0;
constructor(limits: ArtifactLimits) {
assertPositiveLimit("maxArtifactCount", limits.maxArtifactCount);
assertPositiveLimit("maxArtifactBytes", limits.maxArtifactBytes);
assertPositiveLimit("maxTotalBytes", limits.maxTotalBytes);
this.#limits = limits;
}
add(path: string, content: string): void {
const bytes = Buffer.byteLength(content);
if (
this.#limits.maxArtifactBytes !== undefined &&
bytes > this.#limits.maxArtifactBytes
) {
throw artifactError(
`Artifact ${path} exceeds the ${this.#limits.maxArtifactBytes} byte limit`,
);
}
if (
this.#limits.maxArtifactCount !== undefined &&
this.#count + 1 > this.#limits.maxArtifactCount
) {
throw artifactError(
`Artifact count exceeds the ${this.#limits.maxArtifactCount} limit`,
);
}
if (
this.#limits.maxTotalBytes !== undefined &&
this.#bytes + bytes > this.#limits.maxTotalBytes
) {
throw artifactError(
`Artifact bytes exceed the ${this.#limits.maxTotalBytes} byte limit`,
);
}
this.#count += 1;
this.#bytes += bytes;
}
}
export class LocalArtifactProvider implements ArtifactProvider {
readonly #workspace: string;
readonly #strict: boolean;
readonly #budget: ArtifactBudget;
constructor(options: LocalArtifactProviderOptions) {
this.#workspace = options.strict
? realpathSync(options.workspace)
: resolve(options.workspace);
this.#strict = options.strict ?? false;
this.#budget = new ArtifactBudget(options);
}
#resolve(path: string, options: ArtifactReadOptions): string {
if (!this.#strict) {
return options.expandHome && path.startsWith("~")
? resolve(process.env.HOME ?? "", path.slice(1))
: resolve(this.#workspace, path);
}
const candidate = resolve(this.#workspace, toWorkspacePath(path));
assertWithinWorkspace(this.#workspace, candidate);
assertRealPathWithinWorkspace(this.#workspace, candidate);
return candidate;
}
isFile(path: string, options: ArtifactReadOptions = {}): boolean {
const resolved = this.#resolve(path, options);
return existsSync(resolved) && statSync(resolved).isFile();
}
readText(path: string, options: ArtifactReadOptions = {}): string {
const resolved = this.#resolve(path, options);
if (this.#strict) {
// Resolve again at read time so a symlink swap cannot bypass confinement.
assertWithinWorkspace(this.#workspace, realpathSync(resolved));
}
const content = readFileSync(resolved, "utf8");
this.#budget.add(path, content);
return content;
}
}
export class BundleArtifactProvider implements ArtifactProvider {
readonly #files = new Map<string, string>();
constructor(
bundle: ArtifactBundle,
options: BundleArtifactProviderOptions = {},
) {
if (bundle.version !== 1)
throw new Error("Unsupported artifact bundle version");
const budget = new ArtifactBudget({
...DEFAULT_ARTIFACT_LIMITS,
...options,
});
for (const [path, content] of Object.entries(bundle.files)) {
const normalized = toWorkspacePath(path);
if (this.#files.has(normalized)) {
throw artifactError(`Duplicate artifact path: ${path}`);
}
budget.add(normalized, content);
this.#files.set(normalized, content);
}
}
isFile(path: string): boolean {
return this.#files.has(toWorkspacePath(path));
}
readText(path: string): string {
const normalized = toWorkspacePath(path);
const content = this.#files.get(normalized);
if (content === undefined) {
throw artifactError(`Artifact not found: ${path}`, "ENOENT");
}
return content;
}
}
export function createArtifactBundle(
files: Record<string, string>,
): ArtifactBundle {
return { version: 1, files: { ...files } };
}
+63
View File
@@ -0,0 +1,63 @@
export const BUILD_PROTOCOL_VERSION = 1 as const;
export type Sha256Digest = `sha256:${string}`;
export type BuildArchitecture = "amd64" | "arm64";
export type WorkspaceFile = {
path: string;
type: "file" | "symlink";
digest: Sha256Digest;
size: number;
mode: 0o644 | 0o755 | 0o777;
};
export type WorkspaceManifest = {
version: typeof BUILD_PROTOCOL_VERSION;
files: WorkspaceFile[];
};
export type BuildSpec = {
architecture: BuildArchitecture;
image: string;
context: string;
dockerfile?: string;
target?: string;
buildArgs: string[];
workspace: Sha256Digest;
};
export type BuildRequest = {
version: typeof BUILD_PROTOCOL_VERSION;
id: string;
project: string;
service: string;
spec: BuildSpec;
};
export type BuildState = "queued" | "running" | "succeeded" | "failed";
export type BuildStatus = {
version: typeof BUILD_PROTOCOL_VERSION;
id: string;
state: BuildState;
createdAt: string;
startedAt?: string;
finishedAt?: string;
digest?: Sha256Digest;
error?: string;
};
export type BuildEvent =
| { type: "status"; status: BuildStatus }
| { type: "log"; id: string; sequence: number; message: string };
export function isSha256Digest(value: unknown): value is Sha256Digest {
return typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value);
}
export function assertSha256Digest(
value: unknown,
): asserts value is Sha256Digest {
if (!isSha256Digest(value))
throw new Error(`Invalid SHA-256 digest: ${value}`);
}