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/ai",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 optional bounded model gateway and remote providers",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/cluster-admin",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 cluster operations and bounded Copilot MCP/Console surfaces",
"license": "Apache-2.0",
@@ -3,6 +3,8 @@ import type {
PostgresDatabaseResource,
} from '@qinglong/runtime-core';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import { createSingletonApiCredentialPepperKeyring } from '@qinglong/runtime-core/api-credential-pepper-keyring';
import { LEGACY_API_CREDENTIAL_PEPPER_KEY_ID } from '@qinglong/runtime-core/api-credential';
import { assertWorkerCredentialPepper } from '@qinglong/runtime-core/worker-credential-token';
import type { SecurityAuditQueryRepository } from '@qinglong/runtime-core/security-audit-query';
import {
@@ -97,7 +99,10 @@ export async function bootstrapClusterAdmin(
administration: createClusterAdministrationService(
identities,
credentials,
options.apiCredentialPepper,
createSingletonApiCredentialPepperKeyring(
options.apiCredentialPepper,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
).keys[0]!,
{
...(options.now ? { now: options.now } : {}),
...(options.randomBytes ? { randomBytes: options.randomBytes } : {}),
@@ -13,9 +13,11 @@ import {
formatApiCredentialToken,
} from '@qinglong/runtime-core/api-credential-token';
import {
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
assertApiCredentialPepperKeyId,
type ApiCredentialRecord,
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
} from '@qinglong/runtime-core/api-credential';
import type { ApiCredentialPepperKey } from '@qinglong/runtime-core/api-credential-pepper-keyring';
import {
type AppendIdentitySubjectResult,
IdentityAdministrationMutationConflictError,
@@ -283,7 +285,7 @@ function sameCredentialReplay(
export function createClusterAdministrationService(
identities: IdentityAdministrationRepository,
credentials: ApiCredentialAdministrationRepository,
pepper: string,
activePepperKeyValue: Readonly<ApiCredentialPepperKey> | string,
options: ClusterAdministrationOptions = {},
): ClusterAdministrationService {
if (
@@ -305,10 +307,28 @@ export function createClusterAdministrationService(
'credential repository is invalid',
);
}
const activePepperKey =
typeof activePepperKeyValue === 'string'
? Object.freeze({
pepperKeyId: LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
pepper: activePepperKeyValue,
})
: activePepperKeyValue;
try {
assertApiCredentialPepper(pepper);
if (
!activePepperKey ||
typeof activePepperKey !== 'object' ||
Array.isArray(activePepperKey) ||
Object.keys(activePepperKey).sort().join(',') !== 'pepper,pepperKeyId'
) {
throw new TypeError();
}
assertApiCredentialPepper(activePepperKey.pepper);
assertApiCredentialPepperKeyId(activePepperKey.pepperKeyId);
} catch {
throw new ClusterAdministrationConfigurationError('pepper is invalid');
throw new ClusterAdministrationConfigurationError(
'active pepper key is invalid',
);
}
exactObject(options, 'options');
const optionKeys = Object.keys(options);
@@ -442,7 +462,7 @@ export function createClusterAdministrationService(
try {
secretBase64Url = secret.toString('base64url');
secretDigest = apiCredentialSecretDigest(
pepper,
activePepperKey.pepper,
request.credentialId,
secretBase64Url,
);
@@ -454,7 +474,7 @@ export function createClusterAdministrationService(
const credential: ApiCredentialRecord = {
credentialId: request.credentialId,
version: request.expectedCurrentVersion + 1,
pepperKeyId: LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
pepperKeyId: activePepperKey.pepperKeyId,
state: operation === 'revoke' ? 'revoked' : 'active',
subject: request.subject,
subjectStatus: identity.status,
@@ -6,7 +6,7 @@ import {
} from './clusterAdministrationCommand';
const USAGE =
'Usage: ql3-security-admin --command=/absolute/command.json --assertion=/absolute/assertion.jwt --keyset=/absolute/keyset.json --pepper=/absolute/pepper [--delivery=/absolute/token.json]';
'Usage: ql3-security-admin --command=/absolute/command.json --assertion=/absolute/assertion.jwt --keyset=/absolute/keyset.json (--pepper=/absolute/pepper | --pepper-keyring=/absolute/keyring.json) [--delivery=/absolute/token.json]';
function argumentsFrom(argv: readonly string[]) {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
@@ -14,9 +14,10 @@ function argumentsFrom(argv: readonly string[]) {
}
const values = new Map<string, string>();
for (const argument of argv) {
const match = /^--(command|assertion|keyset|pepper|delivery)=(\/.+)$/.exec(
argument,
);
const match =
/^--(command|assertion|keyset|pepper|pepper-keyring|delivery)=(\/.+)$/.exec(
argument,
);
if (!match || values.has(match[1]!)) {
throw new ClusterAdministrationCommandError('CLI arguments are invalid');
}
@@ -26,7 +27,7 @@ function argumentsFrom(argv: readonly string[]) {
!values.has('command') ||
!values.has('assertion') ||
!values.has('keyset') ||
!values.has('pepper')
values.has('pepper') === values.has('pepper-keyring')
) {
throw new ClusterAdministrationCommandError('CLI arguments are invalid');
}
@@ -36,7 +37,9 @@ function argumentsFrom(argv: readonly string[]) {
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
keysetFile: values.get('keyset')!,
pepperFile: values.get('pepper')!,
...(values.has('pepper')
? { pepperFile: values.get('pepper')! }
: { pepperKeyringFile: values.get('pepper-keyring')! }),
...(values.has('delivery')
? { deliveryFile: values.get('delivery')! }
: {}),
@@ -1,7 +1,17 @@
import { createHash } from 'node:crypto';
import { basename } from 'node:path';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
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 {
normalizeApiCredentialPepperReferenceKeyId,
normalizeApiCredentialPepperReferenceLimit,
type ApiCredentialPepperReferenceRepository,
} from '@qinglong/runtime-core/api-credential-pepper-reference';
import { normalizeIdentityAdministrationSubject } from '@qinglong/runtime-core/identity-administration';
import {
normalizeSecurityAuditQuery,
@@ -34,6 +44,7 @@ const MAX_VERSION = 2_147_483_646;
const MAX_COMMAND_BYTES = 64 * 1024;
const MAX_ASSERTION_BYTES = 16 * 1024;
const MAX_PEPPER_BYTES = 256;
const MAX_PEPPER_KEYRING_BYTES = 2 * 1024;
export type ClusterAdministrationCommandOperation =
| 'identity.register'
@@ -42,6 +53,7 @@ export type ClusterAdministrationCommandOperation =
| 'credential.issue'
| 'credential.rotate'
| 'credential.revoke'
| 'pepper.references'
| 'audit.list';
interface BaseMutationRequest {
@@ -79,16 +91,27 @@ interface AuditCommand {
readonly request: SecurityAuditQuery;
}
interface PepperReferenceCommand {
readonly schemaVersion: 1;
readonly operation: 'pepper.references';
readonly request: Readonly<{
readonly pepperKeyId: string;
readonly limit: number;
}>;
}
export type ClusterAdministrationCommand =
| IdentityCommand
| CredentialCommand
| PepperReferenceCommand
| AuditCommand;
export interface ClusterAdministrationCommandPaths {
readonly commandFile: string;
readonly assertionFile: string;
readonly keysetFile: string;
readonly pepperFile: string;
readonly pepperFile?: string;
readonly pepperKeyringFile?: string;
readonly deliveryFile?: string;
}
@@ -115,18 +138,27 @@ export type ClusterAdministrationCommandResult =
schemaVersion: 1;
operation: 'audit.list';
page: Readonly<SecurityAuditQueryPage>;
}>
| Readonly<{
schemaVersion: 1;
operation: 'pepper.references';
pepperKeyId: string;
observedAtMs: number;
credentialIds: readonly string[];
hasMore: boolean;
}>;
export interface ClusterAdministrationCommandAuthority {
readonly administration: ClusterAdministrationService;
readonly audit: SecurityAuditQueryRepository;
readonly pepperReferences: ApiCredentialPepperReferenceRepository;
close(): Promise<void>;
}
export interface ClusterAdministrationCommandDependencies {
readonly openAuthority: (
environment: Readonly<Record<string, string | undefined>>,
pepper: string,
pepperKeyring: Readonly<ApiCredentialPepperKeyring>,
) => Promise<Readonly<ClusterAdministrationCommandAuthority>>;
readonly authenticate: (
keysetFile: string,
@@ -243,6 +275,7 @@ export function normalizeClusterAdministrationCommand(
'credential.issue',
'credential.rotate',
'credential.revoke',
'pepper.references',
'audit.list',
];
if (
@@ -257,6 +290,28 @@ export function normalizeClusterAdministrationCommand(
);
}
const operation = value.operation as ClusterAdministrationCommandOperation;
if (operation === 'pepper.references') {
exactObject(value.request, ['limit', 'pepperKeyId'], 'pepper reference');
try {
return Object.freeze({
schemaVersion: 1 as const,
operation,
request: Object.freeze({
pepperKeyId: normalizeApiCredentialPepperReferenceKeyId(
value.request.pepperKeyId as string,
),
limit: normalizeApiCredentialPepperReferenceLimit(
value.request.limit as number,
),
}),
});
} catch (error) {
throw new ClusterAdministrationCommandError(
'pepper reference query is invalid',
error,
);
}
}
if (operation === 'audit.list') {
let request: Readonly<SecurityAuditQuery>;
try {
@@ -370,9 +425,11 @@ export function createClusterAdministrationCommandRunner(
MAX_ASSERTION_BYTES,
true,
);
const pepperBytes = dependencies.readFile(
paths.pepperFile,
MAX_PEPPER_BYTES,
const pepperKeyringBytes = dependencies.readFile(
paths.pepperKeyringFile ?? paths.pepperFile!,
paths.pepperKeyringFile === undefined
? MAX_PEPPER_BYTES
: MAX_PEPPER_KEYRING_BYTES,
true,
);
let authority:
@@ -380,13 +437,46 @@ export function createClusterAdministrationCommandRunner(
| undefined;
try {
const assertion = strictUtf8(assertionBytes, 'assertion file').trim();
const pepper = strictUtf8(pepperBytes, 'pepper file').trim();
assertApiCredentialPepper(pepper);
let pepperKeyring: Readonly<ApiCredentialPepperKeyring>;
if (paths.pepperKeyringFile === undefined) {
pepperKeyring = createSingletonApiCredentialPepperKeyring(
strictUtf8(pepperKeyringBytes, 'pepper file').trim(),
LEGACY_API_CREDENTIAL_PEPPER_KEY_ID,
);
} else {
try {
pepperKeyring = normalizeApiCredentialPepperKeyring(
JSON.parse(
strictUtf8(pepperKeyringBytes, 'pepper keyring file'),
),
);
} catch (error) {
throw new ClusterAdministrationCommandError(
'pepper keyring file is invalid',
error,
);
}
}
const principal = await dependencies.authenticate(
paths.keysetFile,
assertion,
);
authority = await dependencies.openAuthority(environment, pepper);
authority = await dependencies.openAuthority(
environment,
pepperKeyring,
);
if (command.operation === 'pepper.references') {
void principal;
const inspection = await authority.pepperReferences.inspect(
command.request.pepperKeyId,
command.request.limit,
);
return Object.freeze({
schemaVersion: 1 as const,
operation: command.operation,
...inspection,
});
}
if (command.operation === 'audit.list') {
// Successful verification is the short-lived admin admission. Audit
// queries remain read-only and use the repository's bounded contract.
@@ -460,7 +550,7 @@ export function createClusterAdministrationCommandRunner(
});
} finally {
assertionBytes.fill(0);
pepperBytes.fill(0);
pepperKeyringBytes.fill(0);
await authority?.close();
}
},
@@ -20,9 +20,13 @@ import {
resolve,
} from 'node:path';
import { assertApiCredentialPepper } from '@qinglong/runtime-core/api-credential-token';
import {
activeApiCredentialPepperKey,
type ApiCredentialPepperKeyring,
} from '@qinglong/runtime-core/api-credential-pepper-keyring';
import {
PostgresApiCredentialAdministrationRepository,
PostgresApiCredentialPepperReferenceRepository,
PostgresIdentityAdministrationRepository,
PostgresSecurityAuditQueryRepository,
assertPostgresAdminSchemaReady,
@@ -249,9 +253,9 @@ function defaultDatabaseOpener(
async function openDefaultAuthority(
environment: Readonly<Record<string, string | undefined>>,
pepper: string,
pepperKeyring: Readonly<ApiCredentialPepperKeyring>,
): Promise<Readonly<ClusterAdministrationCommandAuthority>> {
assertApiCredentialPepper(pepper);
const activePepperKey = activeApiCredentialPepperKey(pepperKeyring);
const database = await defaultDatabaseOpener(environment)();
let closePromise: Promise<void> | undefined;
const close = (): Promise<void> => {
@@ -264,9 +268,12 @@ async function openDefaultAuthority(
administration: createClusterAdministrationService(
new PostgresIdentityAdministrationRepository(database.pool),
new PostgresApiCredentialAdministrationRepository(database.pool),
pepper,
activePepperKey,
),
audit: new PostgresSecurityAuditQueryRepository(database.pool),
pepperReferences: new PostgresApiCredentialPepperReferenceRepository(
database.pool,
),
close,
});
} catch (error) {
@@ -388,11 +395,20 @@ export function normalizeClusterAdministrationCommandPaths(
'command paths must be an object',
);
}
const pepperPathKeys = [
value.pepperFile === undefined ? null : 'pepperFile',
value.pepperKeyringFile === undefined ? null : 'pepperKeyringFile',
].filter((key): key is string => key !== null);
if (pepperPathKeys.length !== 1) {
throw new ClusterAdministrationCommandError(
'exactly one pepper source is required',
);
}
const expected = [
'assertionFile',
'commandFile',
'keysetFile',
'pepperFile',
pepperPathKeys[0]!,
...(requiresDelivery ? ['deliveryFile'] : []),
].sort();
const actual = Object.keys(value).sort();
@@ -417,10 +433,19 @@ export function normalizeClusterAdministrationCommandPaths(
value.keysetFile,
'identity keyset file',
),
pepperFile: boundedClusterAdministrationFile(
value.pepperFile,
'pepper file',
),
...(value.pepperFile === undefined
? {
pepperKeyringFile: boundedClusterAdministrationFile(
value.pepperKeyringFile,
'pepper keyring file',
),
}
: {
pepperFile: boundedClusterAdministrationFile(
value.pepperFile,
'pepper file',
),
}),
...(requiresDelivery
? {
deliveryFile: boundedClusterAdministrationFile(
@@ -441,11 +466,18 @@ export function clusterAdministrationCommandFileBeforeAdmission(
);
}
const candidate = value as Record<string, unknown>;
const required = ['assertionFile', 'commandFile', 'keysetFile', 'pepperFile'];
const required = ['assertionFile', 'commandFile', 'keysetFile'];
const pepperSources = ['pepperFile', 'pepperKeyringFile'].filter((key) =>
Object.hasOwn(candidate, key),
);
if (
required.some((key) => !Object.hasOwn(candidate, key)) ||
pepperSources.length !== 1 ||
Object.keys(candidate).some(
(key) => !required.includes(key) && key !== 'deliveryFile',
(key) =>
!required.includes(key) &&
!pepperSources.includes(key) &&
key !== 'deliveryFile',
)
) {
throw new ClusterAdministrationCommandError(
@@ -122,6 +122,29 @@ test('issues one token, stores only its digest and clears mutable secret bytes',
);
});
test('binds newly issued credentials to the selected active pepper key', async () => {
const repos = repositories();
const nextPepper = Buffer.alloc(32, 2).toString('base64url');
const generated = Buffer.alloc(32, 9);
const service = createClusterAdministrationService(
repos.identities,
repos.credentials,
{ pepperKeyId: 'rotation-2026-08', pepper: nextPepper },
{ now: () => NOW, randomBytes: () => generated },
);
const result = await service.issueCredential(request());
const secret = result.token.split('_').at(-1);
assert.equal(
repos.credentialCommands[0].credential.pepperKeyId,
'rotation-2026-08',
);
assert.equal(
repos.credentialCommands[0].credential.secretDigest,
apiCredentialSecretDigest(nextPepper, 'credential_primary', secret),
);
});
test('semantic mutation replay returns no token and does not generate a new secret', async () => {
const repos = repositories();
let randomCalls = 0;
@@ -114,6 +114,17 @@ function authority(overrides = {}) {
return { records: [], nextCursor: null };
},
},
pepperReferences: {
async inspect(pepperKeyId, limit) {
calls.push(['pepper.references', { pepperKeyId, limit }]);
return {
pepperKeyId,
observedAtMs: 1_000,
credentialIds: ['automation-primary'],
hasMore: false,
};
},
},
async close() {
closes += 1;
},
@@ -175,7 +186,11 @@ test('executes one strongly authenticated identity mutation and closes authority
assertion: 'signed.assertion.value',
},
]);
assert.equal(execution.opens[0].pepper, 'A'.repeat(43));
assert.deepEqual(execution.opens[0].pepper, {
schemaVersion: 1,
activePepperKeyId: 'legacy-v1',
keys: [{ pepperKeyId: 'legacy-v1', pepper: 'A'.repeat(43) }],
});
assert.equal(target.calls[0][1].principal, PRINCIPAL);
assert.equal(target.closes(), 1);
assert.equal(
@@ -312,6 +327,73 @@ test('keeps audit query bounded and rejects widened command shapes before admiss
);
});
test('loads a dual-generation keyring and exposes bounded retirement references', async () => {
const target = authority();
const nextPepper = Buffer.alloc(32, 2).toString('base64url');
const command = {
schemaVersion: 1,
operation: 'pepper.references',
request: { pepperKeyId: 'legacy-v1', limit: 32 },
};
const paths = {
commandFile: PATHS.commandFile,
assertionFile: PATHS.assertionFile,
keysetFile: PATHS.keysetFile,
pepperKeyringFile: '/private/pepper-keyring.json',
};
const buffers = [];
const opens = [];
const files = new Map([
[paths.commandFile, JSON.stringify(command)],
[paths.assertionFile, 'signed.assertion.value'],
[
paths.pepperKeyringFile,
JSON.stringify({
schemaVersion: 1,
activePepperKeyId: 'rotation-2026-08',
keys: [
{ pepperKeyId: 'legacy-v1', pepper: 'A'.repeat(43) },
{ pepperKeyId: 'rotation-2026-08', pepper: nextPepper },
],
}),
],
]);
const instance = createClusterAdministrationCommandRunner({
async openAuthority(environment, keyring) {
opens.push({ environment, keyring });
return target.value;
},
async authenticate() {
return PRINCIPAL;
},
readFile(filePath) {
const buffer = Buffer.from(files.get(filePath));
buffers.push(buffer);
return buffer;
},
publishDelivery() {
throw new Error('unexpected delivery');
},
});
assert.deepEqual(await instance.run(paths, {}), {
schemaVersion: 1,
operation: 'pepper.references',
pepperKeyId: 'legacy-v1',
observedAtMs: 1_000,
credentialIds: ['automation-primary'],
hasMore: false,
});
assert.equal(opens[0].keyring.activePepperKeyId, 'rotation-2026-08');
assert.equal(opens[0].keyring.keys.length, 2);
assert.equal(target.calls[0][0], 'pepper.references');
assert.equal(target.closes(), 1);
assert.equal(
buffers.every((buffer) => buffer.every((byte) => byte === 0)),
true,
);
});
test('rejects widened path authority before reading a command file', async () => {
let reads = 0;
const instance = createClusterAdministrationCommandRunner({
+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,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/cluster-postgres",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 cluster-only PostgreSQL driver and schema package",
"license": "Apache-2.0",
@@ -33,6 +33,7 @@ export {
} from '../connection/certificateAuthority';
export * from '../security/identityAdministrationRepository';
export * from '../security/apiCredentialAdministrationRepository';
export * from '../security/apiCredentialPepperReferenceRepository';
export * from '../security/securityAuditQueryRepository';
export * from '../worker-credential/workerCredentialAdministrationRepository';
export * from '../automation/automationAdministrationRepository';
@@ -0,0 +1,118 @@
import {
ApiCredentialPepperReferenceUnavailableError,
normalizeApiCredentialPepperReferenceKeyId,
normalizeApiCredentialPepperReferenceLimit,
type ApiCredentialPepperReferenceInspection,
type ApiCredentialPepperReferenceRepository,
} from '@qinglong/runtime-core/api-credential-pepper-reference';
import type { PostgresPool } from '@qinglong/runtime-core';
import { assertApiCredentialId } from '@qinglong/runtime-core/api-credential';
interface ReferenceRow extends Record<string, unknown> {
observedAtMs: unknown;
credentialId: unknown;
}
const INSPECT_SQL = `
WITH clock AS (
SELECT floor(
extract(epoch FROM statement_timestamp()) * 1000
)::bigint AS observed_at_ms
), current_references AS (
SELECT credential.credential_id
FROM "ql3"."api_credentials" AS credential
CROSS JOIN clock
WHERE credential.pepper_key_id = $1
AND credential.state = 'active'
AND credential.expires_at_ms > clock.observed_at_ms
AND NOT EXISTS (
SELECT 1
FROM "ql3"."api_credentials" AS newer
WHERE newer.credential_id = credential.credential_id
AND newer.version > credential.version
)
ORDER BY credential.credential_id
LIMIT $2
)
SELECT
clock.observed_at_ms AS "observedAtMs",
reference.credential_id AS "credentialId"
FROM clock
LEFT JOIN current_references AS reference ON true
ORDER BY reference.credential_id
`.trim();
function safeInteger(value: unknown): number {
const parsed = typeof value === 'string' ? Number(value) : value;
if (!Number.isSafeInteger(parsed) || (parsed as number) < 0) {
throw new ApiCredentialPepperReferenceUnavailableError();
}
return parsed as number;
}
export class PostgresApiCredentialPepperReferenceRepository
implements ApiCredentialPepperReferenceRepository
{
constructor(private readonly pool: Pick<PostgresPool, 'query'>) {
if (!pool || typeof pool.query !== 'function') {
throw new TypeError(
'PostgreSQL API credential pepper reference pool is invalid',
);
}
}
async inspect(
requestedPepperKeyId: string,
requestedLimit?: number,
): Promise<Readonly<ApiCredentialPepperReferenceInspection>> {
const pepperKeyId = normalizeApiCredentialPepperReferenceKeyId(
requestedPepperKeyId,
);
const limit = normalizeApiCredentialPepperReferenceLimit(requestedLimit);
try {
const result = await this.pool.query<ReferenceRow>(INSPECT_SQL, [
pepperKeyId,
limit + 1,
]);
if (result.rows.length < 1) {
throw new ApiCredentialPepperReferenceUnavailableError();
}
const observedAtMs = safeInteger(result.rows[0]!.observedAtMs);
const emptyReferenceSet =
result.rows.length === 1 && result.rows[0]!.credentialId === null;
const allCredentialIds = emptyReferenceSet
? []
: result.rows.map((row) => {
if (typeof row.credentialId !== 'string') {
throw new ApiCredentialPepperReferenceUnavailableError();
}
try {
assertApiCredentialId(row.credentialId);
} catch {
throw new ApiCredentialPepperReferenceUnavailableError();
}
return row.credentialId;
});
const credentialIds = allCredentialIds.slice(0, limit);
if (
result.rows.some(
(row) => safeInteger(row.observedAtMs) !== observedAtMs,
) ||
new Set(credentialIds).size !== credentialIds.length
) {
throw new ApiCredentialPepperReferenceUnavailableError();
}
return Object.freeze({
pepperKeyId,
observedAtMs,
credentialIds: Object.freeze(credentialIds),
hasMore: allCredentialIds.length > limit,
});
} catch (error) {
if (error instanceof ApiCredentialPepperReferenceUnavailableError) {
throw error;
}
throw new ApiCredentialPepperReferenceUnavailableError();
}
}
}
@@ -0,0 +1,75 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const {
PostgresApiCredentialPepperReferenceRepository,
} = require('../dist/security/apiCredentialPepperReferenceRepository.js');
test('returns bounded current pepper references using database time', async () => {
const calls = [];
const repository = new PostgresApiCredentialPepperReferenceRepository({
async query(sql, parameters) {
calls.push({ sql, parameters });
return {
rows: [
{ observedAtMs: '1000', credentialId: 'credential-a' },
{ observedAtMs: '1000', credentialId: 'credential-b' },
{ observedAtMs: '1000', credentialId: 'credential-c' },
],
};
},
});
assert.deepEqual(await repository.inspect('legacy-v1', 2), {
pepperKeyId: 'legacy-v1',
observedAtMs: 1000,
credentialIds: ['credential-a', 'credential-b'],
hasMore: true,
});
assert.deepEqual(calls[0].parameters, ['legacy-v1', 3]);
assert.match(calls[0].sql, /statement_timestamp\(\)/);
assert.match(calls[0].sql, /newer\.version > credential\.version/);
});
test('represents an empty reference set without losing database time', async () => {
const repository = new PostgresApiCredentialPepperReferenceRepository({
async query() {
return { rows: [{ observedAtMs: 1000, credentialId: null }] };
},
});
assert.deepEqual(await repository.inspect('retired-v1'), {
pepperKeyId: 'retired-v1',
observedAtMs: 1000,
credentialIds: [],
hasMore: false,
});
});
test('fails closed on invalid input and malformed database rows', async () => {
const repository = new PostgresApiCredentialPepperReferenceRepository({
async query() {
return { rows: [{ observedAtMs: 'invalid', credentialId: null }] };
},
});
await assert.rejects(() => repository.inspect('legacy-v1'), /unavailable/);
await assert.rejects(() => repository.inspect('legacy-v1', 65));
await assert.rejects(() => repository.inspect('bad key'));
for (const credentialId of [42, null, 'bad credential']) {
const malformedRepository =
new PostgresApiCredentialPepperReferenceRepository({
async query() {
return {
rows: [
{ observedAtMs: 1000, credentialId: 'credential-a' },
{ observedAtMs: 1000, credentialId },
],
};
},
});
await assert.rejects(
() => malformedRepository.inspect('legacy-v1'),
/unavailable/,
);
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-admin",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 short-lived local database adoption authority",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-api",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 optional single-process authenticated Local HTTP API",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-application",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 local application activation composition root",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-command-file",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 bounded private local file authorities",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-execution",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 local execution, control, recovery and dispatch runtime",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-mcp-server",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 optional authenticated local MCP stdio server",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-owner-cli",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 short-lived Local Owner product command-file CLI",
"license": "Apache-2.0",
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-owner-console",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 short-lived POSIX local Owner console and internal ceremonies",
"license": "Apache-2.0",
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-owner-maintenance",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 short-lived Local Owner maintenance authorities and GC command adapter",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-process",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 durable local process launcher and receipt lifecycle",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-secret",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 encrypted local SecretStore and private keyring",
"license": "Apache-2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/local-sqlite",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 Node 24 SQLite storage adapter for local Profiles",
"license": "Apache-2.0",
+11 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/runtime-core",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 profile-neutral runtime contracts",
"license": "Apache-2.0",
@@ -961,6 +961,16 @@
"require": "./dist/security/identity-credential/apiCredentialToken.js",
"default": "./dist/security/identity-credential/apiCredentialToken.js"
},
"./api-credential-pepper-keyring": {
"types": "./dist/security/identity-credential/apiCredentialPepperKeyring.d.ts",
"require": "./dist/security/identity-credential/apiCredentialPepperKeyring.js",
"default": "./dist/security/identity-credential/apiCredentialPepperKeyring.js"
},
"./api-credential-pepper-reference": {
"types": "./dist/security/identity-credential/apiCredentialPepperReference.d.ts",
"require": "./dist/security/identity-credential/apiCredentialPepperReference.js",
"default": "./dist/security/identity-credential/apiCredentialPepperReference.js"
},
"./identity-administration": {
"types": "./dist/security/identity-credential/identityAdministration.d.ts",
"require": "./dist/security/identity-credential/identityAdministration.js",
@@ -0,0 +1,139 @@
import { assertApiCredentialPepperKeyId } from './apiCredential';
import { assertApiCredentialPepper } from './apiCredentialToken';
export const MAX_API_CREDENTIAL_PEPPER_KEYS = 2;
export interface ApiCredentialPepperKey {
readonly pepperKeyId: string;
readonly pepper: string;
}
export interface ApiCredentialPepperKeyring {
readonly schemaVersion: 1;
readonly activePepperKeyId: string;
readonly keys: readonly Readonly<ApiCredentialPepperKey>[];
}
export class ApiCredentialPepperKeyringConfigurationError extends TypeError {
constructor(message: string) {
super(`API credential pepper keyring is invalid: ${message}`);
this.name = 'ApiCredentialPepperKeyringConfigurationError';
}
}
function exactObject(
value: unknown,
expectedKeys: readonly string[],
label: string,
): asserts value is Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new ApiCredentialPepperKeyringConfigurationError(
`${label} must be an object`,
);
}
const actual = Object.keys(value).sort();
const expected = [...expectedKeys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new ApiCredentialPepperKeyringConfigurationError(
`${label} shape is invalid`,
);
}
}
function normalizeKey(value: unknown): Readonly<ApiCredentialPepperKey> {
exactObject(value, ['pepper', 'pepperKeyId'], 'key');
try {
assertApiCredentialPepperKeyId(value.pepperKeyId as string);
assertApiCredentialPepper(value.pepper as string);
} catch {
throw new ApiCredentialPepperKeyringConfigurationError(
'key material is invalid',
);
}
return Object.freeze({
pepperKeyId: value.pepperKeyId as string,
pepper: value.pepper as string,
});
}
export function normalizeApiCredentialPepperKeyring(
value: unknown,
): Readonly<ApiCredentialPepperKeyring> {
exactObject(
value,
['activePepperKeyId', 'keys', 'schemaVersion'],
'keyring',
);
if (
value.schemaVersion !== 1 ||
!Array.isArray(value.keys) ||
value.keys.length < 1 ||
value.keys.length > MAX_API_CREDENTIAL_PEPPER_KEYS
) {
throw new ApiCredentialPepperKeyringConfigurationError(
'version or key count is invalid',
);
}
let activePepperKeyId: string;
try {
assertApiCredentialPepperKeyId(value.activePepperKeyId as string);
activePepperKeyId = value.activePepperKeyId as string;
} catch {
throw new ApiCredentialPepperKeyringConfigurationError(
'activePepperKeyId is invalid',
);
}
const keys = value.keys.map(normalizeKey);
const keyIds = new Set(keys.map((key) => key.pepperKeyId));
if (keyIds.size !== keys.length || !keyIds.has(activePepperKeyId)) {
throw new ApiCredentialPepperKeyringConfigurationError(
'keys must be unique and contain the active key',
);
}
return Object.freeze({
schemaVersion: 1,
activePepperKeyId,
keys: Object.freeze(keys),
});
}
export function createSingletonApiCredentialPepperKeyring(
pepper: string,
pepperKeyId: string,
): Readonly<ApiCredentialPepperKeyring> {
return normalizeApiCredentialPepperKeyring({
schemaVersion: 1,
activePepperKeyId: pepperKeyId,
keys: [{ pepperKeyId, pepper }],
});
}
export function resolveApiCredentialPepperKey(
keyring: Readonly<ApiCredentialPepperKeyring>,
pepperKeyId: string,
): Readonly<ApiCredentialPepperKey> | null {
const normalized = normalizeApiCredentialPepperKeyring(keyring);
try {
assertApiCredentialPepperKeyId(pepperKeyId);
} catch {
throw new ApiCredentialPepperKeyringConfigurationError(
'pepperKeyId is invalid',
);
}
return (
normalized.keys.find((key) => key.pepperKeyId === pepperKeyId) ?? null
);
}
export function activeApiCredentialPepperKey(
keyring: Readonly<ApiCredentialPepperKeyring>,
): Readonly<ApiCredentialPepperKey> {
const normalized = normalizeApiCredentialPepperKeyring(keyring);
return resolveApiCredentialPepperKey(
normalized,
normalized.activePepperKeyId,
)!;
}
@@ -0,0 +1,47 @@
import { assertApiCredentialPepperKeyId } from './apiCredential';
export const MAX_API_CREDENTIAL_PEPPER_REFERENCES = 64;
export interface ApiCredentialPepperReferenceInspection {
readonly pepperKeyId: string;
readonly observedAtMs: number;
readonly credentialIds: readonly string[];
readonly hasMore: boolean;
}
export interface ApiCredentialPepperReferenceRepository {
inspect(
pepperKeyId: string,
limit?: number,
): Promise<Readonly<ApiCredentialPepperReferenceInspection>>;
}
export class ApiCredentialPepperReferenceUnavailableError extends Error {
readonly code = 'API_CREDENTIAL_PEPPER_REFERENCE_UNAVAILABLE';
constructor() {
super('API credential pepper references are unavailable');
this.name = 'ApiCredentialPepperReferenceUnavailableError';
}
}
export function normalizeApiCredentialPepperReferenceLimit(
value: number | undefined,
): number {
const resolved = value ?? MAX_API_CREDENTIAL_PEPPER_REFERENCES;
if (
!Number.isSafeInteger(resolved) ||
resolved < 1 ||
resolved > MAX_API_CREDENTIAL_PEPPER_REFERENCES
) {
throw new TypeError('API credential pepper reference limit is invalid');
}
return resolved;
}
export function normalizeApiCredentialPepperReferenceKeyId(
value: string,
): string {
assertApiCredentialPepperKeyId(value);
return value;
}
@@ -0,0 +1,81 @@
const assert = require('node:assert/strict');
const test = require('node:test');
const {
activeApiCredentialPepperKey,
createSingletonApiCredentialPepperKeyring,
normalizeApiCredentialPepperKeyring,
resolveApiCredentialPepperKey,
} = require('../dist/security/identity-credential/apiCredentialPepperKeyring.js');
const LEGACY = 'A'.repeat(43);
const NEXT = Buffer.alloc(32, 2).toString('base64url');
test('normalizes a bounded dual-generation keyring and resolves exact keys', () => {
const keyring = normalizeApiCredentialPepperKeyring({
schemaVersion: 1,
activePepperKeyId: 'rotation-2026-08',
keys: [
{ pepperKeyId: 'legacy-v1', pepper: LEGACY },
{ pepperKeyId: 'rotation-2026-08', pepper: NEXT },
],
});
assert.deepEqual(activeApiCredentialPepperKey(keyring), {
pepperKeyId: 'rotation-2026-08',
pepper: NEXT,
});
assert.deepEqual(resolveApiCredentialPepperKey(keyring, 'legacy-v1'), {
pepperKeyId: 'legacy-v1',
pepper: LEGACY,
});
assert.equal(resolveApiCredentialPepperKey(keyring, 'missing-v1'), null);
});
test('singleton compatibility is explicit and preserves its key id', () => {
assert.deepEqual(
createSingletonApiCredentialPepperKeyring(LEGACY, 'legacy-v1'),
{
schemaVersion: 1,
activePepperKeyId: 'legacy-v1',
keys: [{ pepperKeyId: 'legacy-v1', pepper: LEGACY }],
},
);
});
test('rejects widened, duplicate, missing-active, empty, and oversized keyrings', () => {
const valid = {
schemaVersion: 1,
activePepperKeyId: 'legacy-v1',
keys: [{ pepperKeyId: 'legacy-v1', pepper: LEGACY }],
};
for (const candidate of [
{ ...valid, extra: true },
{ ...valid, keys: [] },
{
...valid,
keys: [valid.keys[0], valid.keys[0]],
},
{ ...valid, activePepperKeyId: 'missing-v1' },
{
...valid,
keys: [
valid.keys[0],
{ pepperKeyId: 'next-v1', pepper: NEXT },
{
pepperKeyId: 'third-v1',
pepper: Buffer.alloc(32, 3).toString('base64url'),
},
],
},
{
...valid,
keys: [{ ...valid.keys[0], extra: true }],
},
]) {
assert.throws(
() => normalizeApiCredentialPepperKeyring(candidate),
/pepper keyring is invalid/,
);
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qinglong/worker-runtime",
"version": "3.0.0-alpha.0",
"version": "3.0.0-alpha.1",
"private": true,
"description": "QingLong 3.0 headless Worker profile runtime",
"license": "Apache-2.0",