feat: add CI deployment and live progress

This commit is contained in:
2026-09-05 12:09:16 +00:00 Unverified
parent 321f4e807a
commit aa02826dbb
27 changed files with 2711 additions and 180 deletions
+102 -1
View File
@@ -1,5 +1,10 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import { isRole, type Role } from "./authorization";
import {
isCapability,
isRole,
type Capability,
type Role,
} from "./authorization";
export type KuberUser = {
username: string;
@@ -25,6 +30,18 @@ export type SessionInput =
expiresAt: string;
};
export type ApiKeyRecord = {
id: string;
tokenHash: string;
username: string;
capabilities: Capability[];
workspace?: string;
expiresAt: string;
disabled?: boolean;
};
export type NewApiKey = ApiKeyRecord;
export type NewKuberUser = Omit<KuberUser, "authVersion"> & {
authVersion?: number;
};
@@ -49,6 +66,11 @@ export interface AuthStore {
revokeUserSessions(username: string): Promise<number>;
listExpiredSessions(now?: number): Promise<SessionRecord[]>;
deleteExpiredSessions(now?: number): Promise<number>;
getApiKey(tokenHash: string): Promise<ApiKeyRecord | undefined>;
createApiKey(key: NewApiKey): Promise<void>;
listApiKeys(username: string): Promise<ApiKeyRecord[]>;
revokeApiKey(username: string, id: string): Promise<boolean>;
deleteExpiredApiKeys(now?: number): Promise<number>;
}
export function normalizeUser(user: NewKuberUser | KuberUser): KuberUser {
@@ -85,6 +107,42 @@ export function normalizeSession(session: SessionRecord): SessionRecord {
return { ...session };
}
export function normalizeApiKey(key: NewApiKey): ApiKeyRecord {
if (!/^[a-zA-Z0-9_-]{16,128}$/.test(key.id))
throw new Error("API key ID is invalid");
if (!/^[a-f0-9]{64}$/.test(key.tokenHash))
throw new Error("API key token hash must be a SHA-256 hex digest");
if (!key.username || key.username !== key.username.trim())
throw new Error("API key username is required");
if (
!Array.isArray(key.capabilities) ||
key.capabilities.length === 0 ||
new Set(key.capabilities).size !== key.capabilities.length ||
!key.capabilities.every(isCapability)
) {
throw new Error("API key requires unique valid capabilities");
}
if (
key.workspace !== undefined &&
(!/^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$/.test(key.workspace) ||
key.workspace.length > 63)
) {
throw new Error("API key workspace scope is invalid");
}
const expiresAt = new Date(key.expiresAt);
if (
!Number.isFinite(expiresAt.getTime()) ||
expiresAt.toISOString() !== key.expiresAt
) {
throw new Error("API key expiration must be an ISO timestamp");
}
return {
...key,
capabilities: [...key.capabilities],
disabled: Boolean(key.disabled),
};
}
export function hashToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
@@ -107,6 +165,7 @@ export function tokenHashesEqual(left: string, right: string): boolean {
export class MemoryAuthStore implements AuthStore {
readonly users = new Map<string, KuberUser>();
readonly sessions = new Map<string, SessionRecord>();
readonly apiKeys = new Map<string, ApiKeyRecord>();
async getUser(username: string): Promise<KuberUser | undefined> {
return this.users.get(username);
@@ -148,6 +207,8 @@ export class MemoryAuthStore implements AuthStore {
async deleteUser(username: string): Promise<boolean> {
await this.revokeUserSessions(username);
for (const [id, key] of this.apiKeys)
if (key.username === username) this.apiKeys.delete(id);
return this.users.delete(username);
}
@@ -201,4 +262,44 @@ export class MemoryAuthStore implements AuthStore {
for (const session of expired) this.sessions.delete(session.tokenHash);
return expired.length;
}
async getApiKey(tokenHash: string): Promise<ApiKeyRecord | undefined> {
const key = [...this.apiKeys.values()].find(
(item) => item.tokenHash === tokenHash,
);
if (!key || key.disabled || Date.parse(key.expiresAt) <= Date.now()) return;
const user = this.users.get(key.username);
if (!user || user.disabled) return;
return key;
}
async createApiKey(key: NewApiKey): Promise<void> {
const normalized = normalizeApiKey(key);
const user = this.users.get(normalized.username);
if (!user || user.disabled) throw new Error("API key user is not active");
if (this.apiKeys.has(normalized.id))
throw new Error("API key already exists");
this.apiKeys.set(normalized.id, normalized);
}
async listApiKeys(username: string): Promise<ApiKeyRecord[]> {
return [...this.apiKeys.values()]
.filter((key) => key.username === username)
.sort((left, right) => left.id.localeCompare(right.id));
}
async revokeApiKey(username: string, id: string): Promise<boolean> {
const key = this.apiKeys.get(id);
if (!key || key.username !== username) return false;
this.apiKeys.delete(id);
return true;
}
async deleteExpiredApiKeys(now = Date.now()): Promise<number> {
const expired = [...this.apiKeys.values()].filter(
(key) => Date.parse(key.expiresAt) <= now,
);
for (const key of expired) this.apiKeys.delete(key.id);
return expired.length;
}
}