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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/cluster-control",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 cluster-control composition root",
"license": "Apache-2.0",
@@ -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),
};
@@ -13,6 +13,7 @@ const {
const NOW = 10_000;
const PEPPER = Buffer.alloc(32, 1).toString('base64url');
const NEXT_PEPPER = Buffer.alloc(32, 3).toString('base64url');
const SECRET = Buffer.alloc(32, 2).toString('base64url');
const CREDENTIAL_ID = 'app_primary';
@@ -86,6 +87,53 @@ test('derives a domain-separated HMAC digest and user assurance', async () => {
assert.equal(principal.assurance, 'single_factor');
});
test('authenticates overlap generations by exact stored key id without fallback', async () => {
const keyring = {
schemaVersion: 1,
activePepperKeyId: 'rotation-2026-08',
keys: [
{ pepperKeyId: 'legacy-v1', pepper: PEPPER },
{ pepperKeyId: 'rotation-2026-08', pepper: NEXT_PEPPER },
],
};
const records = new Map([
[
'legacy',
credential({
credentialId: 'legacy',
secretDigest: apiCredentialSecretDigest(PEPPER, 'legacy', SECRET),
}),
],
[
'next',
credential({
credentialId: 'next',
pepperKeyId: 'rotation-2026-08',
secretDigest: apiCredentialSecretDigest(
NEXT_PEPPER,
'next',
SECRET,
),
}),
],
['unknown', credential({ credentialId: 'unknown', pepperKeyId: 'missing' })],
]);
const verifier = createClusterControlApiCredentialAuthenticator(
{ async resolve(credentialId) { return records.get(credentialId) ?? null; } },
keyring,
{ now: () => NOW },
);
const request = (credentialId) =>
metadata(`Bearer ql3c_${credentialId}_${SECRET}`);
assert.equal((await verifier.authenticate(request('legacy'))).subject.id, 'app_primary');
assert.equal((await verifier.authenticate(request('next'))).subject.id, 'app_primary');
await assert.rejects(
verifier.authenticate(request('unknown')),
ClusterControlApiCredentialUnavailableError,
);
});
test('rejects missing, malformed, wrong, inactive and disabled credentials', async () => {
let repositoryCalls = 0;
const strict = createClusterControlApiCredentialAuthenticator(
@@ -1,4 +1,6 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
@@ -112,7 +114,16 @@ test('builds an exact runtime-only TLS-verified Pool configuration', async () =>
},
});
assert.deepEqual(config.security, {
apiCredentialPepper: BASE_ENV.QL3_API_CREDENTIAL_PEPPER,
apiCredentialPepperKeyring: {
schemaVersion: 1,
activePepperKeyId: 'legacy-v1',
keys: [
{
pepperKeyId: 'legacy-v1',
pepper: BASE_ENV.QL3_API_CREDENTIAL_PEPPER,
},
],
},
});
assert.deepEqual(config.logRetention, {
enabled: true,
@@ -133,6 +144,50 @@ test('builds an exact runtime-only TLS-verified Pool configuration', async () =>
await database.close();
});
test('loads an exact private dual-generation pepper keyring file', (context) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-keyring-'));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
fs.chmodSync(directory, 0o700);
const keyringFile = path.join(directory, 'api-credential-keyring.json');
const nextPepper = Buffer.alloc(32, 2).toString('base64url');
fs.writeFileSync(
keyringFile,
JSON.stringify({
schemaVersion: 1,
activePepperKeyId: 'rotation-2026-08',
keys: [
{ pepperKeyId: 'legacy-v1', pepper: 'A'.repeat(43) },
{ pepperKeyId: 'rotation-2026-08', pepper: nextPepper },
],
}),
{ mode: 0o600 },
);
const environment = {
...BASE_ENV,
QL3_API_CREDENTIAL_PEPPER: undefined,
QL3_API_CREDENTIAL_PEPPER_KEYRING_FILE: keyringFile,
};
assert.equal(
loadClusterControlConfig(environment).security.apiCredentialPepperKeyring
.activePepperKeyId,
'rotation-2026-08',
);
assert.throws(
() =>
loadClusterControlConfig({
...environment,
QL3_API_CREDENTIAL_PEPPER: 'A'.repeat(43),
}),
/exactly one API credential pepper source/,
);
fs.chmodSync(keyringFile, 0o622);
assert.throws(
() => loadClusterControlConfig(environment),
/file authority is invalid/,
);
});
test('loads discrete operator-managed runtime credentials without a DSN copy', () => {
const {
QL3_POSTGRES_RUNTIME_URL: _connectionString,