feat(ql3): cut alpha.1 candidate milestone

This commit is contained in:
whyour
2026-08-26 01:41:20 +08:00
parent c2df0c7215
commit 07cdc76bae
94 changed files with 1469 additions and 168 deletions
@@ -8,6 +8,12 @@ import type {
DeploymentProfile,
OpenPostgresDatabase,
} from '@qinglong/runtime-core';
import {
createSingletonApiCredentialPepperKeyring,
normalizeApiCredentialPepperKeyring,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import { LEGACY_API_CREDENTIAL_PEPPER_KEY_ID } from '@qinglong/runtime-core/api-credential';
import {
bootstrapClusterControlRuntime,
type ClusterControlAssemblyInput,
@@ -17,13 +23,13 @@ import {
type ClusterSchedulerRuntimeOptions,
type ClusterWorkerRuntimeDependencies,
} from './clusterControlRuntime';
import { assertClusterControlApiCredentialPepper } from '../authentication/apiCredentialAuthenticator';
import {
startClusterControlHttpSurface,
type ClusterControlAdmissionPipeline,
type ClusterControlHttpAddress,
type ClusterControlHttpSurfaceOptions,
} from '../transport/httpSurface';
import { ClusterControlApiCredentialConfigurationError } from '../authentication/apiCredentialAuthenticator';
import type { ClusterControlAvailabilitySource } from '../database/availability';
export interface ClusterControlApplicationStack {
@@ -36,7 +42,9 @@ export interface ClusterControlApplicationStack {
export interface ClusterControlApplicationOptions {
readonly enabled?: boolean;
readonly profile: DeploymentProfile;
/** Explicit singleton compatibility bridge; production config emits a keyring. */
readonly apiCredentialPepper?: string;
readonly apiCredentialPepperKeyring?: Readonly<ApiCredentialPepperKeyring>;
readonly recovery?: ClusterControlRecoveryRuntimeOptions;
readonly scheduler?: ClusterSchedulerRuntimeOptions;
readonly cancellationConvergence?: ClusterRunCancellationConvergenceRuntimeOptions;
@@ -83,6 +91,9 @@ function inactiveBootstrap(
...(options.apiCredentialPepper === undefined
? {}
: { apiCredentialPepper: options.apiCredentialPepper }),
...(options.apiCredentialPepperKeyring === undefined
? {}
: { apiCredentialPepperKeyring: options.apiCredentialPepperKeyring }),
...(options.recovery === undefined ? {} : { recovery: options.recovery }),
...(options.scheduler === undefined
? {}
@@ -121,8 +132,30 @@ export async function startClusterControlApplication(
return inactive;
}
assertClusterControlApiCredentialPepper(options.apiCredentialPepper ?? '');
const apiCredentialPepper = options.apiCredentialPepper!;
if (
(options.apiCredentialPepper === undefined) ===
(options.apiCredentialPepperKeyring === undefined)
) {
throw new TypeError(
'Cluster-control API credential configuration is invalid',
);
}
let apiCredentialPepperKeyring: Readonly<ApiCredentialPepperKeyring>;
try {
apiCredentialPepperKeyring =
options.apiCredentialPepperKeyring === undefined
? createSingletonApiCredentialPepperKeyring(
options.apiCredentialPepper!,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
)
: normalizeApiCredentialPepperKeyring(
options.apiCredentialPepperKeyring,
);
} catch {
throw new ClusterControlApiCredentialConfigurationError(
'pepper keyring is invalid',
);
}
if (
!options.availability ||
typeof options.availability.subscribe !== 'function'
@@ -160,7 +193,7 @@ export async function startClusterControlApplication(
activation = await bootstrapClusterControlRuntime({
enabled: true,
profile: options.profile,
apiCredentialPepper,
apiCredentialPepperKeyring,
...(options.recovery === undefined ? {} : { recovery: options.recovery }),
...(options.scheduler === undefined
? {}
@@ -30,6 +30,12 @@ import {
type ClusterRunCancellationConvergenceCycleResult,
} from '@qinglong/runtime-core';
import type { ClusterRunCancellationRepository } from '@qinglong/runtime-core/cluster-run-cancellation';
import {
createSingletonApiCredentialPepperKeyring,
normalizeApiCredentialPepperKeyring,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import { LEGACY_API_CREDENTIAL_PEPPER_KEY_ID } from '@qinglong/runtime-core/api-credential';
import {
MAX_CLUSTER_RUN_ATTEMPT_LOG_RETENTION_CLAIMS,
MAX_CLUSTER_RUN_ATTEMPT_LOG_RETENTION_LEASE_MS,
@@ -100,7 +106,7 @@ import {
PostgresAuthorizedPluginPackageWorkflowStepRunListRepository,
} from '@qinglong/cluster-postgres/plugin-package-workflow-administration';
import {
assertClusterControlApiCredentialPepper,
ClusterControlApiCredentialConfigurationError,
createClusterControlApiCredentialAuthenticator,
} from '../authentication/apiCredentialAuthenticator';
import type {
@@ -218,7 +224,9 @@ export interface ClusterRunAttemptLogRetentionRuntimeOptions {
export interface ClusterControlBootstrapOptions {
readonly enabled?: boolean;
readonly profile: DeploymentProfile;
/** Explicit singleton compatibility bridge; production config emits a keyring. */
readonly apiCredentialPepper?: string;
readonly apiCredentialPepperKeyring?: Readonly<ApiCredentialPepperKeyring>;
readonly recovery?: ClusterControlRecoveryRuntimeOptions;
readonly scheduler?: ClusterSchedulerRuntimeOptions;
readonly cancellationConvergence?: ClusterRunCancellationConvergenceRuntimeOptions;
@@ -628,8 +636,33 @@ export async function bootstrapClusterControlRuntime(
| undefined;
let logRetentionRuntime: PreparedLogRetentionRuntime | undefined;
let recoveryRegistry: ClusterControlRecoveryEvidenceRegistry | undefined;
let apiCredentialPepperKeyring:
| Readonly<ApiCredentialPepperKeyring>
| undefined;
if ((options.enabled ?? false) && options.profile === 'cluster-control') {
assertClusterControlApiCredentialPepper(options.apiCredentialPepper ?? '');
if (
(options.apiCredentialPepper === undefined) ===
(options.apiCredentialPepperKeyring === undefined)
) {
throw new TypeError(
'Cluster-control API credential configuration is invalid',
);
}
try {
apiCredentialPepperKeyring =
options.apiCredentialPepperKeyring === undefined
? createSingletonApiCredentialPepperKeyring(
options.apiCredentialPepper!,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
)
: normalizeApiCredentialPepperKeyring(
options.apiCredentialPepperKeyring,
);
} catch {
throw new ClusterControlApiCredentialConfigurationError(
'pepper keyring is invalid',
);
}
recoveryRuntime = prepareRecoveryRuntime(options.recovery);
schedulerRuntime = prepareSchedulerRuntime(
options.scheduler,
@@ -857,7 +890,7 @@ export async function bootstrapClusterControlRuntime(
evidence,
authenticator: createClusterControlApiCredentialAuthenticator(
new PostgresApiCredentialRepository(database.pool),
options.apiCredentialPepper ?? '',
apiCredentialPepperKeyring!,
),
policies: new PostgresProjectPolicyRepository(database.pool),
runs,
@@ -166,6 +166,7 @@ export interface ProductionClusterControlApplicationOptions
| 'enabled'
| 'profile'
| 'apiCredentialPepper'
| 'apiCredentialPepperKeyring'
| 'openDatabase'
| 'availability'
| 'http'
@@ -436,7 +437,7 @@ export function startProductionClusterControlApplication(
...applicationOptions,
enabled: true,
profile: 'cluster-control',
apiCredentialPepper: config.security.apiCredentialPepper,
apiCredentialPepperKeyring: config.security.apiCredentialPepperKeyring,
http: config.http,
...(workerIngress === undefined
? {}
@@ -7,6 +7,13 @@ import {
normalizeApiCredentialRecord,
type ApiCredentialRepository,
} from '@qinglong/runtime-core/api-credential';
import {
activeApiCredentialPepperKey,
createSingletonApiCredentialPepperKeyring,
normalizeApiCredentialPepperKeyring,
resolveApiCredentialPepperKey,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import {
normalizeSecurityPrincipal,
type SecurityPrincipal,
@@ -137,7 +144,7 @@ function parseAuthorization(
export function createClusterControlApiCredentialAuthenticator(
repository: ApiCredentialRepository,
pepperBase64Url: string,
pepperKeyringValue: Readonly<ApiCredentialPepperKeyring> | string,
options: ClusterControlApiCredentialAuthenticatorOptions = {},
): ClusterControlRequestAuthenticator {
if (!repository || typeof repository.resolve !== 'function') {
@@ -164,16 +171,25 @@ export function createClusterControlApiCredentialAuthenticator(
if (options.now !== undefined && typeof options.now !== 'function') {
throw new ClusterControlApiCredentialConfigurationError('now is invalid');
}
const pepperKeyId =
options.pepperKeyId ?? LEGACY_API_CREDENTIAL_PEPPER_KEY_ID;
let pepperKeyring: Readonly<ApiCredentialPepperKeyring>;
try {
assertApiCredentialPepperKeyId(pepperKeyId);
if (typeof pepperKeyringValue === 'string') {
const pepperKeyId =
options.pepperKeyId ?? LEGACY_API_CREDENTIAL_PEPPER_KEY_ID;
assertApiCredentialPepperKeyId(pepperKeyId);
pepperKeyring = createSingletonApiCredentialPepperKeyring(
pepperKeyringValue,
pepperKeyId,
);
} else {
if (options.pepperKeyId !== undefined) throw new TypeError();
pepperKeyring = normalizeApiCredentialPepperKeyring(pepperKeyringValue);
}
} catch {
throw new ClusterControlApiCredentialConfigurationError(
'pepperKeyId is invalid',
'pepper keyring is invalid',
);
}
const pepper = decodeSecret('pepper', pepperBase64Url);
const ttlMs = principalTtl(options.principalTtlMs);
const now = options.now ?? Date.now;
@@ -183,37 +199,42 @@ export function createClusterControlApiCredentialAuthenticator(
): Promise<Readonly<SecurityPrincipal> | null> {
const parsed = parseAuthorization(metadata);
if (!parsed) return null;
const presentedDigest = digest(
pepper,
parsed.credentialId,
parsed.secret,
);
parsed.secret.fill(0);
let candidate;
try {
candidate = await repository.resolve(parsed.credentialId);
} catch (error) {
presentedDigest.fill(0);
parsed.secret.fill(0);
if (error instanceof ApiCredentialUnavailableError) {
throw new ClusterControlApiCredentialUnavailableError();
}
throw new ClusterControlApiCredentialUnavailableError();
}
if (metadata.signal.aborted) {
presentedDigest.fill(0);
parsed.secret.fill(0);
throw new ClusterControlApiCredentialUnavailableError();
}
let record;
try {
record = candidate ? normalizeApiCredentialRecord(candidate) : null;
} catch {
presentedDigest.fill(0);
parsed.secret.fill(0);
throw new ClusterControlApiCredentialUnavailableError();
}
if (record && record.pepperKeyId !== pepperKeyId) {
presentedDigest.fill(0);
const key = record
? resolveApiCredentialPepperKey(pepperKeyring, record.pepperKeyId)
: activeApiCredentialPepperKey(pepperKeyring);
if (!key) {
parsed.secret.fill(0);
throw new ClusterControlApiCredentialUnavailableError();
}
const pepper = decodeSecret('pepper', key.pepper);
const presentedDigest = digest(
pepper,
parsed.credentialId,
parsed.secret,
);
pepper.fill(0);
parsed.secret.fill(0);
const storedDigest = record
? Buffer.from(record.secretDigest, 'hex')
: Buffer.alloc(32);
@@ -2,6 +2,20 @@ import type {
DeploymentProfile,
OpenPostgresDatabase,
} from '@qinglong/runtime-core';
import {
createSingletonApiCredentialPepperKeyring,
normalizeApiCredentialPepperKeyring,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import { LEGACY_API_CREDENTIAL_PEPPER_KEY_ID } from '@qinglong/runtime-core/api-credential';
import {
closeSync,
constants,
fstatSync,
openSync,
readFileSync,
} from 'node:fs';
import { isAbsolute, normalize } from 'node:path';
import {
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
@@ -41,7 +55,7 @@ export interface EnabledClusterControlConfig {
pool: PostgresPoolOptions;
}>;
readonly security: Readonly<{
apiCredentialPepper: string;
apiCredentialPepperKeyring: Readonly<ApiCredentialPepperKeyring>;
}>;
readonly logRetention:
| Readonly<{ readonly enabled: false }>
@@ -224,26 +238,88 @@ function runtimeConnection(
});
}
function apiCredentialPepper(environment: ClusterControlEnvironment): string {
const value = boundedValue(
function apiCredentialPepperKeyring(
environment: ClusterControlEnvironment,
): Readonly<ApiCredentialPepperKeyring> {
const legacyPepper = boundedValue(
environment,
'QL3_API_CREDENTIAL_PEPPER',
64,
true,
)!;
if (!/^[A-Za-z0-9_-]{43}$/.test(value)) {
);
const keyringFile = boundedValue(
environment,
'QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE',
4_096,
);
if ((legacyPepper === undefined) === (keyringFile === undefined)) {
throw new ClusterControlConfigError(
'QL3_API_CREDENTIAL_PEPPER must be canonical base64url for 32 bytes',
'exactly one API credential pepper source is required',
);
}
const decoded = Buffer.from(value, 'base64url');
if (decoded.byteLength !== 32 || decoded.toString('base64url') !== value) {
if (legacyPepper !== undefined) {
try {
return createSingletonApiCredentialPepperKeyring(
legacyPepper,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
);
} catch {
throw new ClusterControlConfigError(
'QL3_API_CREDENTIAL_PEPPER must be canonical base64url for 32 bytes',
);
}
}
if (
!isAbsolute(keyringFile!) ||
normalize(keyringFile!) !== keyringFile ||
keyringFile!.includes('\0')
) {
throw new ClusterControlConfigError(
'QL3_API_CREDENTIAL_PEPPER must be canonical base64url for 32 bytes',
'QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE must be a normalized absolute path',
);
}
decoded.fill(0);
return value;
let descriptor: number | undefined;
let bytes: Buffer | undefined;
try {
descriptor = openSync(
keyringFile!,
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
);
const before = fstatSync(descriptor);
if (
!before.isFile() ||
before.size < 1 ||
before.size > 2_048 ||
(before.mode & 0o022) !== 0
) {
throw new ClusterControlConfigError(
'API credential pepper keyring file authority is invalid',
);
}
bytes = readFileSync(descriptor);
const after = fstatSync(descriptor);
if (
before.dev !== after.dev ||
before.ino !== after.ino ||
before.size !== after.size ||
before.mtimeMs !== after.mtimeMs ||
bytes.byteLength !== before.size
) {
throw new ClusterControlConfigError(
'API credential pepper keyring changed while being read',
);
}
return normalizeApiCredentialPepperKeyring(
JSON.parse(bytes.toString('utf8')),
);
} catch (error) {
if (error instanceof ClusterControlConfigError) throw error;
throw new ClusterControlConfigError(
'QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE is invalid',
);
} finally {
bytes?.fill(0);
if (descriptor !== undefined) closeSync(descriptor);
}
}
function logRetentionConfig(
@@ -443,7 +519,7 @@ export function loadClusterControlConfig(
}),
}),
security: Object.freeze({
apiCredentialPepper: apiCredentialPepper(environment),
apiCredentialPepperKeyring: apiCredentialPepperKeyring(environment),
}),
logRetention: logRetentionConfig(environment),
};