mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): cut alpha.1 candidate milestone
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user