feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,83 @@
// Cluster Plugin Package executor boundary; keep approved-action dispatch explicit.
import type { PostgresPool } from '@qinglong/runtime-core';
import {
ApprovedActionDispatcher,
type ApprovedActionDispatcherOptions,
} from '@qinglong/runtime-core/approved-action-dispatcher';
import { PluginPackageApprovedActionHandler } from '@qinglong/runtime-core/plugin-package-approved-action';
import { PostgresApprovedActionExecutionRepository } from '@qinglong/cluster-postgres/approved-action-execution';
import { PostgresPluginPackageInstallRepository } from '@qinglong/cluster-postgres/plugin-package-install';
import { PostgresPluginPackageInstallProposalRepository } from '@qinglong/cluster-postgres/plugin-package-proposal';
import {
PostgresPluginPackagePublisherRevocationProposalRepository,
PostgresPluginPackagePublisherTrustTransitionProposalRepository,
PostgresPluginPackagePublisherTrustTransitionRepository,
} from '@qinglong/cluster-postgres/package-executor';
import {
ClusterPluginPackagePublisherRevocationApprovedActionHandler,
type ClusterPluginPackagePublisherRevocationExecutionPort,
} from '../publisher/pluginPackagePublisherRevocationApprovedAction';
import {
ClusterPluginPackagePublisherTrustTransitionApprovedActionHandler,
type ClusterPluginPackagePublisherTrustTransitionExecutionPort,
} from '../publisher/pluginPackagePublisherTrustTransitionApprovedAction';
export const CLUSTER_PLUGIN_PACKAGE_DISPATCH_BATCH_LIMIT = 16;
export interface ClusterPluginPackageApprovedActionDispatcherOptions
extends Omit<ApprovedActionDispatcherOptions, 'defaultBatchSize'> {
readonly pool: PostgresPool;
readonly defaultBatchSize?: number;
readonly publisherRevocations?: ClusterPluginPackagePublisherRevocationExecutionPort;
readonly publisherTrustTransitions?: ClusterPluginPackagePublisherTrustTransitionExecutionPort;
}
export function createClusterPluginPackageApprovedActionDispatcher(
options: ClusterPluginPackageApprovedActionDispatcherOptions,
): ApprovedActionDispatcher {
if (!options || typeof options !== 'object') {
throw new TypeError('cluster Package Approved Action options are invalid');
}
const {
pool,
defaultBatchSize,
publisherRevocations,
publisherTrustTransitions,
...dispatcherOptions
} = options;
const executions = new PostgresApprovedActionExecutionRepository(pool);
const handler = new PluginPackageApprovedActionHandler(
new PostgresPluginPackageInstallProposalRepository(pool),
new PostgresPluginPackageInstallRepository(pool),
);
const handlers = [
handler,
...(['overlap_add', 'safe_retire'] as const).map(
(mode) =>
new ClusterPluginPackagePublisherTrustTransitionApprovedActionHandler(
mode,
new PostgresPluginPackagePublisherTrustTransitionProposalRepository(
pool,
),
publisherTrustTransitions ??
new PostgresPluginPackagePublisherTrustTransitionRepository(pool),
),
),
...(publisherRevocations
? [
new ClusterPluginPackagePublisherRevocationApprovedActionHandler(
new PostgresPluginPackagePublisherRevocationProposalRepository(
pool,
),
publisherRevocations,
),
]
: []),
];
return new ApprovedActionDispatcher(executions, handlers, {
...dispatcherOptions,
defaultBatchSize:
defaultBatchSize ?? CLUSTER_PLUGIN_PACKAGE_DISPATCH_BATCH_LIMIT,
});
}
@@ -0,0 +1,70 @@
#!/usr/bin/env node
// Cluster Plugin Package executor boundary; keep the operational CLI explicit.
import { runClusterPluginPackageExecutorProcess } from './pluginPackageExecutorProcess';
const USAGE = 'Usage: ql3-plugin-package-execute';
function emit(value: Readonly<Record<string, unknown>>): void {
process.stdout.write(`${JSON.stringify(value)}\n`);
}
async function run(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
if (argv.length !== 0) {
process.stderr.write(
`${JSON.stringify({
code: 'QL3_PLUGIN_PACKAGE_EXECUTOR_CLI_USAGE_INVALID',
message: USAGE,
})}\n`,
);
process.exitCode = 64;
return;
}
try {
const result = await runClusterPluginPackageExecutorProcess({
environment: process.env,
});
if (result.status === 'disabled') {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-executor',
event: 'executor_disabled',
});
return;
}
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-executor',
event: 'executor_completed',
databaseContractVersion: result.database.contractVersion,
databaseMigrationCount: result.database.migrationIds.length,
batches: result.batches,
});
} catch (error) {
const candidate = error as {
readonly name?: unknown;
readonly code?: unknown;
};
process.stderr.write(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-plugin-package-executor',
event: 'executor_failed',
name:
typeof candidate?.name === 'string'
? candidate.name.slice(0, 128)
: 'Error',
...(typeof candidate?.code === 'string'
? { code: candidate.code.slice(0, 128) }
: {}),
})}\n`,
);
process.exitCode = 1;
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,464 @@
// Cluster Plugin Package executor boundary; keep process composition explicit.
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
PostgresPool,
} from '@qinglong/runtime-core';
import type {
ApprovedActionDispatchBatchSummary,
ApprovedActionDispatcher,
} from '@qinglong/runtime-core/approved-action-dispatcher';
import {
assertPostgresPackageExecutorSchemaReady,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
type PostgresConnectionOptions,
type PostgresPoolOptions,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-executor';
import {
createClusterPluginPackageApprovedActionDispatcher,
type ClusterPluginPackageApprovedActionDispatcherOptions,
} from './pluginPackageApprovedAction';
import {
consumeClusterPluginPackagePublisherRevocationApprovals,
type ClusterPluginPackagePublisherRevocationApprovalSummary,
type ConsumeClusterPluginPackagePublisherRevocationApprovalsOptions,
} from '../publisher/pluginPackagePublisherRevocationApprovalConsumer';
import {
consumeClusterPluginPackagePublisherTrustTransitionApprovals,
type ClusterPluginPackagePublisherTrustTransitionApprovalSummary,
type ConsumeClusterPluginPackagePublisherTrustTransitionApprovalsOptions,
} from '../publisher/pluginPackagePublisherTrustTransitionApprovalConsumer';
import {
runClusterPluginPackagePublisherRevocation,
} from '../publisher/pluginPackagePublisherRevocation';
export type ClusterPluginPackageExecutorProcessEnvironment = Readonly<
Record<string, string | undefined>
>;
export type ClusterPluginPackageExecutorProcessConfig =
| Readonly<{ enabled: false }>
| Readonly<{
enabled: true;
owner: string;
approvalBatchSize: number;
dispatchBatchSize: number;
maxBatches: number;
leaseDurationMs: number;
revocationPageSize: number;
revocationMaxPages: number;
database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}>;
}>;
export interface ClusterPluginPackageExecutorBatchResult {
readonly approvals: Readonly<ClusterPluginPackagePublisherRevocationApprovalSummary>;
readonly trustTransitionApprovals: Readonly<ClusterPluginPackagePublisherTrustTransitionApprovalSummary>;
readonly dispatch: Readonly<ApprovedActionDispatchBatchSummary>;
}
export type ClusterPluginPackageExecutorProcessResult =
| Readonly<{ status: 'disabled' }>
| Readonly<{
status: 'completed';
database: PostgresSchemaReadinessReport;
batches: readonly Readonly<ClusterPluginPackageExecutorBatchResult>[];
}>;
export interface RunClusterPluginPackageExecutorProcessOptions {
readonly environment: ClusterPluginPackageExecutorProcessEnvironment;
readonly openDatabase?: OpenPostgresDatabase;
readonly consumeApprovals?: (
options: ConsumeClusterPluginPackagePublisherRevocationApprovalsOptions,
) => Promise<
Readonly<ClusterPluginPackagePublisherRevocationApprovalSummary>
>;
readonly consumeTrustTransitionApprovals?: (
options: ConsumeClusterPluginPackagePublisherTrustTransitionApprovalsOptions,
) => Promise<
Readonly<ClusterPluginPackagePublisherTrustTransitionApprovalSummary>
>;
readonly createDispatcher?: (
options: ClusterPluginPackageApprovedActionDispatcherOptions,
) => ApprovedActionDispatcher;
readonly now?: () => number;
}
export class ClusterPluginPackageExecutorProcessConfigError extends TypeError {
readonly code = 'QL3_PLUGIN_PACKAGE_EXECUTOR_PROCESS_CONFIG_INVALID';
constructor(message: string) {
super(
`Plugin Package executor process configuration is invalid: ${message}`,
);
this.name = 'ClusterPluginPackageExecutorProcessConfigError';
}
}
const SAFE_OWNER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function enabledValue(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): boolean {
const value = environment.QL3_PLUGIN_PACKAGE_EXECUTOR_ENABLED;
if (value === undefined || value === '' || value === 'false') return false;
if (value === 'true') return true;
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_PLUGIN_PACKAGE_EXECUTOR_ENABLED must be true or false',
);
}
function boundedValue(
environment: ClusterPluginPackageExecutorProcessEnvironment,
name: string,
maximumLength: number,
): string | undefined {
const value = environment[name];
if (value === undefined || value === '') return undefined;
if (value.length > maximumLength || /[\0\r\n]/.test(value)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
`${name} is invalid`,
);
}
return value;
}
function integerValue(
environment: ClusterPluginPackageExecutorProcessEnvironment,
name: string,
defaultValue: number,
minimum: number,
maximum: number,
): number {
const value = environment[name];
if (value === undefined || value === '') return defaultValue;
if (!/^\d+$/.test(value)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
`${name} must be an integer`,
);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
throw new ClusterPluginPackageExecutorProcessConfigError(
`${name} must be between ${minimum} and ${maximum}`,
);
}
return parsed;
}
function databaseConfig(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}> {
let connection: PostgresConnectionOptions;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_PACKAGE_EXECUTOR_URL',
host: 'QL3_POSTGRES_PACKAGE_EXECUTOR_HOST',
port: 'QL3_POSTGRES_PACKAGE_EXECUTOR_PORT',
database: 'QL3_POSTGRES_PACKAGE_EXECUTOR_DATABASE',
user: 'QL3_POSTGRES_PACKAGE_EXECUTOR_USER',
password: 'QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD',
});
} catch (error) {
throw new ClusterPluginPackageExecutorProcessConfigError(
error instanceof Error
? error.message
: 'PostgreSQL Package executor connection is invalid',
);
}
const tlsMode = environment.QL3_POSTGRES_TLS_MODE ?? 'verify-full';
let tls: PostgresConnectionOptions['tls'];
if (tlsMode === 'disable') {
if (environment.QL3_POSTGRES_ALLOW_INSECURE !== 'true') {
throw new ClusterPluginPackageExecutorProcessConfigError(
'disabling PostgreSQL TLS requires QL3_POSTGRES_ALLOW_INSECURE=true',
);
}
tls = Object.freeze({ mode: 'disable' });
} else if (tlsMode === 'verify-full') {
const servername = boundedValue(
environment,
'QL3_POSTGRES_TLS_SERVERNAME',
253,
);
if (!isPostgresTlsDnsServername(servername)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_POSTGRES_TLS_SERVERNAME must be an explicit DNS name for verify-full',
);
}
const caFile = boundedValue(
environment,
'QL3_POSTGRES_TLS_CA_FILE',
4096,
);
let ca: string | undefined;
if (caFile !== undefined) {
try {
ca = loadPostgresCertificateAuthorityFile(caFile);
} catch {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_POSTGRES_TLS_CA_FILE must contain a bounded trusted CA bundle',
);
}
}
tls = Object.freeze({
mode: 'verify-full',
servername,
...(ca === undefined ? {} : { ca }),
});
} else {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_POSTGRES_TLS_MODE must be verify-full or disable',
);
}
const applicationName =
boundedValue(environment, 'QL3_POSTGRES_APPLICATION_NAME', 63) ??
'qinglong3-plugin-package-executor';
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(applicationName)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_POSTGRES_APPLICATION_NAME is invalid',
);
}
return Object.freeze({
connection: Object.freeze({ ...connection, tls }),
pool: Object.freeze({
applicationName,
maxConnections: integerValue(
environment,
'QL3_POSTGRES_MAX_CONNECTIONS',
2,
1,
4,
),
connectionTimeoutMs: integerValue(
environment,
'QL3_POSTGRES_CONNECTION_TIMEOUT_MS',
5_000,
100,
60_000,
),
idleTimeoutMs: integerValue(
environment,
'QL3_POSTGRES_IDLE_TIMEOUT_MS',
10_000,
1_000,
300_000,
),
}),
});
}
export function loadClusterPluginPackageExecutorProcessConfig(
environment: ClusterPluginPackageExecutorProcessEnvironment,
): ClusterPluginPackageExecutorProcessConfig {
if (!environment || typeof environment !== 'object') {
throw new ClusterPluginPackageExecutorProcessConfigError(
'environment is required',
);
}
if (!enabledValue(environment)) return Object.freeze({ enabled: false });
const owner =
boundedValue(environment, 'QL3_PLUGIN_PACKAGE_EXECUTOR_OWNER', 128) ??
'cluster_package_executor_1';
if (!SAFE_OWNER.test(owner)) {
throw new ClusterPluginPackageExecutorProcessConfigError(
'QL3_PLUGIN_PACKAGE_EXECUTOR_OWNER is invalid',
);
}
return Object.freeze({
enabled: true,
owner,
approvalBatchSize: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_EXECUTOR_APPROVAL_BATCH_SIZE',
8,
1,
64,
),
dispatchBatchSize: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_EXECUTOR_DISPATCH_BATCH_SIZE',
8,
1,
64,
),
maxBatches: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_EXECUTOR_MAX_BATCHES',
4,
1,
64,
),
leaseDurationMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_EXECUTOR_LEASE_DURATION_MS',
600_000,
1,
600_000,
),
revocationPageSize: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_EXECUTOR_REVOCATION_PAGE_SIZE',
16,
1,
128,
),
revocationMaxPages: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_EXECUTOR_REVOCATION_MAX_PAGES',
16,
1,
64,
),
database: databaseConfig(environment),
});
}
function borrowedDatabase(
database: PostgresDatabaseResource,
): OpenPostgresDatabase {
return async () =>
Object.freeze({
pool: database.pool,
close: async () => undefined,
});
}
function isIdleBatch(
batch: Readonly<ClusterPluginPackageExecutorBatchResult>,
): boolean {
return (
batch.approvals.scanned === 0 &&
batch.trustTransitionApprovals.scanned === 0 &&
batch.dispatch.scanned === 0
);
}
export async function runClusterPluginPackageExecutorProcess(
options: RunClusterPluginPackageExecutorProcessOptions,
): Promise<ClusterPluginPackageExecutorProcessResult> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
!options.environment ||
(options.openDatabase !== undefined &&
typeof options.openDatabase !== 'function') ||
(options.consumeApprovals !== undefined &&
typeof options.consumeApprovals !== 'function') ||
(options.consumeTrustTransitionApprovals !== undefined &&
typeof options.consumeTrustTransitionApprovals !== 'function') ||
(options.createDispatcher !== undefined &&
typeof options.createDispatcher !== 'function') ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError('Plugin Package executor process options are invalid');
}
const config = loadClusterPluginPackageExecutorProcessConfig(
options.environment,
);
if (!config.enabled) return Object.freeze({ status: 'disabled' });
const openDatabase =
options.openDatabase ??
createPostgresDatabaseOpener({
role: 'package-executor',
connection: config.database.connection,
pool: config.database.pool,
onPoolError: () => undefined,
});
const database = await openDatabase();
let failure: unknown;
try {
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const dispatcherFactory =
options.createDispatcher ??
createClusterPluginPackageApprovedActionDispatcher;
const consumeApprovals =
options.consumeApprovals ??
consumeClusterPluginPackagePublisherRevocationApprovals;
const consumeTrustTransitionApprovals =
options.consumeTrustTransitionApprovals ??
consumeClusterPluginPackagePublisherTrustTransitionApprovals;
const dispatcher = dispatcherFactory({
pool: database.pool,
owner: config.owner,
leaseDurationMs: config.leaseDurationMs,
defaultBatchSize: config.dispatchBatchSize,
...(options.now ? { clock: options.now } : {}),
publisherRevocations: {
async run(receipt) {
const result = await runClusterPluginPackagePublisherRevocation({
openDatabase: borrowedDatabase(database),
receipt,
// Durable proposal, dispatch, Project Policy fence and trust-head
// generation are revalidated in the same SERIALIZABLE mutation.
confirmAuthorization: () => undefined,
pageSize: config.revocationPageSize,
maxPages: config.revocationMaxPages,
});
return Object.freeze({
safeToAdmit: result.safeToAdmit,
receiptDigest: result.receiptDigest,
impactDigest: result.impactDigest,
});
},
},
});
const batches: Readonly<ClusterPluginPackageExecutorBatchResult>[] = [];
for (let index = 0; index < config.maxBatches; index += 1) {
const approvals = await consumeApprovals({
pool: database.pool,
limit: config.approvalBatchSize,
...(options.now ? { now: options.now } : {}),
});
const trustTransitionApprovals =
await consumeTrustTransitionApprovals({
pool: database.pool,
limit: config.approvalBatchSize,
...(options.now ? { now: options.now } : {}),
});
const dispatch = await dispatcher.dispatchBatch({
limit: config.dispatchBatchSize,
});
const batch = Object.freeze({
approvals,
trustTransitionApprovals,
dispatch,
});
batches.push(batch);
if (isIdleBatch(batch)) break;
}
return Object.freeze({
status: 'completed',
database: evidence,
batches: Object.freeze([...batches]),
});
} catch (error) {
failure = error;
throw error;
} finally {
try {
await database.close();
} catch (closeError) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Plugin Package executor process failed and PostgreSQL did not close',
);
}
throw closeError;
}
}
}
@@ -0,0 +1,450 @@
// Cluster Plugin Package lifecycle boundary; keep execution authority explicit.
import {
PostgresPluginPackageLifecyclePlanRepository,
PostgresPluginPackageLifecycleRepository,
assertPostgresPackageExecutorSchemaReady,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-executor';
import { PostgresApprovalRequestRepository } from '@qinglong/cluster-postgres/approved-action';
import { PostgresProjectPolicyRepository } from '@qinglong/cluster-postgres/project-policy';
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
PostgresPool,
} from '@qinglong/runtime-core';
import {
normalizeApprovalRequestRecord,
type ApprovedActionBinding,
type ApprovedActionDispatchRecord,
} from '@qinglong/runtime-core/approved-action';
import {
createPluginPackageLifecycleEvent,
pluginPackageLifecycleActionDigest,
PluginPackageLifecycleConflictError,
type PluginPackageLifecycleAction,
type PluginPackageLifecycleReceipt,
} from '@qinglong/runtime-core/plugin-package-lifecycle';
import {
MAX_PLUGIN_PACKAGE_LIFECYCLE_PLAN_LIFETIME_MS,
PluginPackageLifecyclePlanConflictError,
createPluginPackageLifecyclePlan,
normalizePluginPackageLifecyclePlan,
type PluginPackageLifecyclePlan,
} from '@qinglong/runtime-core/plugin-package-lifecycle-plan';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const PACKAGE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const CLUSTER_LIFECYCLE_CONSUMER = Object.freeze({
subject: Object.freeze({
type: 'system' as const,
id: 'cluster_plugin_package_lifecycle_executor',
}),
authenticationId: 'cluster_plugin_package_lifecycle_executor_v1',
});
export interface RunClusterPluginPackageLifecyclePlanOptions {
readonly openDatabase: OpenPostgresDatabase;
readonly actionRef: string;
readonly action: PluginPackageLifecycleAction;
readonly projectId: string;
readonly packageName: string;
readonly requestedBy: SecuritySubject;
readonly confirmAuthorization: () => void | Promise<void>;
readonly lifetimeMs?: number;
}
export interface ClusterPluginPackageLifecyclePlanRun {
readonly database: PostgresSchemaReadinessReport;
readonly status: 'created' | 'existing';
readonly plan: Readonly<PluginPackageLifecyclePlan>;
}
export interface RunClusterPluginPackageLifecycleExecutionOptions {
readonly openDatabase: OpenPostgresDatabase;
readonly actionRef: string;
readonly approvalRequestId: string;
readonly consumptionId: string;
readonly dispatchId: string;
readonly auditEventId: string;
readonly confirmAuthorization: () => void | Promise<void>;
}
export interface ClusterPluginPackageLifecycleExecutionRun {
readonly database: PostgresSchemaReadinessReport;
readonly status: 'created' | 'existing';
readonly receipt: Readonly<PluginPackageLifecycleReceipt>;
}
type Row = Record<string, unknown>;
function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
throw new TypeError(`${label} is invalid`);
}
return value;
}
function actionRef(value: unknown): string {
if (typeof value !== 'string' || !ACTION_REF_PATTERN.test(value)) {
throw new TypeError('actionRef is invalid');
}
return value;
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
async function databaseNowMs(pool: PostgresPool): Promise<number> {
const result = await pool.query<Row>(
`SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
AS "nowMs"`,
);
const value = result.rows[0]?.nowMs;
const parsed =
typeof value === 'number'
? value
: typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)
? Number(value)
: Number.NaN;
if (
result.rows.length !== 1 ||
!Number.isSafeInteger(parsed) ||
parsed < 0
) {
throw new Error('PostgreSQL lifecycle clock is unavailable');
}
return parsed;
}
function binding(
plan: Readonly<PluginPackageLifecyclePlan>,
): Readonly<ApprovedActionBinding> {
return Object.freeze({
permission: 'package.manage',
actionType: `plugin_package.lifecycle.${plan.impact.action}`,
actionRef: plan.actionRef,
actionDigest: pluginPackageLifecycleActionDigest(plan.impact),
previewDigest: plan.impact.impactDigest,
});
}
function audit(
eventId: string,
approvalRequestId: string,
projectId: string,
fence: Readonly<SecurityPolicyFence>,
occurredAtMs: number,
): Readonly<SecurityAuditRecord> {
return Object.freeze({
eventId,
requestId: approvalRequestId,
operationId: 'approval.consume',
projectId,
subject: CLUSTER_LIFECYCLE_CONSUMER.subject,
authenticationId: CLUSTER_LIFECYCLE_CONSUMER.authenticationId,
outcome: 'allowed',
reasons: Object.freeze(['package_lifecycle_review']),
fence,
occurredAtMs,
});
}
async function closeDatabase(
database: PostgresDatabaseResource | undefined,
failure: unknown,
): Promise<void> {
if (!database) {
if (failure !== undefined) throw failure;
return;
}
try {
await database.close();
} catch (closeError) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Cluster Plugin Package lifecycle failed and PostgreSQL did not close',
);
}
throw closeError;
}
if (failure !== undefined) throw failure;
}
export async function runClusterPluginPackageLifecyclePlan(
options: RunClusterPluginPackageLifecyclePlanOptions,
): Promise<Readonly<ClusterPluginPackageLifecyclePlanRun>> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
typeof options.openDatabase !== 'function' ||
typeof options.confirmAuthorization !== 'function' ||
typeof options.projectId !== 'string' ||
!PROJECT_ID_PATTERN.test(options.projectId) ||
typeof options.packageName !== 'string' ||
!PACKAGE_NAME_PATTERN.test(options.packageName)
) {
throw new TypeError(
'Cluster Plugin Package lifecycle plan options are invalid',
);
}
const requestedActionRef = actionRef(options.actionRef);
const lifetimeMs =
options.lifetimeMs ?? MAX_PLUGIN_PACKAGE_LIFECYCLE_PLAN_LIFETIME_MS;
if (
!Number.isSafeInteger(lifetimeMs) ||
lifetimeMs < 1_000 ||
lifetimeMs > MAX_PLUGIN_PACKAGE_LIFECYCLE_PLAN_LIFETIME_MS
) {
throw new TypeError(
'Cluster Plugin Package lifecycle plan lifetime is invalid',
);
}
let database: PostgresDatabaseResource | undefined;
let failure: unknown;
let result: Readonly<ClusterPluginPackageLifecyclePlanRun> | undefined;
try {
await options.confirmAuthorization();
database = await options.openDatabase();
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const lifecycles = new PostgresPluginPackageLifecycleRepository(
database.pool,
);
const plans = new PostgresPluginPackageLifecyclePlanRepository(
database.pool,
);
const existingValue = await plans.findByActionRef(requestedActionRef);
if (existingValue) {
const existing = normalizePluginPackageLifecyclePlan(existingValue);
if (
existing.impact.action !== options.action ||
existing.impact.target.projectId !== options.projectId ||
existing.impact.target.packageName !== options.packageName ||
!same(existing.requestedBy, options.requestedBy) ||
existing.expiresAtMs - existing.plannedAtMs !== lifetimeMs
) {
throw new PluginPackageLifecyclePlanConflictError(
'actionRef is bound to another lifecycle request',
);
}
await options.confirmAuthorization();
result = Object.freeze({
database: evidence,
status: 'existing' as const,
plan: existing,
});
} else {
const impact = await lifecycles.plan(
options.action,
options.projectId,
options.packageName,
);
const plannedAtMs = await databaseNowMs(database.pool);
const plan = createPluginPackageLifecyclePlan({
actionRef: requestedActionRef,
impact,
requestedBy: options.requestedBy,
plannedAtMs,
expiresAtMs: plannedAtMs + lifetimeMs,
});
await options.confirmAuthorization();
const created = await plans.create(plan);
result = Object.freeze({
database: evidence,
status: created.status,
plan: created.plan,
});
}
} catch (error) {
failure = error;
}
await closeDatabase(database, failure);
if (!result) {
throw new Error('Cluster Plugin Package lifecycle plan produced no result');
}
return result;
}
export async function runClusterPluginPackageLifecycleExecution(
options: RunClusterPluginPackageLifecycleExecutionOptions,
): Promise<Readonly<ClusterPluginPackageLifecycleExecutionRun>> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
typeof options.openDatabase !== 'function' ||
typeof options.confirmAuthorization !== 'function'
) {
throw new TypeError(
'Cluster Plugin Package lifecycle execution options are invalid',
);
}
const requestedActionRef = actionRef(options.actionRef);
const approvalRequestId = identifier(
options.approvalRequestId,
'approvalRequestId',
);
const consumptionId = identifier(options.consumptionId, 'consumptionId');
const dispatchId = identifier(options.dispatchId, 'dispatchId');
const auditEventId = identifier(options.auditEventId, 'auditEventId');
let database: PostgresDatabaseResource | undefined;
let failure: unknown;
let result: Readonly<ClusterPluginPackageLifecycleExecutionRun> | undefined;
try {
await options.confirmAuthorization();
database = await options.openDatabase();
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const plans = new PostgresPluginPackageLifecyclePlanRepository(
database.pool,
);
const planValue = await plans.findByActionRef(requestedActionRef);
if (!planValue) {
throw new PluginPackageLifecycleConflictError(
'durable lifecycle plan is absent',
);
}
const plan = normalizePluginPackageLifecyclePlan(planValue);
const approvals = new PostgresApprovalRequestRepository(database.pool);
let approvalValue = await approvals.findById(approvalRequestId);
if (!approvalValue) {
throw new PluginPackageLifecycleConflictError(
'lifecycle approval is absent',
);
}
let approval = normalizeApprovalRequestRecord(approvalValue);
const approvedAction = binding(plan);
if (
approval.projectId !== plan.impact.target.projectId ||
approval.decisionMode !== 'separation_of_duty' ||
!same(approval.action, approvedAction) ||
!same(approval.requestedBy, plan.requestedBy)
) {
throw new PluginPackageLifecycleConflictError(
'lifecycle approval does not match durable plan',
);
}
let dispatch: Readonly<ApprovedActionDispatchRecord> | null = null;
if (approval.version === 2 && approval.state === 'approved') {
const policy = new ProjectPolicyEngine(
new PostgresProjectPolicyRepository(database.pool),
);
const decision = await policy.decide({
subject: plan.requestedBy,
projectId: plan.impact.target.projectId,
permission: 'package.manage',
});
if (
(decision.effect !== 'allow' &&
decision.effect !== 'require_approval') ||
decision.fence === null
) {
throw new PluginPackageLifecycleConflictError(
'lifecycle requester is no longer authorized',
);
}
const consumedAtMs = await databaseNowMs(database.pool);
const consumed = await approvals.consume({
requestId: approvalRequestId,
expectedVersion: 2,
consumptionId,
dispatchId,
action: approvedAction,
requestedBy: plan.requestedBy,
consumedBy: CLUSTER_LIFECYCLE_CONSUMER.subject,
consumedAtMs,
authorizationFence: decision.fence,
audit: audit(
auditEventId,
approvalRequestId,
plan.impact.target.projectId,
decision.fence,
consumedAtMs,
),
});
approval = consumed.request;
dispatch = consumed.dispatch;
} else if (approval.version === 3 && approval.state === 'consumed') {
dispatch = await approvals.findDispatchById(dispatchId);
}
if (
approval.version !== 3 ||
approval.state !== 'consumed' ||
approval.consumptionId !== consumptionId ||
approval.dispatchId !== dispatchId ||
!dispatch ||
!same(dispatch.action, approvedAction) ||
!same(dispatch.requestedBy, plan.requestedBy) ||
!same(dispatch.approvedBy, approval.decidedBy) ||
!same(dispatch.consumedBy, CLUSTER_LIFECYCLE_CONSUMER.subject)
) {
throw new PluginPackageLifecycleConflictError(
'lifecycle dispatch does not match durable approval',
);
}
const lifecycles = new PostgresPluginPackageLifecycleRepository(
database.pool,
);
const event = createPluginPackageLifecycleEvent({
dispatchId: dispatch.id,
impact: plan.impact,
requestedBy: dispatch.requestedBy,
approvedBy: dispatch.approvedBy,
authorizationMode: 'separation_of_duty',
occurredAtMs: dispatch.createdAtMs,
});
const existingReceipt = await lifecycles.findByEventDigest(
event.eventDigest,
);
if (existingReceipt) {
await options.confirmAuthorization();
result = Object.freeze({
database: evidence,
status: 'existing' as const,
receipt: existingReceipt,
});
} else {
const currentImpact = await lifecycles.plan(
plan.impact.action,
plan.impact.target.projectId,
plan.impact.target.packageName,
);
if (!same(currentImpact, plan.impact)) {
throw new PluginPackageLifecycleConflictError(
'approved lifecycle impact is stale',
);
}
const transitioned = await lifecycles.transition(
event,
options.confirmAuthorization,
);
result = Object.freeze({
database: evidence,
status: transitioned.status,
receipt: transitioned.receipt,
});
}
} catch (error) {
failure = error;
}
await closeDatabase(database, failure);
if (!result) {
throw new Error(
'Cluster Plugin Package lifecycle execution produced no result',
);
}
return result;
}
@@ -0,0 +1,525 @@
// Cluster Plugin Package lifecycle boundary; keep approval management authority explicit.
import { PostgresApprovalRequestRepository } from '@qinglong/cluster-postgres/approved-action';
import { PostgresPluginPackageLifecyclePlanReader } from '@qinglong/cluster-postgres/package-manager';
import { PostgresProjectPolicyRepository } from '@qinglong/cluster-postgres/project-policy';
import type { PostgresPool } from '@qinglong/runtime-core';
import {
createApprovalRequest,
normalizeApprovalRequestRecord,
type ApprovalRequestRecord,
type CreateApprovalRequestResult,
type DecideApprovalRequestResult,
} from '@qinglong/runtime-core/approved-action';
import { pluginPackageLifecycleActionDigest } from '@qinglong/runtime-core/plugin-package-lifecycle';
import {
normalizePluginPackageLifecyclePlan,
type PluginPackageLifecyclePlan,
} from '@qinglong/runtime-core/plugin-package-lifecycle-plan';
import {
PluginPackageManagementAuthorizationError,
PluginPackageManagementConflictError,
PluginPackageManagementRequestError,
PluginPackageManagementUnavailableError,
} from '@qinglong/runtime-core/plugin-package-management';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPolicyFence,
type SecurityPrincipal,
type SecuritySubject,
} from '@qinglong/runtime-core/security';
import type { SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
const DEFAULT_APPROVAL_LIFETIME_MS = 15 * 60 * 1000;
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const REASON_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
export interface ClusterPluginPackageLifecycleManagementOptions {
readonly pool: PostgresPool;
readonly now?: () => number;
readonly approvalLifetimeMs?: number;
}
export interface ProposeClusterPluginPackageLifecycleRequest {
readonly actionRef: string;
readonly approvalRequestId: string;
readonly approvalAuditEventId: string;
readonly principal: SecurityPrincipal;
}
export interface ProposeClusterPluginPackageLifecycleResult {
readonly plan: Readonly<PluginPackageLifecyclePlan>;
readonly approvalStatus: CreateApprovalRequestResult['status'];
readonly approvalRequest: Readonly<ApprovalRequestRecord>;
}
export interface DecideClusterPluginPackageLifecycleRequest {
readonly actionRef: string;
readonly approvalRequestId: string;
readonly expectedVersion: number;
readonly decisionId: string;
readonly auditEventId: string;
readonly decision: 'approved' | 'rejected';
readonly reasonCode: string;
readonly principal: SecurityPrincipal;
}
export interface InspectClusterPluginPackageLifecycleRequest {
readonly actionRef: string;
readonly approvalRequestId: string;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export interface InspectClusterPluginPackageLifecycleResult {
readonly plan: Readonly<PluginPackageLifecyclePlan> | null;
readonly approvalRequest: Readonly<ApprovalRequestRecord> | null;
readonly stale: boolean;
}
export interface ClusterPluginPackageLifecycleManagementService {
propose(
request: ProposeClusterPluginPackageLifecycleRequest,
): Promise<Readonly<ProposeClusterPluginPackageLifecycleResult>>;
decide(
request: DecideClusterPluginPackageLifecycleRequest,
): Promise<Readonly<DecideApprovalRequestResult>>;
inspectAuthorized(
request: InspectClusterPluginPackageLifecycleRequest,
): Promise<Readonly<InspectClusterPluginPackageLifecycleResult>>;
}
function identifier(value: unknown, label: string): string {
if (typeof value !== 'string' || !IDENTIFIER_PATTERN.test(value)) {
throw new PluginPackageManagementRequestError(`${label} is invalid`);
}
return value;
}
function actionRef(value: unknown): string {
if (typeof value !== 'string' || !ACTION_REF_PATTERN.test(value)) {
throw new PluginPackageManagementRequestError('actionRef is invalid');
}
return value;
}
function currentTime(now: () => number): number {
const value = now();
if (!Number.isSafeInteger(value) || value < 0) {
throw new PluginPackageManagementUnavailableError();
}
return value;
}
function sameSubject(
left: Readonly<SecuritySubject>,
right: Readonly<SecuritySubject>,
): boolean {
return left.type === right.type && left.id === right.id;
}
function action(plan: Readonly<PluginPackageLifecyclePlan>) {
return Object.freeze({
permission: 'package.manage' as const,
actionType: `plugin_package.lifecycle.${plan.impact.action}`,
actionRef: plan.actionRef,
actionDigest: pluginPackageLifecycleActionDigest(plan.impact),
previewDigest: plan.impact.impactDigest,
});
}
function audit(
eventId: string,
requestId: string,
operationId: 'approval.request' | 'approval.decide',
projectId: string,
subject: Readonly<SecuritySubject>,
authenticationId: string,
outcome: 'allowed' | 'approval_required',
fence: Readonly<SecurityPolicyFence>,
occurredAtMs: number,
): Readonly<SecurityAuditRecord> {
return Object.freeze({
eventId,
requestId,
operationId,
projectId,
subject,
authenticationId,
outcome,
reasons: Object.freeze(['package_lifecycle_review']),
fence,
occurredAtMs,
});
}
export function createClusterPluginPackageLifecycleManagementService(
options: ClusterPluginPackageLifecycleManagementOptions,
): Readonly<ClusterPluginPackageLifecycleManagementService> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
key !== 'pool' &&
key !== 'now' &&
key !== 'approvalLifetimeMs',
) ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError(
'Cluster Plugin Package lifecycle management options are invalid',
);
}
const approvalLifetimeMs =
options.approvalLifetimeMs ?? DEFAULT_APPROVAL_LIFETIME_MS;
if (
!Number.isSafeInteger(approvalLifetimeMs) ||
approvalLifetimeMs < 1_000 ||
approvalLifetimeMs > DEFAULT_APPROVAL_LIFETIME_MS
) {
throw new TypeError(
'Cluster Plugin Package lifecycle approval lifetime is invalid',
);
}
const now = options.now ?? Date.now;
const plans = new PostgresPluginPackageLifecyclePlanReader(options.pool);
const approvals = new PostgresApprovalRequestRepository(options.pool);
const policy = new ProjectPolicyEngine(
new PostgresProjectPolicyRepository(options.pool),
);
const authorize = async (
principalValue: SecurityPrincipal,
projectId: string,
permission: 'package.manage' | 'approval.decide',
observedAtMs: number,
): Promise<
Readonly<{
principal: Readonly<SecurityPrincipal>;
fence: Readonly<SecurityPolicyFence>;
}>
> => {
let principal;
try {
principal = normalizeSecurityPrincipal(principalValue, observedAtMs);
} catch {
throw new PluginPackageManagementAuthorizationError();
}
if (
principal.subject.type !== 'user' ||
(principal.assurance !== 'multi_factor' &&
principal.assurance !== 'hardware')
) {
throw new PluginPackageManagementAuthorizationError();
}
let decision;
try {
decision = await policy.authorize(principal, projectId, permission);
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
if (decision.effect !== 'allow' || decision.fence === null) {
throw new PluginPackageManagementAuthorizationError();
}
return Object.freeze({ principal, fence: decision.fence });
};
const loadPlan = async (
requestedActionRef: string,
): Promise<Readonly<PluginPackageLifecyclePlan>> => {
let plan;
try {
plan = await plans.findByActionRef(actionRef(requestedActionRef));
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
if (!plan) {
throw new PluginPackageManagementConflictError(
'Plugin Package lifecycle plan does not exist',
);
}
return normalizePluginPackageLifecyclePlan(plan);
};
return Object.freeze({
async propose(request: ProposeClusterPluginPackageLifecycleRequest) {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).sort().join('\0') !==
[
'actionRef',
'approvalAuditEventId',
'approvalRequestId',
'principal',
]
.sort()
.join('\0')
) {
throw new PluginPackageManagementRequestError(
'lifecycle proposal request is invalid',
);
}
const approvalRequestId = identifier(
request.approvalRequestId,
'approvalRequestId',
);
const approvalAuditEventId = identifier(
request.approvalAuditEventId,
'approvalAuditEventId',
);
const plan = await loadPlan(request.actionRef);
const observedAtMs = currentTime(now);
if (observedAtMs > plan.expiresAtMs) {
throw new PluginPackageManagementConflictError(
'Plugin Package lifecycle plan expired',
);
}
const authorization = await authorize(
request.principal,
plan.impact.target.projectId,
'package.manage',
observedAtMs,
);
if (!sameSubject(plan.requestedBy, authorization.principal.subject)) {
throw new PluginPackageManagementAuthorizationError();
}
const binding = action(plan);
const existing = await approvals.findById(approvalRequestId);
if (existing) {
const normalized = normalizeApprovalRequestRecord(existing);
if (
normalized.projectId !== plan.impact.target.projectId ||
normalized.decisionMode !== 'separation_of_duty' ||
!sameSubject(normalized.requestedBy, plan.requestedBy) ||
JSON.stringify(normalized.action) !== JSON.stringify(binding)
) {
throw new PluginPackageManagementConflictError(
'Approval request is bound to another lifecycle plan',
);
}
return Object.freeze({
plan,
approvalStatus: 'existing' as const,
approvalRequest: normalized,
});
}
const expiresAtMs = Math.min(
observedAtMs + approvalLifetimeMs,
plan.expiresAtMs,
);
if (expiresAtMs <= observedAtMs) {
throw new PluginPackageManagementConflictError(
'Plugin Package lifecycle plan has no approval lifetime',
);
}
const result = await approvals.create({
request: createApprovalRequest({
id: approvalRequestId,
projectId: plan.impact.target.projectId,
action: binding,
risk: 'high',
decisionMode: 'separation_of_duty',
requestedBy: authorization.principal.subject,
requestedAtMs: observedAtMs,
expiresAtMs,
requestFence: authorization.fence,
}),
audit: audit(
approvalAuditEventId,
approvalRequestId,
'approval.request',
plan.impact.target.projectId,
authorization.principal.subject,
authorization.principal.authenticationId,
'approval_required',
authorization.fence,
observedAtMs,
),
});
return Object.freeze({
plan,
approvalStatus: result.status,
approvalRequest: result.request,
});
},
async decide(request: DecideClusterPluginPackageLifecycleRequest) {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).sort().join('\0') !==
[
'actionRef',
'approvalRequestId',
'auditEventId',
'decision',
'decisionId',
'expectedVersion',
'principal',
'reasonCode',
]
.sort()
.join('\0') ||
(request.decision !== 'approved' &&
request.decision !== 'rejected') ||
typeof request.reasonCode !== 'string' ||
!REASON_PATTERN.test(request.reasonCode) ||
!Number.isSafeInteger(request.expectedVersion) ||
request.expectedVersion < 1
) {
throw new PluginPackageManagementRequestError(
'lifecycle decision request is invalid',
);
}
const plan = await loadPlan(request.actionRef);
const approvalRequestId = identifier(
request.approvalRequestId,
'approvalRequestId',
);
const decisionId = identifier(request.decisionId, 'decisionId');
const auditEventId = identifier(request.auditEventId, 'auditEventId');
const current = await approvals.findById(approvalRequestId);
if (!current) {
throw new PluginPackageManagementConflictError(
'Approval request does not exist',
);
}
const approval = normalizeApprovalRequestRecord(current);
if (
approval.action.actionRef !== plan.actionRef ||
JSON.stringify(approval.action) !== JSON.stringify(action(plan))
) {
throw new PluginPackageManagementConflictError(
'Approval request does not match lifecycle plan',
);
}
const observedAtMs = currentTime(now);
const authorization = await authorize(
request.principal,
approval.projectId,
'approval.decide',
observedAtMs,
);
if (
approval.decisionId === decisionId &&
approval.decision === request.decision &&
approval.decisionReasonCode === request.reasonCode &&
approval.decidedBy &&
sameSubject(approval.decidedBy, authorization.principal.subject)
) {
return Object.freeze({
status: 'existing' as const,
request: approval,
});
}
return approvals.decide({
requestId: approvalRequestId,
expectedVersion: request.expectedVersion,
decisionId,
decision: request.decision,
reasonCode: request.reasonCode,
principal: authorization.principal,
decidedAtMs: observedAtMs,
authorizationFence: authorization.fence,
audit: audit(
auditEventId,
approvalRequestId,
'approval.decide',
approval.projectId,
authorization.principal.subject,
authorization.principal.authenticationId,
'allowed',
authorization.fence,
observedAtMs,
),
});
},
async inspectAuthorized(
request: InspectClusterPluginPackageLifecycleRequest,
) {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).sort().join('\0') !==
[
'actionRef',
'approvalRequestId',
'inspectionId',
'principal',
]
.sort()
.join('\0')
) {
throw new PluginPackageManagementRequestError(
'lifecycle inspection request is invalid',
);
}
identifier(request.inspectionId, 'inspectionId');
const requestedActionRef = actionRef(request.actionRef);
const approvalRequestId = identifier(
request.approvalRequestId,
'approvalRequestId',
);
const [planValue, approvalValue] = await Promise.all([
plans.findByActionRef(requestedActionRef),
approvals.findById(approvalRequestId),
]);
if (!planValue && !approvalValue) {
throw new PluginPackageManagementConflictError(
'Plugin Package lifecycle state does not exist',
);
}
const plan = planValue
? normalizePluginPackageLifecyclePlan(planValue)
: null;
const approval = approvalValue
? normalizeApprovalRequestRecord(approvalValue)
: null;
const projectId = plan?.impact.target.projectId ?? approval?.projectId;
if (!projectId) {
throw new PluginPackageManagementUnavailableError();
}
const observedAtMs = currentTime(now);
try {
await authorize(
request.principal,
projectId,
'package.manage',
observedAtMs,
);
} catch (error) {
if (!(error instanceof PluginPackageManagementAuthorizationError)) {
throw error;
}
await authorize(
request.principal,
projectId,
'approval.decide',
observedAtMs,
);
}
return Object.freeze({
plan,
approvalRequest: approval,
stale:
plan === null ||
approval === null ||
approval.action.actionRef !== plan.actionRef ||
JSON.stringify(approval.action) !== JSON.stringify(action(plan)) ||
observedAtMs > plan.expiresAtMs,
});
},
});
}
@@ -0,0 +1,174 @@
// Cluster Plugin Package lifecycle boundary; keep quarantine authority explicit.
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
} from '@qinglong/runtime-core';
import {
InvalidPluginPackageQuarantineError,
normalizePluginPackageQuarantineEvent,
type PluginPackageQuarantineEvent,
type PluginPackageQuarantineRepository,
type PluginPackageWithdrawalReceipt,
} from '@qinglong/runtime-core/plugin-package-quarantine';
import {
PostgresPluginPackageQuarantineRepository,
assertPostgresPackageExecutorSchemaReady,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-executor';
export const CLUSTER_PLUGIN_PACKAGE_QUARANTINE_BATCH_LIMIT = 128;
export interface ClusterPluginPackageQuarantineService {
quarantine(
events: readonly Readonly<PluginPackageQuarantineEvent>[],
confirmAuthorization: (
event: Readonly<PluginPackageQuarantineEvent>,
) => void | Promise<void>,
): Promise<
readonly Readonly<{
status: 'created' | 'existing';
eventDigest: string;
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>[]
>;
}
export interface RunClusterPluginPackageQuarantineOptions {
readonly openDatabase: OpenPostgresDatabase;
readonly events: readonly Readonly<PluginPackageQuarantineEvent>[];
readonly confirmAuthorization: (
event: Readonly<PluginPackageQuarantineEvent>,
) => void | Promise<void>;
}
export interface ClusterPluginPackageQuarantineRun {
readonly database: PostgresSchemaReadinessReport;
readonly results: readonly Readonly<{
status: 'created' | 'existing';
eventDigest: string;
receipt: Readonly<PluginPackageWithdrawalReceipt>;
}>[];
}
function targetKey(event: Readonly<PluginPackageQuarantineEvent>): string {
return [
event.target.projectId,
event.target.packageName,
event.target.installationId,
event.target.lockDigest,
].join('\0');
}
function normalizedBatch(
events: readonly Readonly<PluginPackageQuarantineEvent>[],
): readonly Readonly<PluginPackageQuarantineEvent>[] {
if (
!Array.isArray(events) ||
events.length < 1 ||
events.length > CLUSTER_PLUGIN_PACKAGE_QUARANTINE_BATCH_LIMIT ||
Object.keys(events).some((key, index) => key !== String(index))
) {
throw new InvalidPluginPackageQuarantineError(
`events must contain 1-${CLUSTER_PLUGIN_PACKAGE_QUARANTINE_BATCH_LIMIT} dense items`,
);
}
const normalized = events.map(normalizePluginPackageQuarantineEvent);
const eventDigests = new Set<string>();
const targets = new Set<string>();
for (const event of normalized) {
const target = targetKey(event);
if (eventDigests.has(event.eventDigest) || targets.has(target)) {
throw new InvalidPluginPackageQuarantineError(
'batch event digests and targets must be unique',
);
}
eventDigests.add(event.eventDigest);
targets.add(target);
}
return Object.freeze(normalized);
}
export function createClusterPluginPackageQuarantineService(
repository: PluginPackageQuarantineRepository,
): Readonly<ClusterPluginPackageQuarantineService> {
if (
!repository ||
typeof repository.findTargetsByLockDigest !== 'function' ||
typeof repository.findByEventDigest !== 'function' ||
typeof repository.quarantine !== 'function'
) {
throw new TypeError(
'Cluster Plugin Package quarantine repository is invalid',
);
}
return Object.freeze({
async quarantine(
events: readonly Readonly<PluginPackageQuarantineEvent>[],
confirmAuthorization: (
event: Readonly<PluginPackageQuarantineEvent>,
) => void | Promise<void>,
) {
const batch = normalizedBatch(events);
if (typeof confirmAuthorization !== 'function') {
throw new InvalidPluginPackageQuarantineError(
'confirmAuthorization is invalid',
);
}
const results = [];
for (const event of batch) {
const result = await repository.quarantine(event, () =>
confirmAuthorization(event),
);
results.push(
Object.freeze({
status: result.status,
eventDigest: event.eventDigest,
receipt: result.receipt,
}),
);
}
return Object.freeze(results);
},
});
}
export async function runClusterPluginPackageQuarantine(
options: RunClusterPluginPackageQuarantineOptions,
): Promise<Readonly<ClusterPluginPackageQuarantineRun>> {
if (!options || typeof options !== 'object' || Array.isArray(options)) {
throw new TypeError(
'Cluster Plugin Package quarantine options are invalid',
);
}
if (
Object.keys(options).some(
(key) =>
!['openDatabase', 'events', 'confirmAuthorization'].includes(key),
) ||
typeof options.openDatabase !== 'function'
) {
throw new TypeError(
'Cluster Plugin Package quarantine options shape is invalid',
);
}
const events = normalizedBatch(options.events);
if (typeof options.confirmAuthorization !== 'function') {
throw new InvalidPluginPackageQuarantineError(
'confirmAuthorization is invalid',
);
}
let database: PostgresDatabaseResource | undefined;
try {
database = await options.openDatabase();
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const results =
await createClusterPluginPackageQuarantineService(
new PostgresPluginPackageQuarantineRepository(database.pool),
).quarantine(events, options.confirmAuthorization);
return Object.freeze({ database: evidence, results });
} finally {
await database?.close();
}
}
@@ -0,0 +1,409 @@
/** Plugin Package management service boundary. */
import { PostgresApprovalRequestRepository } from '@qinglong/cluster-postgres/approved-action';
import { PostgresPluginPackageInstallInventoryReader } from '@qinglong/cluster-postgres/package-manager';
import { PostgresPluginPackageInstallProposalRepository } from '@qinglong/cluster-postgres/plugin-package-proposal';
import { PostgresProjectPolicyRepository } from '@qinglong/cluster-postgres/project-policy';
import type { PostgresPool } from '@qinglong/runtime-core';
import {
PluginPackageManagementAuthorizationError,
PluginPackageManagementConflictError,
PluginPackageManagementQuotaExceededError,
PluginPackageManagementRequestError,
PluginPackageManagementUnavailableError,
createPluginPackageManagementService,
type InspectPluginPackageInstallResult,
type PluginPackageManagementQuotaPort,
type PluginPackageManagementService as RuntimePluginPackageManagementService,
} from '@qinglong/runtime-core/plugin-package-management';
import {
MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE,
normalizePluginPackageInstallInventoryCursor,
type PluginPackageInstallInventoryItem,
type PluginPackageInstallInventoryPage,
} from '@qinglong/runtime-core/plugin-package-install';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPolicyDecision,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
export const CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_DECISION_MODE =
'separation_of_duty' as const;
type ClusterPluginPackageManagementMutationService = Pick<
RuntimePluginPackageManagementService,
'propose' | 'decide' | 'inspect'
>;
export interface InspectAuthorizedClusterPluginPackageRequest {
readonly actionRef: string;
readonly approvalRequestId: string;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export interface InspectAuthorizedClusterPluginPackageInstallationRequest {
readonly projectId: string;
readonly packageName: string;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export interface ListAuthorizedClusterPluginPackageInstallationsRequest {
readonly projectId: string;
readonly limit: number;
readonly after?: Readonly<{ packageName: string }>;
readonly inspectionId: string;
readonly principal: SecurityPrincipal;
}
export type ClusterPluginPackageManagementService =
ClusterPluginPackageManagementMutationService &
Readonly<{
inspectAuthorized(
request: InspectAuthorizedClusterPluginPackageRequest,
): Promise<Readonly<InspectPluginPackageInstallResult>>;
inspectInstallationAuthorized(
request: InspectAuthorizedClusterPluginPackageInstallationRequest,
): Promise<Readonly<PluginPackageInstallInventoryItem> | null>;
listInstallationsAuthorized(
request: ListAuthorizedClusterPluginPackageInstallationsRequest,
): Promise<Readonly<PluginPackageInstallInventoryPage>>;
}>;
export interface ClusterPluginPackageManagementOptions {
readonly pool: PostgresPool;
readonly approvalLifetimeMs?: number;
readonly now?: () => number;
readonly quota?: PluginPackageManagementQuotaPort;
}
const INSPECTION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const PACKAGE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
export function createClusterPluginPackageManagementService(
options: ClusterPluginPackageManagementOptions,
): Readonly<ClusterPluginPackageManagementService> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
key !== 'pool' &&
key !== 'approvalLifetimeMs' &&
key !== 'now' &&
key !== 'quota',
)
) {
throw new TypeError(
'cluster Plugin Package management options are invalid',
);
}
const now = options.now ?? Date.now;
const policy = new ProjectPolicyEngine(
new PostgresProjectPolicyRepository(options.pool),
);
const installations = new PostgresPluginPackageInstallInventoryReader(
options.pool,
);
const service = createPluginPackageManagementService(
policy,
new PostgresPluginPackageInstallProposalRepository(options.pool),
new PostgresApprovalRequestRepository(options.pool),
Object.freeze({
async dispatchBatch(): Promise<never> {
throw new Error(
'cluster Plugin Package management cannot execute approved actions',
);
},
}),
{
decisionMode: CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_DECISION_MODE,
consumer: Object.freeze({
subject: Object.freeze({
type: 'system' as const,
id: 'cluster_package_management_unreachable_consumer',
}),
authenticationId: 'cluster-package-management-unreachable-consumer',
}),
...(options.approvalLifetimeMs === undefined
? {}
: { approvalLifetimeMs: options.approvalLifetimeMs }),
now,
...(options.quota === undefined ? {} : { quota: options.quota }),
},
);
const allowed = (
decision: Readonly<SecurityPolicyDecision>,
allowApproval: boolean,
): boolean =>
decision.fence !== null &&
(decision.effect === 'allow' ||
(allowApproval && decision.effect === 'require_approval'));
const authorizeInstallationInventory = async (
projectId: string,
inspectionId: string,
principalValue: SecurityPrincipal,
): Promise<Readonly<SecurityPrincipal>> => {
const observedAtMs = now();
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
throw new PluginPackageManagementUnavailableError();
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(principalValue, observedAtMs);
} catch {
throw new PluginPackageManagementAuthorizationError();
}
let decision: Readonly<SecurityPolicyDecision>;
try {
decision = await policy.authorize(principal, projectId, 'package.manage');
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
if (!allowed(decision, true)) {
throw new PluginPackageManagementAuthorizationError();
}
if (options.quota) {
try {
await options.quota.consume({
projectId,
subject: principal.subject,
operation: 'plugin-package.inspect',
idempotencyKey: inspectionId,
});
} catch (error) {
if (error instanceof PluginPackageManagementQuotaExceededError) {
throw error;
}
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
return principal;
};
return Object.freeze({
propose: service.propose,
decide: service.decide,
inspect: service.inspect,
async inspectInstallationAuthorized(
request: InspectAuthorizedClusterPluginPackageInstallationRequest,
): Promise<Readonly<PluginPackageInstallInventoryItem> | null> {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).sort().join('\0') !==
['inspectionId', 'packageName', 'principal', 'projectId']
.sort()
.join('\0') ||
typeof request.projectId !== 'string' ||
!PROJECT_ID_PATTERN.test(request.projectId) ||
typeof request.packageName !== 'string' ||
!PACKAGE_NAME_PATTERN.test(request.packageName) ||
typeof request.inspectionId !== 'string' ||
!INSPECTION_ID_PATTERN.test(request.inspectionId)
) {
throw new PluginPackageManagementRequestError(
'installation inspection request is invalid',
);
}
await authorizeInstallationInventory(
request.projectId,
request.inspectionId,
request.principal,
);
try {
return await installations.findCurrent(
request.projectId,
request.packageName,
);
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
},
async listInstallationsAuthorized(
request: ListAuthorizedClusterPluginPackageInstallationsRequest,
): Promise<Readonly<PluginPackageInstallInventoryPage>> {
const keys =
request && typeof request === 'object' && !Array.isArray(request)
? Object.keys(request)
: [];
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
!keys.includes('projectId') ||
!keys.includes('limit') ||
!keys.includes('inspectionId') ||
!keys.includes('principal') ||
keys.some(
(key) =>
![
'after',
'inspectionId',
'limit',
'principal',
'projectId',
].includes(key),
) ||
typeof request.projectId !== 'string' ||
!PROJECT_ID_PATTERN.test(request.projectId) ||
!Number.isSafeInteger(request.limit) ||
request.limit < 1 ||
request.limit > MAX_PLUGIN_PACKAGE_INSTALL_INVENTORY_PAGE_SIZE ||
typeof request.inspectionId !== 'string' ||
!INSPECTION_ID_PATTERN.test(request.inspectionId)
) {
throw new PluginPackageManagementRequestError(
'installation list request is invalid',
);
}
let after: Readonly<{ packageName: string }> | undefined;
try {
after =
request.after === undefined
? undefined
: normalizePluginPackageInstallInventoryCursor(request.after);
} catch {
throw new PluginPackageManagementRequestError(
'installation list cursor is invalid',
);
}
await authorizeInstallationInventory(
request.projectId,
request.inspectionId,
request.principal,
);
try {
return await installations.listCurrentPage({
projectId: request.projectId,
limit: request.limit,
...(after === undefined ? {} : { after }),
});
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
},
async inspectAuthorized(
request: InspectAuthorizedClusterPluginPackageRequest,
): Promise<Readonly<InspectPluginPackageInstallResult>> {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
Object.keys(request).length !== 4 ||
Object.keys(request).some(
(key) =>
![
'actionRef',
'approvalRequestId',
'inspectionId',
'principal',
].includes(key),
) ||
typeof request.inspectionId !== 'string' ||
!INSPECTION_ID_PATTERN.test(request.inspectionId)
) {
throw new PluginPackageManagementRequestError(
'inspection request is invalid',
);
}
const observedAtMs = now();
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
throw new PluginPackageManagementUnavailableError();
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(request.principal, observedAtMs);
} catch {
throw new PluginPackageManagementAuthorizationError();
}
const current = await service.inspect(
request.actionRef,
request.approvalRequestId,
);
const projectId =
current.proposal?.projectId ?? current.approvalRequest?.projectId;
if (!projectId) {
throw new PluginPackageManagementConflictError(
'Plugin Package management state does not exist',
);
}
if (
(current.proposal &&
current.approvalRequest &&
(current.proposal.projectId !== current.approvalRequest.projectId ||
current.approvalRequest.action.actionRef !==
current.proposal.actionRef ||
current.approvalRequest.action.actionDigest !==
current.proposal.actionDigest ||
current.approvalRequest.action.previewDigest !==
current.proposal.previewDigest)) ||
(current.proposal &&
current.proposal.actionRef !== request.actionRef) ||
(current.approvalRequest &&
current.approvalRequest.id !== request.approvalRequestId)
) {
throw new PluginPackageManagementUnavailableError();
}
let packageDecision: Readonly<SecurityPolicyDecision>;
let approvalDecision: Readonly<SecurityPolicyDecision> | undefined;
try {
packageDecision = await policy.authorize(
principal,
projectId,
'package.manage',
);
if (!allowed(packageDecision, true)) {
approvalDecision = await policy.authorize(
principal,
projectId,
'approval.decide',
);
}
} catch (error) {
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
if (
!allowed(packageDecision, true) &&
(!approvalDecision || !allowed(approvalDecision, false))
) {
throw new PluginPackageManagementAuthorizationError();
}
if (options.quota) {
try {
await options.quota.consume({
projectId,
subject: principal.subject,
operation: 'plugin-package.inspect',
idempotencyKey: request.inspectionId,
});
} catch (error) {
if (error instanceof PluginPackageManagementQuotaExceededError) {
throw error;
}
throw new PluginPackageManagementUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
}
return current;
},
});
}
@@ -0,0 +1,111 @@
#!/usr/bin/env node
/** One-shot Plugin Package management process CLI boundary. */
import {
startClusterPluginPackageManagementProcess,
type ClusterPluginPackageManagementProcessRuntime,
} from './pluginPackageManagementProcess';
const USAGE = 'Usage: ql3-plugin-package-manage';
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as {
readonly name?: unknown;
readonly code?: unknown;
};
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_failed',
name:
typeof candidate?.name === 'string' && candidate.name.length <= 128
? candidate.name
: 'Error',
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
? { code: candidate.code }
: {}),
});
}
function emit(value: Readonly<Record<string, unknown>>): void {
process.stdout.write(`${JSON.stringify(value)}\n`);
}
async function run(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
if (argv.length !== 0) {
process.stderr.write(
`${JSON.stringify({
code: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_CLI_USAGE_INVALID',
message: USAGE,
})}\n`,
);
process.exitCode = 64;
return;
}
let runtime: Readonly<ClusterPluginPackageManagementProcessRuntime>;
try {
runtime = await startClusterPluginPackageManagementProcess({
environment: process.env,
onError() {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_unavailable',
});
},
});
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
return;
}
if (runtime.status === 'disabled') {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_disabled',
});
return;
}
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_started',
address: runtime.address,
identityGeneration: runtime.identity.generation,
databaseContractVersion: runtime.database.contractVersion,
databaseMigrationCount: runtime.database.migrationIds.length,
});
let stopping: Promise<void> | undefined;
const stop = (): Promise<void> => {
stopping ??= runtime.close().then(() => {
emit({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management',
event: 'management_stopped',
});
});
return stopping;
};
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.once(signal, () => {
void stop().then(
() => {
process.exitCode = 0;
},
(error) => {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
},
);
});
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,102 @@
#!/usr/bin/env node
/** One-shot Plugin Package management client CLI boundary. */
import {
ClusterPluginPackageManagementClientRemoteError,
executeClusterPluginPackageManagementClient,
} from '../../management-support/pluginPackageManagementClient';
const USAGE =
'Usage: ql3-plugin-package-client --config=/absolute/client.json --command=/absolute/command.json --assertion=/absolute/assertion.jwt';
function parseArguments(
argv: readonly string[],
): Readonly<{
configFile: string;
commandFile: string;
assertionFile: string;
}> | null {
if (argv.length !== 3) return null;
const values = new Map<string, string>();
for (const argument of argv) {
const match = /^--(config|command|assertion)=(\/.+)$/.exec(argument);
if (!match || values.has(match[1]!)) return null;
values.set(match[1]!, match[2]!);
}
if (
!values.has('config') ||
!values.has('command') ||
!values.has('assertion')
) {
return null;
}
return Object.freeze({
configFile: values.get('config')!,
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
});
}
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as {
readonly code?: unknown;
};
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management-client',
event: 'command_failed',
code:
typeof candidate?.code === 'string' &&
candidate.code.length <= 128
? candidate.code
: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_CLIENT_FAILED',
...(error instanceof ClusterPluginPackageManagementClientRemoteError
? {
statusCode: error.statusCode,
responseCode: error.responseCode,
requestId: error.requestId,
...(error.retryAfterSeconds === null
? {}
: { retryAfterSeconds: error.retryAfterSeconds }),
}
: {}),
});
}
async function run(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
const paths = parseArguments(argv);
if (!paths) {
process.stderr.write(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management-client',
event: 'usage_invalid',
code: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_CLIENT_USAGE_INVALID',
})}\n`,
);
process.exitCode = 64;
return;
}
try {
const result =
await executeClusterPluginPackageManagementClient(paths);
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
component: 'qinglong3-plugin-package-management-client',
event: 'command_completed',
requestId: result.requestId,
result: result.result,
})}\n`,
);
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,796 @@
/** Explicit Kubernetes PortForward client boundary for Plugin Package management. */
import {
createPrivateKey,
X509Certificate,
} from 'node:crypto';
import { Duplex, PassThrough, Writable } from 'node:stream';
import { TextDecoder } from 'node:util';
import {
ClusterPluginPackageManagementClientConfigurationError,
ClusterPluginPackageManagementClientRemoteError,
ClusterPluginPackageManagementClientRequestError,
executeClusterPluginPackageManagementClient,
readCanonicalFile,
type ClusterPluginPackageManagementClientPaths,
type ClusterPluginPackageManagementClientRawConnection,
type ClusterPluginPackageManagementClientResult,
} from '../../management-support/pluginPackageManagementClient';
const MAX_KUBERNETES_CONFIG_BYTES = 16 * 1024;
const MAX_KUBECONFIG_BYTES = 256 * 1024;
const MAX_KUBERNETES_CA_BYTES = 256 * 1024;
const MAX_KUBERNETES_CLIENT_MATERIAL_BYTES = 256 * 1024;
const MAX_KUBERNETES_TOKEN_BYTES = 16 * 1024;
const MANAGEMENT_NAME = 'ql3-plugin-package-management';
const MANAGEMENT_PORT = 8443;
const MANAGEMENT_LABEL_SELECTOR =
'app.kubernetes.io/name=ql3-plugin-package-management,' +
'app.kubernetes.io/component=plugin-package-management';
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const DNS_LABEL_PATTERN =
/^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const CONTEXT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,255}$/;
const POD_NAME_PATTERN =
/^ql3-plugin-package-management-[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?(?:-[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?)?$/;
const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~+/-]{0,16383}$/;
type JsonObject = Record<string, unknown>;
type KubernetesModule = typeof import('@kubernetes/client-node', {
with: { 'resolution-mode': 'import' }
});
type KubernetesConfig = InstanceType<KubernetesModule['KubeConfig']>;
interface ReviewedKubernetesClientConfig {
readonly schemaVersion: 1;
readonly kubeconfigFile: string;
readonly context: string;
readonly namespace: string;
readonly apiTimeoutMs: number;
}
interface KubernetesPod {
readonly metadata?: {
readonly name?: string;
readonly namespace?: string;
readonly uid?: string;
readonly deletionTimestamp?: unknown;
readonly labels?: Readonly<Record<string, string>>;
};
readonly spec?: {
readonly serviceAccountName?: string;
readonly automountServiceAccountToken?: boolean;
readonly containers?: readonly Readonly<{ readonly name?: string }>[];
};
readonly status?: {
readonly phase?: string;
readonly conditions?: readonly Readonly<{
readonly type?: string;
readonly status?: string;
}>[];
readonly containerStatuses?: readonly Readonly<{
readonly name?: string;
readonly ready?: boolean;
}>[];
};
}
interface KubernetesPodList {
readonly metadata?: {
readonly continue?: string;
};
readonly items?: readonly KubernetesPod[];
}
export interface ClusterPluginPackageManagementKubernetesPodApi {
listNamespacedPod(
request: Readonly<{
namespace: string;
labelSelector: string;
limit: number;
timeoutSeconds: number;
watch: false;
}>,
): Promise<KubernetesPodList>;
}
export interface ClusterPluginPackageManagementKubernetesRuntime {
readonly pods: ClusterPluginPackageManagementKubernetesPodApi;
openPortForward(
request: Readonly<{
namespace: string;
podName: string;
port: 8443;
}>,
): Promise<ClusterPluginPackageManagementClientRawConnection>;
}
export interface ClusterPluginPackageManagementPortForwardWebSocket {
addEventListener(
type: 'close' | 'error',
listener: () => void,
): void;
close(): void;
}
export interface ClusterPluginPackageManagementPortForwardApi {
portForward(
namespace: string,
podName: string,
targetPorts: number[],
output: Writable,
error: Writable,
input: PassThrough,
retryCount: 0,
): Promise<
| ClusterPluginPackageManagementPortForwardWebSocket
| (() => ClusterPluginPackageManagementPortForwardWebSocket | null)
>;
}
export interface ClusterPluginPackageManagementKubernetesClientPaths
extends ClusterPluginPackageManagementClientPaths {
readonly kubernetesFile: string;
}
export interface ClusterPluginPackageManagementKubernetesClientOptions {
readonly createRuntime?: (
kubeConfig: KubernetesConfig,
kubernetes: KubernetesModule,
) => ClusterPluginPackageManagementKubernetesRuntime;
}
export class ClusterPluginPackageManagementKubernetesClientConfigurationError extends TypeError {
readonly code =
'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_CONFIG_INVALID';
constructor() {
super('Kubernetes Plugin Package management client configuration is invalid');
this.name =
'ClusterPluginPackageManagementKubernetesClientConfigurationError';
}
}
export class ClusterPluginPackageManagementKubernetesClientTunnelError extends Error {
readonly code =
'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_TUNNEL_FAILED';
constructor(readonly cause?: unknown) {
super('Kubernetes Plugin Package management tunnel failed');
this.name = 'ClusterPluginPackageManagementKubernetesClientTunnelError';
}
}
function configurationFailure(): ClusterPluginPackageManagementKubernetesClientConfigurationError {
return new ClusterPluginPackageManagementKubernetesClientConfigurationError();
}
function exactObject(
value: unknown,
expectedKeys: readonly string[],
): asserts value is JsonObject {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw configurationFailure();
}
const actual = Object.keys(value).sort();
const expected = [...expectedKeys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw configurationFailure();
}
}
function decodeUtf8(bytes: Buffer): string {
try {
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch {
throw configurationFailure();
}
}
function parseJson(bytes: Buffer): unknown {
try {
return JSON.parse(decodeUtf8(bytes));
} catch (error) {
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError
) {
throw error;
}
throw configurationFailure();
}
}
function readPrivateFile(filePath: string, maximumBytes: number): Buffer {
try {
return readCanonicalFile(filePath, maximumBytes, 'private');
} catch {
throw configurationFailure();
}
}
function normalizeConfig(
value: unknown,
): Readonly<ReviewedKubernetesClientConfig> {
exactObject(value, [
'schemaVersion',
'kubeconfigFile',
'context',
'namespace',
'apiTimeoutMs',
]);
if (
value.schemaVersion !== 1 ||
typeof value.kubeconfigFile !== 'string' ||
typeof value.context !== 'string' ||
!CONTEXT_PATTERN.test(value.context) ||
typeof value.namespace !== 'string' ||
!DNS_LABEL_PATTERN.test(value.namespace) ||
!Number.isSafeInteger(value.apiTimeoutMs) ||
(value.apiTimeoutMs as number) < 1_000 ||
(value.apiTimeoutMs as number) > 30_000
) {
throw configurationFailure();
}
return Object.freeze({
schemaVersion: 1,
kubeconfigFile: value.kubeconfigFile,
context: value.context,
namespace: value.namespace,
apiTimeoutMs: value.apiTimeoutMs as number,
});
}
function decodeCanonicalBase64(
value: unknown,
maximumBytes: number,
): Buffer {
if (
typeof value !== 'string' ||
value.length < 4 ||
value.length > maximumBytes * 2 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
value,
)
) {
throw configurationFailure();
}
const bytes = Buffer.from(value, 'base64');
if (
bytes.length < 1 ||
bytes.length > maximumBytes ||
bytes.toString('base64') !== value
) {
bytes.fill(0);
throw configurationFailure();
}
return bytes;
}
function validateRawKubeconfig(
value: unknown,
config: Readonly<ReviewedKubernetesClientConfig>,
): void {
exactObject(value, [
'apiVersion',
'kind',
'clusters',
'users',
'contexts',
'current-context',
]);
if (
value.apiVersion !== 'v1' ||
value.kind !== 'Config' ||
value['current-context'] !== config.context ||
!Array.isArray(value.clusters) ||
value.clusters.length !== 1 ||
!Array.isArray(value.users) ||
value.users.length !== 1 ||
!Array.isArray(value.contexts) ||
value.contexts.length !== 1
) {
throw configurationFailure();
}
const clusterEntry = value.clusters[0];
const userEntry = value.users[0];
const contextEntry = value.contexts[0];
exactObject(clusterEntry, ['name', 'cluster']);
const rawCluster = clusterEntry.cluster;
exactObject(rawCluster, [
'server',
'certificate-authority-data',
]);
exactObject(userEntry, ['name', 'user']);
const rawUser = userEntry.user;
if (!rawUser || typeof rawUser !== 'object' || Array.isArray(rawUser)) {
throw configurationFailure();
}
exactObject(contextEntry, ['name', 'context']);
const rawContext = contextEntry.context;
exactObject(rawContext, [
'cluster',
'user',
'namespace',
]);
if (
typeof clusterEntry.name !== 'string' ||
!CONTEXT_PATTERN.test(clusterEntry.name) ||
typeof userEntry.name !== 'string' ||
!CONTEXT_PATTERN.test(userEntry.name) ||
contextEntry.name !== config.context ||
rawContext.cluster !== clusterEntry.name ||
rawContext.user !== userEntry.name ||
rawContext.namespace !== config.namespace
) {
throw configurationFailure();
}
const userKeys = Object.keys(rawUser).sort();
if (
JSON.stringify(userKeys) !== JSON.stringify(['token']) &&
JSON.stringify(userKeys) !==
JSON.stringify(
['client-certificate-data', 'client-key-data'].sort(),
)
) {
throw configurationFailure();
}
}
function validateKubeConfig(
kubeConfig: KubernetesConfig,
config: Readonly<ReviewedKubernetesClientConfig>,
): void {
kubeConfig.setCurrentContext(config.context);
if (kubeConfig.getCurrentContext() !== config.context) {
throw configurationFailure();
}
const context = kubeConfig.getContextObject(config.context);
const cluster = kubeConfig.getCurrentCluster();
const user = kubeConfig.getCurrentUser();
if (
!context ||
context.namespace !== config.namespace ||
!cluster ||
!user
) {
throw configurationFailure();
}
let server: URL;
try {
server = new URL(cluster.server);
} catch {
throw configurationFailure();
}
if (
server.protocol !== 'https:' ||
server.username !== '' ||
server.password !== '' ||
(server.pathname !== '' && server.pathname !== '/') ||
server.search !== '' ||
server.hash !== '' ||
server.hostname.length < 1 ||
cluster.skipTLSVerify !== false ||
cluster.proxyUrl != null ||
cluster.caFile != null ||
typeof cluster.caData !== 'string' ||
(cluster.tlsServerName != null &&
cluster.tlsServerName !== server.hostname)
) {
throw configurationFailure();
}
const ca = decodeCanonicalBase64(
cluster.caData,
MAX_KUBERNETES_CA_BYTES,
);
try {
new X509Certificate(ca);
} catch {
throw configurationFailure();
} finally {
ca.fill(0);
}
if (
user.exec != null ||
user.authProvider != null ||
user.certFile != null ||
user.keyFile != null ||
user.username != null ||
user.password != null ||
user.impersonateUser != null
) {
throw configurationFailure();
}
const hasToken = user.token != null;
const hasCertificate =
user.certData != null || user.keyData != null;
if (
hasToken === hasCertificate ||
(hasToken &&
(typeof user.token !== 'string' ||
Buffer.byteLength(user.token, 'utf8') >
MAX_KUBERNETES_TOKEN_BYTES ||
CONTROL_PATTERN.test(user.token) ||
!TOKEN_PATTERN.test(user.token)))
) {
throw configurationFailure();
}
if (hasCertificate) {
const certificate = decodeCanonicalBase64(
user.certData,
MAX_KUBERNETES_CLIENT_MATERIAL_BYTES,
);
const privateKey = decodeCanonicalBase64(
user.keyData,
MAX_KUBERNETES_CLIENT_MATERIAL_BYTES,
);
try {
const parsedCertificate = new X509Certificate(certificate);
const parsedPrivateKey = createPrivateKey(privateKey);
if (!parsedCertificate.checkPrivateKey(parsedPrivateKey)) {
throw configurationFailure();
}
} catch (error) {
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError
) {
throw error;
}
throw configurationFailure();
} finally {
certificate.fill(0);
privateKey.fill(0);
}
}
}
function isReviewedPod(
value: KubernetesPod,
namespace: string,
): value is KubernetesPod & {
readonly metadata: {
readonly name: string;
readonly namespace: string;
readonly uid: string;
};
} {
const labels = value.metadata?.labels;
return (
typeof value.metadata?.name === 'string' &&
POD_NAME_PATTERN.test(value.metadata.name) &&
value.metadata.namespace === namespace &&
typeof value.metadata.uid === 'string' &&
value.metadata.uid.length >= 8 &&
value.metadata.uid.length <= 128 &&
!CONTROL_PATTERN.test(value.metadata.uid) &&
value.metadata.deletionTimestamp === undefined &&
labels?.['app.kubernetes.io/name'] === MANAGEMENT_NAME &&
labels?.['app.kubernetes.io/component'] ===
'plugin-package-management' &&
value.spec?.serviceAccountName === MANAGEMENT_NAME &&
value.spec?.automountServiceAccountToken === false &&
value.spec?.containers?.some(({ name }) => name === 'management') ===
true &&
value.status?.phase === 'Running' &&
value.status.conditions?.some(
({ type, status }) => type === 'Ready' && status === 'True',
) === true &&
value.status.containerStatuses?.some(
({ name, ready }) => name === 'management' && ready === true,
) === true
);
}
function selectManagementPod(
value: KubernetesPodList,
namespace: string,
): string {
if (
!value ||
typeof value !== 'object' ||
!Array.isArray(value.items) ||
value.items.length < 1 ||
value.items.length > 3 ||
(value.metadata?.continue !== undefined &&
value.metadata.continue !== '')
) {
throw new ClusterPluginPackageManagementKubernetesClientTunnelError();
}
const current = value.items.filter(
({ metadata }) => metadata?.deletionTimestamp === undefined,
);
if (
current.length < 1 ||
current.length > 2 ||
!current.every((pod) => isReviewedPod(pod, namespace))
) {
throw new ClusterPluginPackageManagementKubernetesClientTunnelError();
}
return current
.map(({ metadata }) => metadata!.name!)
.sort()[0]!;
}
function deadline<T>(
operation: Promise<T>,
timeoutMs: number,
disposeLate?: (value: T) => void | Promise<void>,
): Promise<T> {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (settled) return;
settled = true;
reject(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}, timeoutMs);
operation.then(
(value) => {
if (settled) {
void Promise.resolve(disposeLate?.(value)).catch(() => {});
return;
}
settled = true;
clearTimeout(timer);
resolve(value);
},
(error) => {
if (settled) return;
settled = true;
clearTimeout(timer);
reject(
error instanceof
ClusterPluginPackageManagementKubernetesClientTunnelError
? error
: new ClusterPluginPackageManagementKubernetesClientTunnelError(
error,
),
);
},
);
});
}
export async function openClusterPluginPackageManagementPortForward(
forward: ClusterPluginPackageManagementPortForwardApi,
request: Readonly<{
namespace: string;
podName: string;
port: 8443;
}>,
): Promise<ClusterPluginPackageManagementClientRawConnection> {
const incoming = new PassThrough();
const outgoing = new PassThrough();
let connection: Duplex | undefined;
let pendingError = false;
const errors = new Writable({
write(chunk: Buffer | string, _encoding, callback) {
const bytes = Buffer.isBuffer(chunk)
? chunk
: Buffer.from(chunk);
const failed = bytes.length > 0;
bytes.fill(0);
if (failed) {
pendingError = true;
connection?.destroy(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}
callback();
},
});
const handle = await forward.portForward(
request.namespace,
request.podName,
[request.port],
incoming,
errors,
outgoing,
0,
);
const webSocket =
typeof handle === 'function' ? handle() : handle;
if (!webSocket) {
throw new ClusterPluginPackageManagementKubernetesClientTunnelError();
}
const nodeStreamPair = {
readable: incoming,
writable: outgoing,
};
// Node supports a { readable, writable } pair of Node streams here, while
// @types/node@24.13.3 currently models only the equivalent Web Streams pair.
connection = Duplex.from(
nodeStreamPair as unknown as Parameters<typeof Duplex.from>[0],
);
if (pendingError) {
connection.destroy(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}
let closed = false;
const tunnelFailure = () => {
if (!closed) {
connection?.destroy(
new ClusterPluginPackageManagementKubernetesClientTunnelError(),
);
}
};
webSocket.addEventListener('close', tunnelFailure);
webSocket.addEventListener('error', tunnelFailure);
return Object.freeze({
stream: connection,
close() {
if (closed) return;
closed = true;
connection?.end();
incoming.end();
outgoing.end();
errors.end();
webSocket.close();
},
});
}
function productionRuntime(
kubeConfig: KubernetesConfig,
kubernetes: KubernetesModule,
): ClusterPluginPackageManagementKubernetesRuntime {
const pods = kubeConfig.makeApiClient(
kubernetes.CoreV1Api,
) as unknown as ClusterPluginPackageManagementKubernetesPodApi;
const forward = new kubernetes.PortForward(
kubeConfig,
true,
) as unknown as ClusterPluginPackageManagementPortForwardApi;
const runtime: ClusterPluginPackageManagementKubernetesRuntime = {
pods,
openPortForward: (request) =>
openClusterPluginPackageManagementPortForward(
forward,
request,
),
};
return Object.freeze(runtime);
}
export async function executeClusterPluginPackageManagementKubernetesClient(
paths: ClusterPluginPackageManagementKubernetesClientPaths,
options: ClusterPluginPackageManagementKubernetesClientOptions = {},
): Promise<Readonly<ClusterPluginPackageManagementClientResult>> {
exactObject(paths, [
'configFile',
'commandFile',
'assertionFile',
'kubernetesFile',
]);
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some((key) => key !== 'createRuntime') ||
(options.createRuntime !== undefined &&
typeof options.createRuntime !== 'function')
) {
throw configurationFailure();
}
let kubernetesConfigBytes: Buffer | undefined;
let kubeconfigBytes: Buffer | undefined;
try {
kubernetesConfigBytes = readPrivateFile(
paths.kubernetesFile,
MAX_KUBERNETES_CONFIG_BYTES,
);
const config = normalizeConfig(parseJson(kubernetesConfigBytes));
kubeconfigBytes = readPrivateFile(
config.kubeconfigFile,
MAX_KUBECONFIG_BYTES,
);
const kubernetes = await import('@kubernetes/client-node');
const kubeConfig = new kubernetes.KubeConfig();
try {
const kubeconfigText = decodeUtf8(kubeconfigBytes);
validateRawKubeconfig(parseJson(kubeconfigBytes), config);
kubeConfig.loadFromString(kubeconfigText);
validateKubeConfig(kubeConfig, config);
} catch (error) {
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError
) {
throw error;
}
throw configurationFailure();
}
const runtime = (options.createRuntime ?? productionRuntime)(
kubeConfig,
kubernetes,
);
if (
!runtime ||
typeof runtime !== 'object' ||
typeof runtime.pods?.listNamespacedPod !== 'function' ||
typeof runtime.openPortForward !== 'function'
) {
throw configurationFailure();
}
const expectedHostname =
`${MANAGEMENT_NAME}.${config.namespace}.svc`;
return await executeClusterPluginPackageManagementClient(
{
configFile: paths.configFile,
commandFile: paths.commandFile,
assertionFile: paths.assertionFile,
},
{
async connect(target) {
if (
target.hostname !== expectedHostname ||
target.port !== MANAGEMENT_PORT
) {
throw configurationFailure();
}
const list = await deadline(
runtime.pods.listNamespacedPod({
namespace: config.namespace,
labelSelector: MANAGEMENT_LABEL_SELECTOR,
limit: 3,
timeoutSeconds: Math.ceil(config.apiTimeoutMs / 1_000),
watch: false,
}),
config.apiTimeoutMs,
);
const podName = selectManagementPod(
list,
config.namespace,
);
return await deadline(
runtime.openPortForward({
namespace: config.namespace,
podName,
port: MANAGEMENT_PORT,
}),
config.apiTimeoutMs,
async (connection) => {
await connection.close();
},
);
},
},
);
} catch (error) {
if (
error instanceof ClusterPluginPackageManagementClientRequestError &&
error.cause instanceof
ClusterPluginPackageManagementKubernetesClientTunnelError
) {
throw error.cause;
}
if (
error instanceof
ClusterPluginPackageManagementKubernetesClientConfigurationError ||
error instanceof
ClusterPluginPackageManagementKubernetesClientTunnelError ||
error instanceof
ClusterPluginPackageManagementClientConfigurationError ||
error instanceof ClusterPluginPackageManagementClientRequestError ||
error instanceof ClusterPluginPackageManagementClientRemoteError
) {
throw error;
}
throw new ClusterPluginPackageManagementKubernetesClientTunnelError(
error,
);
} finally {
kubernetesConfigBytes?.fill(0);
kubeconfigBytes?.fill(0);
}
}
@@ -0,0 +1,113 @@
#!/usr/bin/env node
/** One-shot Kubernetes-tunneled Plugin Package management client CLI boundary. */
import {
ClusterPluginPackageManagementClientRemoteError,
} from '../../management-support/pluginPackageManagementClient';
import {
executeClusterPluginPackageManagementKubernetesClient,
} from './pluginPackageManagementKubernetesClient';
const USAGE =
'Usage: ql3-plugin-package-client-kubernetes ' +
'--config=/absolute/client.json --command=/absolute/command.json ' +
'--assertion=/absolute/assertion.jwt ' +
'--kubernetes=/absolute/kubernetes.json';
function parseArguments(
argv: readonly string[],
): Readonly<{
configFile: string;
commandFile: string;
assertionFile: string;
kubernetesFile: string;
}> | null {
if (argv.length !== 4) return null;
const values = new Map<string, string>();
for (const argument of argv) {
const match =
/^--(config|command|assertion|kubernetes)=(\/.+)$/.exec(argument);
if (!match || values.has(match[1]!)) return null;
values.set(match[1]!, match[2]!);
}
if (
!values.has('config') ||
!values.has('command') ||
!values.has('assertion') ||
!values.has('kubernetes')
) {
return null;
}
return Object.freeze({
configFile: values.get('config')!,
commandFile: values.get('command')!,
assertionFile: values.get('assertion')!,
kubernetesFile: values.get('kubernetes')!,
});
}
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as { readonly code?: unknown };
return Object.freeze({
schemaVersion: 1,
component:
'qinglong3-plugin-package-management-kubernetes-client',
event: 'command_failed',
code:
typeof candidate?.code === 'string' &&
candidate.code.length <= 128
? candidate.code
: 'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_FAILED',
...(error instanceof ClusterPluginPackageManagementClientRemoteError
? {
statusCode: error.statusCode,
responseCode: error.responseCode,
requestId: error.requestId,
...(error.retryAfterSeconds === null
? {}
: { retryAfterSeconds: error.retryAfterSeconds }),
}
: {}),
});
}
async function run(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
const paths = parseArguments(argv);
if (!paths) {
process.stderr.write(
`${JSON.stringify({
schemaVersion: 1,
component:
'qinglong3-plugin-package-management-kubernetes-client',
event: 'usage_invalid',
code:
'QL3_PLUGIN_PACKAGE_MANAGEMENT_KUBERNETES_CLIENT_USAGE_INVALID',
})}\n`,
);
process.exitCode = 64;
return;
}
try {
const result =
await executeClusterPluginPackageManagementKubernetesClient(paths);
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
component:
'qinglong3-plugin-package-management-kubernetes-client',
event: 'command_completed',
requestId: result.requestId,
result: result.result,
})}\n`,
);
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
}
}
void run(process.argv.slice(2));
@@ -0,0 +1,710 @@
/** Optional bounded Plugin Package management process composition boundary. */
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
} from '@qinglong/runtime-core';
import type {
ObservePluginPackagePublisherTrustSnapshotInput,
ObservePluginPackagePublisherTrustSnapshotResult,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
assertPostgresPackageManagerSchemaReady,
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
PostgresPluginPackageIdentityKeysetLedgerRepository,
PostgresPluginPackageManagementQuotaRepository,
PostgresPluginPackagePublisherTrustAuthorityRepository,
type PostgresConnectionOptions,
type PostgresPoolOptions,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-manager';
import {
createClusterPluginPackageIdentityKeysetFile,
type ClusterPluginPackageIdentityKeysetFile,
type ClusterPluginPackageIdentityKeysetSnapshot,
} from '../../management-support/pluginPackageIdentityKeyset';
import { createClusterPluginPackageManagementService } from './pluginPackageManagement';
import { createClusterPluginPackageLifecycleManagementService } from '../lifecycle/pluginPackageLifecycleManagement';
import {
loadClusterPluginPackagePublisherTrustFileEvidence,
type ClusterPluginPackagePublisherTrustFileEvidence,
} from '../recovery/pluginPackageRecoveryProcess';
import { createClusterPluginPackagePublisherTrustManagementService } from '../publisher/pluginPackagePublisherTrustManagement';
import {
startClusterPluginPackageManagementHttp,
type ClusterPluginPackageManagementHttpApplication,
type StartClusterPluginPackageManagementHttpOptions,
} from '../../management-support/pluginPackageManagementHttp';
import { createClusterPluginPackageManagementTransport } from './pluginPackageManagementTransport';
import {
absoluteManagementEnvironmentFile,
booleanManagementEnvironmentValue,
boundedManagementEnvironmentValue,
integerManagementEnvironmentValue,
readManagementTlsFile,
} from '../../management-support/managementProcessSupport';
const SAFE_HOST = /^[A-Za-z0-9][A-Za-z0-9.:-]{0,254}$/;
const SAFE_APPLICATION_NAME = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/;
export type ClusterPluginPackageManagementProcessEnvironment = Readonly<
Record<string, string | undefined>
>;
export type ClusterPluginPackageManagementProcessConfig =
| Readonly<{
enabled: false;
}>
| Readonly<{
enabled: true;
profile: 'cluster-admin';
host: string;
port: number;
certificateFile: string;
privateKeyFile: string;
identityKeysetFile: string;
publisherTrust: Readonly<{
file: string;
authorityProjectId: string;
authorityId: string;
observerId: string;
}>;
approvalLifetimeMs: number;
quota: Readonly<{
windowMs: number;
proposeLimit: number;
decideLimit: number;
inspectLimit: number;
}>;
http: Readonly<{
maxBodyBytes: number;
maxConnections: number;
maxConcurrentRequests: number;
requestTimeoutMs: number;
drainTimeoutMs: number;
rateWindowMs: number;
peerRequestLimit: number;
globalRequestLimit: number;
maxRateLimitPeers: number;
}>;
database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}>;
}>;
export type ClusterPluginPackageManagementProcessRuntime =
| Readonly<{
status: 'disabled';
close(): Promise<void>;
}>
| Readonly<{
status: 'active';
address: Readonly<{ host: string; port: number }>;
database: PostgresSchemaReadinessReport;
identity: ClusterPluginPackageIdentityKeysetSnapshot;
publisherTrust: Readonly<{
generation: number;
baseSnapshotDigest: string;
effectiveTrustDigest: string;
}>;
availabilityStatus(): 'ready' | 'unavailable' | 'stopped';
close(): Promise<void>;
}>;
export interface StartClusterPluginPackageManagementProcessOptions {
readonly environment: ClusterPluginPackageManagementProcessEnvironment;
readonly openDatabase?: OpenPostgresDatabase;
readonly identities?: ClusterPluginPackageIdentityKeysetFile;
readonly publisherTrustEvidence?: ClusterPluginPackagePublisherTrustFileEvidence;
readonly observePublisherTrust?: (
pool: PostgresDatabaseResource['pool'],
input: ObservePluginPackagePublisherTrustSnapshotInput,
) => Promise<Readonly<ObservePluginPackagePublisherTrustSnapshotResult>>;
readonly assertReady?: (
pool: PostgresDatabaseResource['pool'],
) => Promise<PostgresSchemaReadinessReport>;
readonly startHttp?: (
options: StartClusterPluginPackageManagementHttpOptions,
) => Promise<Readonly<ClusterPluginPackageManagementHttpApplication>>;
readonly now?: () => number;
readonly onError?: (error: unknown) => void;
}
export class ClusterPluginPackageManagementProcessConfigError extends TypeError {
readonly code = 'QL3_PLUGIN_PACKAGE_MANAGEMENT_PROCESS_CONFIG_INVALID';
constructor(message: string) {
super(
`Plugin Package management process configuration is invalid: ${message}`,
);
this.name = 'ClusterPluginPackageManagementProcessConfigError';
}
}
function configFailure(
message: string,
): ClusterPluginPackageManagementProcessConfigError {
return new ClusterPluginPackageManagementProcessConfigError(message);
}
function boundedValue(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
maximumLength: number,
required = false,
): string | undefined {
return boundedManagementEnvironmentValue(
environment,
name,
maximumLength,
configFailure,
required,
);
}
function booleanValue(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
): boolean {
return booleanManagementEnvironmentValue(environment, name, configFailure);
}
function integerValue(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
fallback: number,
minimum: number,
maximum: number,
): number {
return integerManagementEnvironmentValue(
environment,
name,
fallback,
minimum,
maximum,
configFailure,
);
}
function absoluteFile(
environment: ClusterPluginPackageManagementProcessEnvironment,
name: string,
): string {
return absoluteManagementEnvironmentFile(environment, name, configFailure);
}
function loadConnection(
environment: ClusterPluginPackageManagementProcessEnvironment,
): Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}> {
let connection: PostgresConnectionOptions;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_PACKAGE_MANAGER_URL',
host: 'QL3_POSTGRES_PACKAGE_MANAGER_HOST',
port: 'QL3_POSTGRES_PACKAGE_MANAGER_PORT',
database: 'QL3_POSTGRES_PACKAGE_MANAGER_DATABASE',
user: 'QL3_POSTGRES_PACKAGE_MANAGER_USER',
password: 'QL3_POSTGRES_PACKAGE_MANAGER_PASSWORD',
});
} catch (error) {
throw configFailure(
error instanceof Error
? error.message
: 'PostgreSQL Package manager connection is invalid',
);
}
const mode =
environment.QL3_POSTGRES_PACKAGE_MANAGER_TLS_MODE ?? 'verify-full';
if (mode !== 'verify-full' && mode !== 'disable') {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_MODE must be verify-full or disable',
);
}
if (
mode === 'disable' &&
!booleanValue(environment, 'QL3_POSTGRES_PACKAGE_MANAGER_ALLOW_INSECURE')
) {
throw configFailure(
'disabling Package manager PostgreSQL TLS requires QL3_POSTGRES_PACKAGE_MANAGER_ALLOW_INSECURE=true',
);
}
const servername = boundedValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_SERVERNAME',
253,
);
if (mode === 'verify-full' && !isPostgresTlsDnsServername(servername)) {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_SERVERNAME must be an explicit DNS name',
);
}
const caFile = boundedValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_CA_FILE',
4_096,
);
if (mode === 'disable' && caFile !== undefined) {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_CA_FILE cannot be used when TLS is disabled',
);
}
let ca: string | undefined;
if (caFile !== undefined) {
try {
ca = loadPostgresCertificateAuthorityFile(caFile);
} catch {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_TLS_CA_FILE is invalid',
);
}
}
const applicationName =
boundedValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_APPLICATION_NAME',
63,
) ?? 'qinglong3-plugin-package-manager';
if (!SAFE_APPLICATION_NAME.test(applicationName)) {
throw configFailure(
'QL3_POSTGRES_PACKAGE_MANAGER_APPLICATION_NAME is invalid',
);
}
return Object.freeze({
connection: Object.freeze({
...connection,
tls:
mode === 'disable'
? { mode: 'disable' as const }
: {
mode: 'verify-full' as const,
servername: servername!,
...(ca === undefined ? {} : { ca }),
},
}),
pool: Object.freeze({
applicationName,
maxConnections: integerValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_MAX_CONNECTIONS',
2,
1,
4,
),
connectionTimeoutMs: integerValue(
environment,
'QL3_POSTGRES_PACKAGE_MANAGER_CONNECTION_TIMEOUT_MS',
5_000,
100,
60_000,
),
}),
});
}
export function loadClusterPluginPackageManagementProcessConfig(
environment: ClusterPluginPackageManagementProcessEnvironment,
): Readonly<ClusterPluginPackageManagementProcessConfig> {
if (!environment || typeof environment !== 'object') {
throw configFailure('environment is invalid');
}
if (!booleanValue(environment, 'QL3_PLUGIN_PACKAGE_MANAGEMENT_ENABLED')) {
return Object.freeze({ enabled: false as const });
}
if (environment.QL3_PROFILE !== 'cluster-admin') {
throw configFailure(
'QL3_PROFILE must be cluster-admin when management is enabled',
);
}
const host =
boundedValue(environment, 'QL3_PLUGIN_PACKAGE_MANAGEMENT_HOST', 255) ??
'0.0.0.0';
if (!SAFE_HOST.test(host)) {
throw configFailure('QL3_PLUGIN_PACKAGE_MANAGEMENT_HOST is invalid');
}
const http = Object.freeze({
maxBodyBytes: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_BODY_BYTES',
64 * 1024,
1_024,
256 * 1024,
),
maxConnections: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_CONNECTIONS',
64,
1,
512,
),
maxConcurrentRequests: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_CONCURRENT_REQUESTS',
32,
1,
256,
),
requestTimeoutMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_REQUEST_TIMEOUT_MS',
10_000,
1_000,
60_000,
),
drainTimeoutMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_DRAIN_TIMEOUT_MS',
5_000,
100,
60_000,
),
rateWindowMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_RATE_WINDOW_MS',
60_000,
1_000,
5 * 60_000,
),
peerRequestLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_PEER_REQUEST_LIMIT',
60,
1,
10_000,
),
globalRequestLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_GLOBAL_REQUEST_LIMIT',
600,
1,
100_000,
),
maxRateLimitPeers: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_MAX_RATE_LIMIT_PEERS',
1_024,
1,
16_384,
),
});
if (http.globalRequestLimit < http.peerRequestLimit) {
throw configFailure(
'global request limit cannot be below the peer request limit',
);
}
return Object.freeze({
enabled: true as const,
profile: 'cluster-admin' as const,
host,
port: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_PORT',
8_443,
1,
65_535,
),
certificateFile: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_CERT_FILE',
),
privateKeyFile: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_TLS_KEY_FILE',
),
identityKeysetFile: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_IDENTITY_KEYSET_FILE',
),
publisherTrust: Object.freeze({
file: absoluteFile(
environment,
'QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE',
),
authorityProjectId: boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_PROJECT_ID',
128,
true,
)!,
authorityId:
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_ID',
128,
) ?? 'cluster',
observerId:
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_OBSERVER_ID',
128,
) ?? 'cluster-package-manager',
}),
approvalLifetimeMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_APPROVAL_LIFETIME_MS',
15 * 60_000,
1_000,
24 * 60 * 60_000,
),
quota: Object.freeze({
windowMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_QUOTA_WINDOW_MS',
60_000,
1_000,
5 * 60_000,
),
proposeLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_PROPOSE_QUOTA',
30,
1,
1_000,
),
decideLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_DECIDE_QUOTA',
60,
1,
1_000,
),
inspectLimit: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_MANAGEMENT_INSPECT_QUOTA',
600,
1,
1_000,
),
}),
http,
database: loadConnection(environment),
});
}
function readTlsFile(filePath: string, privateMaterial: boolean): Buffer {
return readManagementTlsFile(filePath, privateMaterial, configFailure);
}
export async function startClusterPluginPackageManagementProcess(
options: StartClusterPluginPackageManagementProcessOptions,
): Promise<Readonly<ClusterPluginPackageManagementProcessRuntime>> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
![
'environment',
'openDatabase',
'identities',
'publisherTrustEvidence',
'observePublisherTrust',
'assertReady',
'startHttp',
'now',
'onError',
].includes(key),
) ||
!options.environment ||
typeof options.environment !== 'object' ||
(options.openDatabase !== undefined &&
typeof options.openDatabase !== 'function') ||
(options.identities !== undefined &&
(typeof options.identities.reload !== 'function' ||
typeof options.identities.bind !== 'function')) ||
(options.publisherTrustEvidence !== undefined &&
(!options.publisherTrustEvidence ||
typeof options.publisherTrustEvidence !== 'object' ||
!options.publisherTrustEvidence.registry ||
!options.publisherTrustEvidence.snapshot)) ||
(options.observePublisherTrust !== undefined &&
typeof options.observePublisherTrust !== 'function') ||
(options.assertReady !== undefined &&
typeof options.assertReady !== 'function') ||
(options.startHttp !== undefined &&
typeof options.startHttp !== 'function') ||
(options.now !== undefined && typeof options.now !== 'function') ||
(options.onError !== undefined && typeof options.onError !== 'function')
) {
throw configFailure('options are invalid');
}
const config = loadClusterPluginPackageManagementProcessConfig(
options.environment,
);
if (!config.enabled) {
return Object.freeze({
status: 'disabled' as const,
close: () => Promise.resolve(),
});
}
const now = options.now ?? Date.now;
let http: Readonly<ClusterPluginPackageManagementHttpApplication> | undefined;
let database: PostgresDatabaseResource | undefined;
let unavailableError: unknown;
let closePromise: Promise<void> | undefined;
const report = (error: unknown): void => {
try {
options.onError?.(error);
} catch {
// Diagnostics do not own availability or cleanup.
}
};
const openDatabase =
options.openDatabase ??
createPostgresDatabaseOpener({
role: 'package-manager',
connection: config.database.connection,
pool: config.database.pool,
onPoolError(error) {
const firstAvailabilityError = unavailableError === undefined;
unavailableError ??= error;
http?.withdraw(error);
if (firstAvailabilityError) report(error);
},
});
try {
database = await openDatabase();
const evidence = await (
options.assertReady ?? assertPostgresPackageManagerSchemaReady
)(database.pool);
if (unavailableError !== undefined) throw unavailableError;
const identities =
options.identities ??
createClusterPluginPackageIdentityKeysetFile({
filePath: config.identityKeysetFile,
now,
ledger: new PostgresPluginPackageIdentityKeysetLedgerRepository(
database.pool,
),
});
const identity = await identities.reload();
const publisherTrustEvidence =
options.publisherTrustEvidence ??
loadClusterPluginPackagePublisherTrustFileEvidence(
config.publisherTrust.file,
);
const publisherTrustObservation = await (
options.observePublisherTrust ??
(async (pool, input) =>
new PostgresPluginPackagePublisherTrustAuthorityRepository(
pool,
).observeSnapshot(input))
)(database.pool, {
authorityId: config.publisherTrust.authorityId,
snapshot: publisherTrustEvidence.snapshot,
observedBy: config.publisherTrust.observerId,
observedAtMs: now(),
});
const quota = new PostgresPluginPackageManagementQuotaRepository(
database.pool,
{
windowMs: config.quota.windowMs,
limits: {
'plugin-package.propose': config.quota.proposeLimit,
'plugin-package.decide': config.quota.decideLimit,
'plugin-package.inspect': config.quota.inspectLimit,
},
},
);
const service = createClusterPluginPackageManagementService({
pool: database.pool,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
quota,
});
const lifecycle =
createClusterPluginPackageLifecycleManagementService({
pool: database.pool,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
});
const publisherTrust =
createClusterPluginPackagePublisherTrustManagementService({
pool: database.pool,
authorityProjectId: config.publisherTrust.authorityProjectId,
trustAuthorityId: config.publisherTrust.authorityId,
materialSnapshot: publisherTrustEvidence.snapshot,
approvalLifetimeMs: config.approvalLifetimeMs,
now,
quota,
});
const transport = createClusterPluginPackageManagementTransport({
service,
lifecycle,
publisherTrust,
now,
});
const privateKey = readTlsFile(config.privateKeyFile, true);
try {
const certificate = readTlsFile(config.certificateFile, false);
http = await (
options.startHttp ?? startClusterPluginPackageManagementHttp
)({
host: config.host,
port: config.port,
tls: { privateKey, certificate },
transport,
identities,
limits: config.http,
now,
onError: report,
});
} finally {
privateKey.fill(0);
}
if (unavailableError !== undefined) {
http.withdraw(unavailableError);
}
return Object.freeze({
status: 'active' as const,
address: http.address,
database: evidence,
identity,
publisherTrust: Object.freeze({
generation: publisherTrustObservation.head.generation,
baseSnapshotDigest:
publisherTrustObservation.head.baseSnapshotDigest,
effectiveTrustDigest:
publisherTrustObservation.head.effectiveTrustDigest,
}),
availabilityStatus: () => http?.availabilityStatus() ?? 'stopped',
close(): Promise<void> {
if (closePromise) return closePromise;
closePromise = (async () => {
let primaryError: unknown;
try {
await http?.close();
} catch (error) {
primaryError = error;
}
try {
await database?.close();
} catch (error) {
primaryError ??= error;
}
if (primaryError) throw primaryError;
})();
return closePromise;
},
});
} catch (error) {
try {
await http?.close();
} catch {
// Preserve startup failure.
}
try {
await database?.close();
} catch {
// Preserve startup failure.
}
throw error;
}
}
@@ -0,0 +1,234 @@
// Cluster Plugin Package publisher boundary; keep provenance recovery authority explicit.
import {
assertPluginPackageInstallMatchesLock,
type PluginPackageInstallCommit,
type PluginPackageInstallCreate,
type PluginPackageInstallRecord,
type PluginPackageInstallRecoveryCursor,
type PluginPackageInstallRecoveryPage,
type PluginPackageInstallRepository,
type PluginPackageLock,
} from '@qinglong/runtime-core/plugin-package-install';
import {
normalizePluginPackageStageEvidence,
type PluginPackageStageEvidence,
} from '@qinglong/runtime-core/plugin-package-installation';
import {
createPluginPackagePublisherProvenance,
type PluginPackagePublisherProvenance,
} from '@qinglong/runtime-core/plugin-package-publisher-provenance';
import {
PostgresPluginPackageInstallRepository,
} from '@qinglong/cluster-postgres/plugin-package-install';
import {
POSTGRES_PLUGIN_PACKAGE_PROVENANCE_RECOVERY_PAGE_LIMIT,
PostgresPluginPackagePublisherProvenanceRepository,
type PluginPackagePublisherProvenanceRecoveryCursor,
} from '@qinglong/cluster-postgres/package-executor';
import type { ClusterPluginPackageStageAuthority } from '../recovery/pluginPackageOciStage';
export const MAX_CLUSTER_PLUGIN_PACKAGE_PROVENANCE_RECOVERY_PAGES = 64;
export interface ClusterPluginPackagePublisherProvenanceRecoveryResult {
readonly pages: number;
readonly scanned: number;
readonly created: number;
readonly existing: number;
readonly remaining: boolean;
readonly safeToAdmit: boolean;
}
function stageEvidence(
record: Readonly<PluginPackageInstallRecord>,
): Readonly<PluginPackageStageEvidence> {
if (record.stageReceipt === null) {
throw new TypeError(
'Plugin Package install lacks durable stage evidence for provenance',
);
}
return normalizePluginPackageStageEvidence({
stageRef: record.stageReceipt.stageRef,
artifactDigest: record.stageReceipt.artifactDigest,
manifestDigest: record.stageReceipt.manifestDigest,
contentDigest: record.stageReceipt.contentDigest,
evidenceDigest: record.stageReceipt.evidenceDigest,
});
}
async function provenanceFor(
authority: ClusterPluginPackageStageAuthority,
lock: Readonly<PluginPackageLock>,
record: Readonly<PluginPackageInstallRecord>,
): Promise<Readonly<PluginPackagePublisherProvenance>> {
assertPluginPackageInstallMatchesLock(lock, record);
const stage = stageEvidence(record);
const signature = await authority.publisherEvidence(lock, stage);
return createPluginPackagePublisherProvenance({
projectId: record.projectId,
packageName: record.packageName,
installationId: record.installationId,
lockDigest: record.lockDigest,
artifactDigest: stage.artifactDigest,
manifestDigest: stage.manifestDigest,
contentDigest: stage.contentDigest,
stageEvidenceDigest: stage.evidenceDigest,
signature,
});
}
export class ClusterPluginPackageProvenanceInstallRepository
implements PluginPackageInstallRepository
{
constructor(
private readonly installs: PostgresPluginPackageInstallRepository,
private readonly provenance: PostgresPluginPackagePublisherProvenanceRepository,
private readonly authority: ClusterPluginPackageStageAuthority,
private readonly trustAuthorityId: string,
) {
if (
!(installs instanceof PostgresPluginPackageInstallRepository) ||
!(provenance instanceof
PostgresPluginPackagePublisherProvenanceRepository) ||
!authority ||
typeof authority.publisherEvidence !== 'function' ||
typeof trustAuthorityId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(trustAuthorityId)
) {
throw new TypeError(
'Cluster Plugin Package provenance install repository is invalid',
);
}
}
find(
projectId: string,
packageName: string,
): Promise<Readonly<PluginPackageInstallRecord> | null> {
return this.installs.find(projectId, packageName);
}
findLock(
lockDigest: string,
): Promise<Readonly<PluginPackageLock> | null> {
return this.installs.findLock(lockDigest);
}
create(command: Readonly<PluginPackageInstallCreate>): Promise<
Readonly<{
status: 'created' | 'existing';
record: Readonly<PluginPackageInstallRecord>;
}>
> {
return this.installs.create(command);
}
async commit(command: Readonly<PluginPackageInstallCommit>): Promise<
Readonly<{
status: 'committed' | 'existing';
record: Readonly<PluginPackageInstallRecord>;
}>
> {
if (command.record.state !== 'staged') {
return this.installs.commit(command);
}
const lock = await this.installs.findLock(command.record.lockDigest);
if (!lock) {
throw new TypeError('Plugin Package stage lock is unavailable');
}
return this.provenance.commitStage(
command,
await provenanceFor(this.authority, lock, command.record),
this.trustAuthorityId,
);
}
listRecoveryPage(options: {
readonly limit: number;
readonly after?: Readonly<PluginPackageInstallRecoveryCursor>;
}): Promise<Readonly<PluginPackageInstallRecoveryPage>> {
return this.installs.listRecoveryPage(options);
}
}
export async function recoverClusterPluginPackagePublisherProvenance(
installs: PostgresPluginPackageInstallRepository,
repository: PostgresPluginPackagePublisherProvenanceRepository,
authority: ClusterPluginPackageStageAuthority,
options: Readonly<{
trustAuthorityId: string;
pageSize?: number;
maxPages?: number;
}>,
): Promise<Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>> {
const pageSize = options.pageSize ?? 16;
const maxPages = options.maxPages ?? 16;
if (
!(installs instanceof PostgresPluginPackageInstallRepository) ||
!(repository instanceof
PostgresPluginPackagePublisherProvenanceRepository) ||
!authority ||
typeof authority.verify !== 'function' ||
typeof authority.publisherEvidence !== 'function' ||
typeof options.trustAuthorityId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(
options.trustAuthorityId,
) ||
!Number.isSafeInteger(pageSize) ||
pageSize < 1 ||
pageSize > POSTGRES_PLUGIN_PACKAGE_PROVENANCE_RECOVERY_PAGE_LIMIT ||
!Number.isSafeInteger(maxPages) ||
maxPages < 1 ||
maxPages > MAX_CLUSTER_PLUGIN_PACKAGE_PROVENANCE_RECOVERY_PAGES
) {
throw new TypeError(
'Cluster Plugin Package provenance recovery configuration is invalid',
);
}
let after:
| Readonly<PluginPackagePublisherProvenanceRecoveryCursor>
| undefined;
const counts = {
pages: 0,
scanned: 0,
created: 0,
existing: 0,
};
let exhausted = false;
while (counts.pages < maxPages) {
const page = await repository.listMissingPage({
limit: pageSize,
...(after ? { after } : {}),
});
counts.pages += 1;
for (const record of page.records) {
const lock = await installs.findLock(record.lockDigest);
if (!lock || record.stageReceipt === null) {
throw new TypeError(
'Cluster Plugin Package provenance recovery source is incomplete',
);
}
assertPluginPackageInstallMatchesLock(lock, record);
await authority.verify(lock, record.stageReceipt);
const result = await repository.recordExisting(
record,
await provenanceFor(authority, lock, record),
options.trustAuthorityId,
);
counts.scanned += 1;
counts[result.status] += 1;
}
if (!page.truncated) {
exhausted = true;
break;
}
after = page.next;
}
const probe = await repository.listMissingPage({ limit: 1 });
const remaining = !exhausted || probe.records.length > 0;
return Object.freeze({
...counts,
remaining,
safeToAdmit: !remaining,
});
}
@@ -0,0 +1,203 @@
// Cluster Plugin Package publisher boundary; keep revocation authority explicit.
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
} from '@qinglong/runtime-core';
import {
normalizePluginPackagePublisherRevocationReceipt,
type PluginPackagePublisherRevocationReceipt,
} from '@qinglong/runtime-core/plugin-package-publisher-provenance';
import {
createPluginPackageQuarantineEvent,
pluginPackageQuarantineMutationId,
} from '@qinglong/runtime-core/plugin-package-quarantine';
import {
PostgresPluginPackagePublisherProvenanceRepository,
PostgresPluginPackageQuarantineRepository,
assertPostgresPackageExecutorSchemaReady,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-executor';
import {
CLUSTER_PLUGIN_PACKAGE_QUARANTINE_BATCH_LIMIT,
createClusterPluginPackageQuarantineService,
} from '../lifecycle/pluginPackageQuarantine';
export const MAX_CLUSTER_PLUGIN_PACKAGE_REVOCATION_PAGES = 64;
export interface RunClusterPluginPackagePublisherRevocationOptions {
readonly openDatabase: OpenPostgresDatabase;
readonly receipt: Readonly<PluginPackagePublisherRevocationReceipt>;
readonly confirmAuthorization: (
receipt: Readonly<PluginPackagePublisherRevocationReceipt>,
) => void | Promise<void>;
readonly pageSize?: number;
readonly maxPages?: number;
}
export interface ClusterPluginPackagePublisherRevocationRun {
readonly database: PostgresSchemaReadinessReport;
readonly receiptStatus: 'created' | 'existing';
readonly receiptDigest: string;
readonly impactDigest: string;
readonly impacted: number;
readonly pages: number;
readonly quarantined: number;
readonly existing: number;
readonly remaining: boolean;
readonly safeToAdmit: boolean;
}
function normalizedOptions(
options: RunClusterPluginPackagePublisherRevocationOptions,
): Readonly<{
receipt: Readonly<PluginPackagePublisherRevocationReceipt>;
pageSize: number;
maxPages: number;
}> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
![
'openDatabase',
'receipt',
'confirmAuthorization',
'pageSize',
'maxPages',
].includes(key),
) ||
typeof options.openDatabase !== 'function' ||
typeof options.confirmAuthorization !== 'function'
) {
throw new TypeError(
'Cluster Plugin Package publisher revocation options are invalid',
);
}
const pageSize = options.pageSize ?? 64;
const maxPages = options.maxPages ?? 32;
if (
!Number.isSafeInteger(pageSize) ||
pageSize < 1 ||
pageSize > CLUSTER_PLUGIN_PACKAGE_QUARANTINE_BATCH_LIMIT ||
!Number.isSafeInteger(maxPages) ||
maxPages < 1 ||
maxPages > MAX_CLUSTER_PLUGIN_PACKAGE_REVOCATION_PAGES
) {
throw new TypeError(
'Cluster Plugin Package publisher revocation bounds are invalid',
);
}
return Object.freeze({
receipt: normalizePluginPackagePublisherRevocationReceipt(options.receipt),
pageSize,
maxPages,
});
}
/**
* Short-lived administration composition. The immutable revocation receipt
* and its bounded impact are committed before quarantine materialization.
* Re-running the same receipt converges on the same facts and skips targets
* already quarantined or superseded by a newer installation head.
*/
export async function runClusterPluginPackagePublisherRevocation(
options: RunClusterPluginPackagePublisherRevocationOptions,
): Promise<Readonly<ClusterPluginPackagePublisherRevocationRun>> {
const normalized = normalizedOptions(options);
let database: PostgresDatabaseResource | undefined;
let result: Readonly<ClusterPluginPackagePublisherRevocationRun> | undefined;
let failure: unknown;
try {
database = await options.openDatabase();
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const provenance =
new PostgresPluginPackagePublisherProvenanceRepository(database.pool);
const quarantine = createClusterPluginPackageQuarantineService(
new PostgresPluginPackageQuarantineRepository(database.pool),
);
const impactResult = await provenance.recordRevocationImpact(
normalized.receipt,
() => options.confirmAuthorization(normalized.receipt),
);
let pages = 0;
let quarantined = 0;
let existing = 0;
while (pages < normalized.maxPages) {
const page = await provenance.listPendingQuarantineTargets(
impactResult.impact.impactDigest,
normalized.pageSize,
);
if (page.targets.length === 0) break;
const events = page.targets.map((target) =>
createPluginPackageQuarantineEvent({
mutationId: pluginPackageQuarantineMutationId(
normalized.receipt.receiptDigest,
target,
),
revocationReceiptDigest: normalized.receipt.receiptDigest,
impactDigest: impactResult.impact.impactDigest,
target,
proposer: normalized.receipt.proposer,
confirmer: normalized.receipt.confirmer,
authorizationMode: normalized.receipt.authorizationMode,
reasonCode: normalized.receipt.reasonCode,
occurredAtMs: normalized.receipt.revokedAtMs,
}),
);
const quarantineResults = await quarantine.quarantine(
events,
() => options.confirmAuthorization(normalized.receipt),
);
pages += 1;
for (const item of quarantineResults) {
if (item.status === 'created') quarantined += 1;
else existing += 1;
}
}
const probe = await provenance.listPendingQuarantineTargets(
impactResult.impact.impactDigest,
1,
);
const remaining = probe.targets.length > 0;
result = Object.freeze({
database: evidence,
receiptStatus: impactResult.status,
receiptDigest: normalized.receipt.receiptDigest,
impactDigest: impactResult.impact.impactDigest,
impacted: impactResult.impact.items.length,
pages,
quarantined,
existing,
remaining,
safeToAdmit: !remaining,
});
} catch (error) {
failure = error;
}
if (database) {
try {
await database.close();
} catch (closeError) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Cluster Plugin Package publisher revocation failed and PostgreSQL did not close',
);
}
throw closeError;
}
}
if (failure !== undefined) throw failure;
if (!result) {
throw new Error(
'Cluster Plugin Package publisher revocation produced no result',
);
}
return result;
}
@@ -0,0 +1,172 @@
// Cluster Plugin Package publisher boundary; keep approval consumption authority explicit.
import { createHash } from 'node:crypto';
import { PostgresApprovalRequestRepository } from '@qinglong/cluster-postgres/approved-action';
import {
PostgresPluginPackagePublisherRevocationProposalRepository,
} from '@qinglong/cluster-postgres/package-executor';
import { PostgresProjectPolicyRepository } from '@qinglong/cluster-postgres/project-policy';
import type { PostgresPool } from '@qinglong/runtime-core';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
export const CLUSTER_PLUGIN_PACKAGE_PUBLISHER_APPROVAL_BATCH_LIMIT = 16;
export interface ConsumeClusterPluginPackagePublisherRevocationApprovalsOptions {
readonly pool: PostgresPool;
readonly now?: () => number;
readonly limit?: number;
}
export interface ClusterPluginPackagePublisherRevocationApprovalSummary {
readonly scanned: number;
readonly consumed: number;
readonly existing: number;
readonly expired: number;
readonly blocked: number;
}
function stableDigest(domain: string, value: string): string {
return createHash('sha256')
.update(domain)
.update('\0')
.update(value)
.digest('hex');
}
function stableId(prefix: string, domain: string, value: string): string {
return `${prefix}-${stableDigest(domain, value)}`;
}
function stableAuditEventId(requestId: string): string {
const bytes = Buffer.from(
stableDigest(
'qinglong/plugin-package-publisher-revocation-consume-audit@v1',
requestId,
),
'hex',
);
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
12,
16,
)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
}
export async function consumeClusterPluginPackagePublisherRevocationApprovals(
options: ConsumeClusterPluginPackagePublisherRevocationApprovalsOptions,
): Promise<
Readonly<ClusterPluginPackagePublisherRevocationApprovalSummary>
> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) => !['pool', 'now', 'limit'].includes(key),
) ||
!options.pool ||
typeof options.pool.query !== 'function' ||
typeof options.pool.connect !== 'function' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError(
'publisher revocation approval consumer options are invalid',
);
}
const limit =
options.limit ?? CLUSTER_PLUGIN_PACKAGE_PUBLISHER_APPROVAL_BATCH_LIMIT;
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 64) {
throw new TypeError(
'publisher revocation approval consumer limit is invalid',
);
}
const now = options.now ?? Date.now;
const observedAtMs = now();
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
throw new TypeError(
'publisher revocation approval consumer clock is invalid',
);
}
const proposals =
new PostgresPluginPackagePublisherRevocationProposalRepository(
options.pool,
);
const approvals = new PostgresApprovalRequestRepository(options.pool);
const policy = new ProjectPolicyEngine(
new PostgresProjectPolicyRepository(options.pool),
);
const requests = await proposals.listApprovedRequests(limit);
let consumed = 0;
let existing = 0;
let expired = 0;
let blocked = 0;
for (const request of requests) {
if (observedAtMs >= request.expiresAtMs) {
expired += 1;
continue;
}
const decision = await policy.decide({
subject: request.requestedBy,
projectId: request.projectId,
permission: 'package.manage',
});
if (
decision.fence === null ||
(decision.effect !== 'allow' &&
decision.effect !== 'require_approval')
) {
blocked += 1;
continue;
}
const consumptionId = stableId(
'pprc',
'qinglong/plugin-package-publisher-revocation-consumption@v1',
request.id,
);
const dispatchId = stableId(
'pprd',
'qinglong/plugin-package-publisher-revocation-dispatch@v1',
request.id,
);
const result = await approvals.consume({
requestId: request.id,
expectedVersion: request.version,
consumptionId,
dispatchId,
action: request.action,
requestedBy: request.requestedBy,
consumedBy: {
type: 'system',
id: 'cluster_package_executor',
},
consumedAtMs: observedAtMs,
authorizationFence: decision.fence,
audit: {
eventId: stableAuditEventId(request.id),
requestId: request.id,
operationId: 'approval.consume',
projectId: request.projectId,
subject: {
type: 'system',
id: 'cluster_package_executor',
},
authenticationId: 'cluster-package-executor',
outcome: 'allowed',
reasons: ['publisher_revocation_execution'],
fence: decision.fence,
occurredAtMs: observedAtMs,
},
});
if (result.status === 'consumed') consumed += 1;
else existing += 1;
}
return Object.freeze({
scanned: requests.length,
consumed,
existing,
expired,
blocked,
});
}
@@ -0,0 +1,160 @@
// Cluster Plugin Package publisher boundary; keep Approved Action authority explicit.
import {
type ApprovedActionHandler,
type ApprovedActionHandlerExecutionContext,
type ApprovedActionHandlerInspection,
type ApprovedActionHandlerResult,
} from '@qinglong/runtime-core/approved-action-dispatcher';
import {
PLUGIN_PACKAGE_PUBLISHER_REVOCATION_ACTION_TYPE,
PluginPackagePublisherRevocationProposalBindingConflictError,
normalizePluginPackagePublisherRevocationProposal,
resolvePluginPackagePublisherRevocationProposal,
type PluginPackagePublisherRevocationProposalRepository,
} from '@qinglong/runtime-core/plugin-package-publisher-revocation-proposal';
import type {
PluginPackagePublisherRevocationReceipt,
} from '@qinglong/runtime-core/plugin-package-publisher-provenance';
export interface ClusterPluginPackagePublisherRevocationExecutionResult {
readonly safeToAdmit: boolean;
readonly receiptDigest: string;
readonly impactDigest: string;
}
export interface ClusterPluginPackagePublisherRevocationExecutionPort {
run(
receipt: Readonly<PluginPackagePublisherRevocationReceipt>,
): Promise<
Readonly<ClusterPluginPackagePublisherRevocationExecutionResult>
>;
}
export class ClusterPluginPackagePublisherRevocationApprovedActionHandler
implements ApprovedActionHandler
{
readonly actionType = PLUGIN_PACKAGE_PUBLISHER_REVOCATION_ACTION_TYPE;
constructor(
readonly proposals: PluginPackagePublisherRevocationProposalRepository,
readonly revocations: ClusterPluginPackagePublisherRevocationExecutionPort,
) {
if (
!proposals ||
typeof proposals.findProposalByActionRef !== 'function' ||
!revocations ||
typeof revocations.run !== 'function'
) {
throw new TypeError(
'publisher revocation Approved Action authority is invalid',
);
}
}
async inspect(
dispatch: ApprovedActionHandlerExecutionContext['dispatch'],
): Promise<ApprovedActionHandlerInspection> {
let proposal;
try {
proposal = await this.proposals.findProposalByActionRef(
dispatch.action.actionRef,
);
} catch {
return Object.freeze({
status: 'retry',
resultCode: 'publisher_revocation_proposal_unavailable',
});
}
if (!proposal) {
return Object.freeze({
status: 'blocked',
resultCode: 'publisher_revocation_proposal_missing',
});
}
try {
const normalized =
normalizePluginPackagePublisherRevocationProposal(proposal);
resolvePluginPackagePublisherRevocationProposal(
normalized,
dispatch,
dispatch.createdAtMs,
);
return Object.freeze({
status: 'ready',
actionDigest: normalized.actionDigest,
});
} catch {
return Object.freeze({
status: 'blocked',
resultCode: 'publisher_revocation_proposal_rejected',
});
}
}
async execute(
context: Readonly<ApprovedActionHandlerExecutionContext>,
): Promise<Readonly<ApprovedActionHandlerResult>> {
const startedAtMs = context.execution.startedAtMs;
if (
context.execution.status !== 'executing' ||
startedAtMs === null ||
context.execution.leaseOwner !== context.fence.owner ||
context.execution.leaseToken !== context.fence.leaseToken ||
context.execution.version !== context.fence.version
) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_revocation_execution_rejected',
});
}
const proposal = await this.proposals.findProposalByActionRef(
context.dispatch.action.actionRef,
);
if (!proposal) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_revocation_proposal_missing',
});
}
let receipt;
try {
receipt = resolvePluginPackagePublisherRevocationProposal(
proposal,
context.dispatch,
startedAtMs,
);
} catch (error) {
if (
error instanceof
PluginPackagePublisherRevocationProposalBindingConflictError
) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_revocation_proposal_rejected',
});
}
throw error;
}
const result = await this.revocations.run(receipt);
if (!result.safeToAdmit) {
return Object.freeze({
outcome: 'indeterminate',
resultCode: 'publisher_revocation_convergence_incomplete',
});
}
if (
result.receiptDigest !== receipt.receiptDigest ||
!/^[0-9a-f]{64}$/.test(result.impactDigest)
) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_revocation_result_rejected',
});
}
return Object.freeze({
outcome: 'succeeded',
resultCode: 'publisher_revocation_converged',
resultDigest: result.impactDigest,
});
}
}
@@ -0,0 +1,178 @@
// Cluster Plugin Package publisher boundary; keep transition approval authority explicit.
import { createHash } from 'node:crypto';
import { PostgresApprovalRequestRepository } from '@qinglong/cluster-postgres/approved-action';
import {
PostgresPluginPackagePublisherTrustTransitionProposalRepository,
} from '@qinglong/cluster-postgres/package-executor';
import { PostgresProjectPolicyRepository } from '@qinglong/cluster-postgres/project-policy';
import type { PostgresPool } from '@qinglong/runtime-core';
import { ProjectPolicyEngine } from '@qinglong/runtime-core/project-policy';
export const CLUSTER_PLUGIN_PACKAGE_PUBLISHER_TRUST_TRANSITION_APPROVAL_BATCH_LIMIT =
16;
export interface ConsumeClusterPluginPackagePublisherTrustTransitionApprovalsOptions {
readonly pool: PostgresPool;
readonly now?: () => number;
readonly limit?: number;
}
export interface ClusterPluginPackagePublisherTrustTransitionApprovalSummary {
readonly scanned: number;
readonly consumed: number;
readonly existing: number;
readonly expired: number;
readonly blocked: number;
}
function stableDigest(domain: string, value: string): string {
return createHash('sha256')
.update(domain)
.update('\0')
.update(value)
.digest('hex');
}
function stableId(prefix: string, domain: string, value: string): string {
return `${prefix}-${stableDigest(domain, value)}`;
}
function stableAuditEventId(requestId: string): string {
const bytes = Buffer.from(
stableDigest(
'qinglong/plugin-package-publisher-trust-transition-consume-audit@v1',
requestId,
),
'hex',
);
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = bytes.toString('hex');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
12,
16,
)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
}
export async function consumeClusterPluginPackagePublisherTrustTransitionApprovals(
options: ConsumeClusterPluginPackagePublisherTrustTransitionApprovalsOptions,
): Promise<
Readonly<ClusterPluginPackagePublisherTrustTransitionApprovalSummary>
> {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) => !['pool', 'now', 'limit'].includes(key),
) ||
!options.pool ||
typeof options.pool.query !== 'function' ||
typeof options.pool.connect !== 'function' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new TypeError(
'publisher trust transition approval consumer options are invalid',
);
}
const limit =
options.limit ??
CLUSTER_PLUGIN_PACKAGE_PUBLISHER_TRUST_TRANSITION_APPROVAL_BATCH_LIMIT;
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 64) {
throw new TypeError(
'publisher trust transition approval consumer limit is invalid',
);
}
const now = options.now ?? Date.now;
const observedAtMs = now();
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
throw new TypeError(
'publisher trust transition approval consumer clock is invalid',
);
}
const proposals =
new PostgresPluginPackagePublisherTrustTransitionProposalRepository(
options.pool,
);
const approvals = new PostgresApprovalRequestRepository(options.pool);
const policy = new ProjectPolicyEngine(
new PostgresProjectPolicyRepository(options.pool),
);
const requests = await proposals.listApprovedRequests(limit);
let consumed = 0;
let existing = 0;
let expired = 0;
let blocked = 0;
for (const request of requests) {
if (observedAtMs >= request.expiresAtMs) {
expired += 1;
continue;
}
if (request.decisionMode !== 'separation_of_duty') {
blocked += 1;
continue;
}
const decision = await policy.decide({
subject: request.requestedBy,
projectId: request.projectId,
permission: 'package.manage',
});
if (
decision.fence === null ||
(decision.effect !== 'allow' &&
decision.effect !== 'require_approval')
) {
blocked += 1;
continue;
}
const consumptionId = stableId(
'ppttc',
'qinglong/plugin-package-publisher-trust-transition-consumption@v1',
request.id,
);
const dispatchId = stableId(
'ppttd',
'qinglong/plugin-package-publisher-trust-transition-dispatch@v1',
request.id,
);
const result = await approvals.consume({
requestId: request.id,
expectedVersion: request.version,
consumptionId,
dispatchId,
action: request.action,
requestedBy: request.requestedBy,
consumedBy: {
type: 'system',
id: 'cluster_package_executor',
},
consumedAtMs: observedAtMs,
authorizationFence: decision.fence,
audit: {
eventId: stableAuditEventId(request.id),
requestId: request.id,
operationId: 'approval.consume',
projectId: request.projectId,
subject: {
type: 'system',
id: 'cluster_package_executor',
},
authenticationId: 'cluster-package-executor',
outcome: 'allowed',
reasons: ['publisher_trust_transition_execution'],
fence: decision.fence,
occurredAtMs: observedAtMs,
},
});
if (result.status === 'consumed') consumed += 1;
else existing += 1;
}
return Object.freeze({
scanned: requests.length,
consumed,
existing,
expired,
blocked,
});
}
@@ -0,0 +1,167 @@
// Cluster Plugin Package publisher boundary; keep transition execution authority explicit.
import {
type ApprovedActionHandler,
type ApprovedActionHandlerExecutionContext,
type ApprovedActionHandlerInspection,
type ApprovedActionHandlerResult,
} from '@qinglong/runtime-core/approved-action-dispatcher';
import {
PLUGIN_PACKAGE_PUBLISHER_TRUST_TRANSITION_ACTION_TYPES,
PluginPackagePublisherTrustTransitionBindingConflictError,
PluginPackagePublisherTrustTransitionConflictError,
normalizePluginPackagePublisherTrustTransitionProposal,
resolvePluginPackagePublisherTrustTransitionProposal,
type PluginPackagePublisherTrustTransitionMode,
type PluginPackagePublisherTrustTransitionProposalRepository,
type PluginPackagePublisherTrustTransitionReceipt,
} from '@qinglong/runtime-core/plugin-package-publisher-trust-transition-proposal';
export interface ClusterPluginPackagePublisherTrustTransitionExecutionResult {
readonly status: 'created' | 'existing';
readonly receipt: Readonly<PluginPackagePublisherTrustTransitionReceipt>;
readonly head: Readonly<{
generation: number;
effectiveTrustDigest: string;
}>;
}
export interface ClusterPluginPackagePublisherTrustTransitionExecutionPort {
applyApprovedTransition(
input: Readonly<{
dispatch: ApprovedActionHandlerExecutionContext['dispatch'];
executedAtMs: number;
}>,
): Promise<
Readonly<ClusterPluginPackagePublisherTrustTransitionExecutionResult>
>;
}
export class ClusterPluginPackagePublisherTrustTransitionApprovedActionHandler
implements ApprovedActionHandler
{
readonly actionType:
(typeof PLUGIN_PACKAGE_PUBLISHER_TRUST_TRANSITION_ACTION_TYPES)[PluginPackagePublisherTrustTransitionMode];
constructor(
readonly mode: PluginPackagePublisherTrustTransitionMode,
readonly proposals: PluginPackagePublisherTrustTransitionProposalRepository,
readonly transitions: ClusterPluginPackagePublisherTrustTransitionExecutionPort,
) {
this.actionType =
PLUGIN_PACKAGE_PUBLISHER_TRUST_TRANSITION_ACTION_TYPES[mode];
if (
(mode !== 'overlap_add' && mode !== 'safe_retire') ||
!proposals ||
typeof proposals.findProposalByActionRef !== 'function' ||
!transitions ||
typeof transitions.applyApprovedTransition !== 'function'
) {
throw new TypeError(
'publisher trust transition Approved Action authority is invalid',
);
}
}
async inspect(
dispatch: ApprovedActionHandlerExecutionContext['dispatch'],
): Promise<ApprovedActionHandlerInspection> {
let proposal;
try {
proposal = await this.proposals.findProposalByActionRef(
dispatch.action.actionRef,
);
} catch {
return Object.freeze({
status: 'retry',
resultCode: 'publisher_trust_transition_proposal_unavailable',
});
}
if (!proposal) {
return Object.freeze({
status: 'blocked',
resultCode: 'publisher_trust_transition_proposal_missing',
});
}
try {
const normalized =
normalizePluginPackagePublisherTrustTransitionProposal(proposal);
if (
normalized.actionInput.mode !== this.mode ||
normalized.actionType !== this.actionType
) {
throw new PluginPackagePublisherTrustTransitionBindingConflictError();
}
resolvePluginPackagePublisherTrustTransitionProposal(
normalized,
dispatch,
dispatch.createdAtMs,
this.mode === 'safe_retire' ? 0 : null,
);
return Object.freeze({
status: 'ready',
actionDigest: normalized.actionDigest,
});
} catch {
return Object.freeze({
status: 'blocked',
resultCode: 'publisher_trust_transition_proposal_rejected',
});
}
}
async execute(
context: Readonly<ApprovedActionHandlerExecutionContext>,
): Promise<Readonly<ApprovedActionHandlerResult>> {
const startedAtMs = context.execution.startedAtMs;
if (
context.execution.status !== 'executing' ||
startedAtMs === null ||
context.execution.leaseOwner !== context.fence.owner ||
context.execution.leaseToken !== context.fence.leaseToken ||
context.execution.version !== context.fence.version
) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_trust_transition_execution_rejected',
});
}
try {
const result = await this.transitions.applyApprovedTransition({
dispatch: context.dispatch,
executedAtMs: startedAtMs,
});
if (
result.receipt.mode !== this.mode ||
result.receipt.mutationId !== context.dispatch.id ||
result.head.generation !== result.receipt.currentGeneration ||
result.head.effectiveTrustDigest !==
result.receipt.currentTrustDigest
) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_trust_transition_result_rejected',
});
}
return Object.freeze({
outcome: 'succeeded',
resultCode:
this.mode === 'overlap_add'
? 'publisher_trust_overlap_added'
: 'publisher_trust_key_retired',
resultDigest: result.receipt.receiptDigest,
});
} catch (error) {
if (
error instanceof
PluginPackagePublisherTrustTransitionBindingConflictError ||
error instanceof PluginPackagePublisherTrustTransitionConflictError
) {
return Object.freeze({
outcome: 'failed',
resultCode: 'publisher_trust_transition_conflict',
});
}
throw error;
}
}
}
@@ -0,0 +1,565 @@
// Cluster Plugin Package recovery boundary; keep Kubernetes activation authority explicit.
import { createHash } from 'node:crypto';
import {
PluginPackageActivationConflictError,
PluginPackageActivationUnavailableError,
normalizePluginPackageActivationIntent,
type PluginPackageActivationIntent,
type PluginPackageActivationObservation,
type PluginPackageActivationPublisher,
} from '@qinglong/runtime-core/plugin-package-activation';
import type {
PluginPackageResourceGeneration,
PluginPackageResourceGenerationSource,
} from '@qinglong/runtime-core/plugin-package-resource-generation';
import {
createPluginPackageActivationReceipt,
normalizePluginPackageActivationReceipt,
type PluginPackageActivationReceipt,
} from '@qinglong/runtime-core/plugin-package-install';
const ACTIVE_POINTER_SCHEMA =
'qinglong/plugin-package-kubernetes-active-pointer@v2';
const ACTIVE_POINTER_KEY = 'active.json';
const MANAGED_BY_LABEL = 'app.kubernetes.io/managed-by';
const MANAGED_BY_VALUE = 'qinglong3';
const ACTIVE_LABEL = 'qinglong.io/plugin-package-active';
const TARGET_LABEL = 'qinglong.io/plugin-package-target';
const INTENT_ANNOTATION = 'qinglong.io/plugin-package-intent';
const FIELD_MANAGER = 'qinglong-plugin-package-activation';
const MAX_ACTIVE_POINTER_BYTES = 512 * 1024;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const SAFE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
const RESOURCE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/+=-]{0,511}$/;
const DIGEST = /^[0-9a-f]{64}$/;
const TARGET_DIGEST_DOMAIN = Buffer.from(
'qinglong/plugin-package-kubernetes-target@v1\0',
'utf8',
);
export interface ClusterPluginPackageStageEvidence {
readonly lockDigest: string;
readonly stageRef: string;
readonly stageReceiptDigest: string;
readonly stageEvidenceDigest: string;
readonly contentDigest: string;
}
export interface ClusterPluginPackageStageEvidenceVerifier {
verify(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<ClusterPluginPackageStageEvidence>>;
}
export interface PluginPackageKubernetesActivationPublisherOptions {
/** Stable operator-reviewed identity for one Kubernetes API cluster. */
readonly clusterIdentity: string;
readonly namespace: string;
/** Explicit authoritative clock called only for a new publication attempt. */
readonly now: () => number | Promise<number>;
}
export interface PluginPackageKubernetesConfigMap {
readonly apiVersion?: string;
readonly kind?: string;
readonly immutable?: boolean;
readonly data?: Readonly<Record<string, string>>;
readonly binaryData?: Readonly<Record<string, string>>;
readonly metadata?: Readonly<{
name?: string;
namespace?: string;
uid?: string;
resourceVersion?: string;
deletionTimestamp?: Date;
finalizers?: readonly string[];
ownerReferences?: readonly Readonly<Record<string, unknown>>[];
labels?: Readonly<Record<string, string>>;
annotations?: Readonly<Record<string, string>>;
}>;
}
interface ConfigMapWrite extends PluginPackageKubernetesConfigMap {
readonly metadata: NonNullable<PluginPackageKubernetesConfigMap['metadata']>;
readonly data: Readonly<Record<string, string>>;
}
export interface PluginPackageKubernetesConfigMapApi {
readNamespacedConfigMap(
request: Readonly<{
name: string;
namespace: string;
}>,
): Promise<PluginPackageKubernetesConfigMap>;
createNamespacedConfigMap(
request: Readonly<{
namespace: string;
body: ConfigMapWrite;
fieldManager: string;
fieldValidation: 'Strict';
}>,
): Promise<PluginPackageKubernetesConfigMap>;
replaceNamespacedConfigMap(
request: Readonly<{
name: string;
namespace: string;
body: ConfigMapWrite;
fieldManager: string;
fieldValidation: 'Strict';
}>,
): Promise<PluginPackageKubernetesConfigMap>;
}
interface ActivePointer {
readonly schema: typeof ACTIVE_POINTER_SCHEMA;
readonly clusterIdentityDigest: string;
readonly intent: Readonly<PluginPackageActivationIntent>;
readonly receipt: Readonly<PluginPackageActivationReceipt>;
}
interface StoredPointer extends ActivePointer {
readonly resourceVersion: string;
}
function apiStatus(error: unknown): number | null {
if (!error || typeof error !== 'object') return null;
if ('code' in error && typeof error.code === 'number') return error.code;
if (
'response' in error &&
error.response &&
typeof error.response === 'object' &&
'statusCode' in error.response &&
typeof error.response.statusCode === 'number'
) {
return error.response.statusCode;
}
return null;
}
function preserveDomainError(error: unknown): never {
if (
error instanceof PluginPackageActivationConflictError ||
error instanceof PluginPackageActivationUnavailableError
) {
throw error;
}
throw new PluginPackageActivationUnavailableError({
cause: error instanceof Error ? error : undefined,
});
}
function dataRecord(value: unknown): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
throw new PluginPackageActivationConflictError();
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
throw new PluginPackageActivationConflictError();
}
return value as Record<string, unknown>;
}
function exactKeys(value: object, expected: readonly string[]): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
throw new PluginPackageActivationConflictError();
}
}
function boundedResourceId(value: unknown): string {
if (typeof value !== 'string' || !RESOURCE_ID.test(value)) {
throw new PluginPackageActivationUnavailableError();
}
return value;
}
function normalizeIntent(
value: Readonly<PluginPackageActivationIntent>,
): Readonly<PluginPackageActivationIntent> {
try {
return normalizePluginPackageActivationIntent(value);
} catch {
throw new PluginPackageActivationConflictError();
}
}
function same(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
/**
* Short-lived Kubernetes ConfigMap publisher. It owns no timer, watcher,
* database connection or cache; every replacement is resourceVersion fenced.
*/
export class PluginPackageKubernetesActivationPublisher
implements
PluginPackageActivationPublisher,
PluginPackageResourceGenerationSource
{
readonly #clusterIdentityDigest: string;
constructor(
private readonly api: PluginPackageKubernetesConfigMapApi,
private readonly stageEvidence: ClusterPluginPackageStageEvidenceVerifier,
private readonly options: PluginPackageKubernetesActivationPublisherOptions,
) {
if (
!api ||
typeof api.readNamespacedConfigMap !== 'function' ||
typeof api.createNamespacedConfigMap !== 'function' ||
typeof api.replaceNamespacedConfigMap !== 'function'
) {
throw new TypeError('Plugin Package Kubernetes ConfigMap API is invalid');
}
if (!stageEvidence || typeof stageEvidence.verify !== 'function') {
throw new TypeError(
'Plugin Package cluster stage evidence verifier is invalid',
);
}
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).sort().join(',') !==
'clusterIdentity,namespace,now' ||
!SAFE_IDENTITY.test(options.clusterIdentity) ||
!DNS_LABEL.test(options.namespace) ||
typeof options.now !== 'function'
) {
throw new TypeError(
'Plugin Package Kubernetes activation options are invalid',
);
}
this.#clusterIdentityDigest = createHash('sha256')
.update('qinglong/plugin-package-kubernetes-cluster@v1\0', 'utf8')
.update(options.clusterIdentity, 'utf8')
.digest('hex');
}
#targetDigest(
identity: Readonly<
Pick<PluginPackageActivationIntent, 'projectId' | 'packageName'>
>,
): string {
return createHash('sha256')
.update(TARGET_DIGEST_DOMAIN)
.update(this.#clusterIdentityDigest, 'utf8')
.update('\0', 'utf8')
.update(this.options.namespace, 'utf8')
.update('\0', 'utf8')
.update(identity.projectId, 'utf8')
.update('\0', 'utf8')
.update(identity.packageName, 'utf8')
.digest('hex');
}
#name(
identity: Readonly<
Pick<PluginPackageActivationIntent, 'projectId' | 'packageName'>
>,
): string {
return `ql3p-${this.#targetDigest(identity).slice(0, 52)}`;
}
async #verifyStage(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<void> {
let value: unknown;
try {
value = await this.stageEvidence.verify(intent);
} catch (error) {
return preserveDomainError(error);
}
const evidence = dataRecord(value);
exactKeys(evidence, [
'lockDigest',
'stageRef',
'stageReceiptDigest',
'stageEvidenceDigest',
'contentDigest',
]);
if (
evidence.lockDigest !== intent.lockDigest ||
evidence.stageRef !== intent.stageRef ||
evidence.stageReceiptDigest !== intent.stageReceiptDigest ||
evidence.stageEvidenceDigest !== intent.stageEvidenceDigest ||
evidence.contentDigest !== intent.contentDigest
) {
throw new PluginPackageActivationConflictError();
}
}
#parsePointer(
configMap: PluginPackageKubernetesConfigMap,
expectedName: string,
): Readonly<StoredPointer> {
try {
const metadata = configMap?.metadata;
if (
configMap.apiVersion !== 'v1' ||
configMap.kind !== 'ConfigMap' ||
configMap.immutable === true ||
configMap.binaryData !== undefined ||
!metadata ||
metadata.name !== expectedName ||
metadata.namespace !== this.options.namespace ||
metadata.deletionTimestamp !== undefined ||
(metadata.finalizers?.length ?? 0) !== 0 ||
(metadata.ownerReferences?.length ?? 0) !== 0 ||
!configMap.data
) {
throw new PluginPackageActivationConflictError();
}
const labels = dataRecord(metadata.labels);
exactKeys(labels, [MANAGED_BY_LABEL, ACTIVE_LABEL, TARGET_LABEL]);
const annotations = dataRecord(metadata.annotations);
exactKeys(annotations, [INTENT_ANNOTATION]);
const data = dataRecord(configMap.data);
exactKeys(data, [ACTIVE_POINTER_KEY]);
const serialized = data[ACTIVE_POINTER_KEY];
if (
labels[MANAGED_BY_LABEL] !== MANAGED_BY_VALUE ||
labels[ACTIVE_LABEL] !== 'v2' ||
typeof serialized !== 'string' ||
Buffer.byteLength(serialized, 'utf8') > MAX_ACTIVE_POINTER_BYTES
) {
throw new PluginPackageActivationConflictError();
}
const pointer = dataRecord(JSON.parse(serialized));
exactKeys(pointer, [
'schema',
'clusterIdentityDigest',
'intent',
'receipt',
]);
const intent = normalizeIntent(
pointer.intent as PluginPackageActivationIntent,
);
const receipt = normalizePluginPackageActivationReceipt(pointer.receipt);
const normalized: ActivePointer = Object.freeze({
schema: ACTIVE_POINTER_SCHEMA,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
if (
pointer.schema !== ACTIVE_POINTER_SCHEMA ||
pointer.clusterIdentityDigest !== this.#clusterIdentityDigest ||
this.#name(intent) !== expectedName ||
labels[TARGET_LABEL] !==
Buffer.from(this.#targetDigest(intent), 'hex').toString(
'base64url',
) ||
annotations[INTENT_ANNOTATION] !== intent.intentDigest ||
receipt.intentDigest !== intent.intentDigest ||
receipt.generation !== intent.targetGeneration ||
receipt.contentDigest !== intent.contentDigest ||
`${JSON.stringify(normalized)}\n` !== serialized
) {
throw new PluginPackageActivationConflictError();
}
boundedResourceId(metadata.uid);
return Object.freeze({
...normalized,
resourceVersion: boundedResourceId(metadata.resourceVersion),
});
} catch (error) {
return preserveDomainError(error);
}
}
async #optionalPointer(
identity: Readonly<
Pick<PluginPackageActivationIntent, 'projectId' | 'packageName'>
>,
): Promise<Readonly<StoredPointer> | null> {
const name = this.#name(identity);
try {
return this.#parsePointer(
await this.api.readNamespacedConfigMap({
name,
namespace: this.options.namespace,
}),
name,
);
} catch (error) {
if (apiStatus(error) === 404) return null;
return preserveDomainError(error);
}
}
async #observe(
intent: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationObservation>> {
await this.#verifyStage(intent);
const pointer = await this.#optionalPointer(intent);
if (!pointer) {
if (intent.previousActiveLockDigest !== null) {
throw new PluginPackageActivationConflictError();
}
return Object.freeze({ status: 'not_published' });
}
if (same(pointer.intent, intent)) {
return Object.freeze({ status: 'published', receipt: pointer.receipt });
}
if (
pointer.intent.projectId === intent.projectId &&
pointer.intent.packageName === intent.packageName &&
pointer.intent.lockDigest === intent.previousActiveLockDigest
) {
return Object.freeze({ status: 'not_published' });
}
throw new PluginPackageActivationConflictError();
}
#body(
intent: Readonly<PluginPackageActivationIntent>,
receipt: Readonly<PluginPackageActivationReceipt>,
current: Readonly<StoredPointer> | null,
): ConfigMapWrite {
const targetDigest = this.#targetDigest(intent);
const pointer: Readonly<ActivePointer> = Object.freeze({
schema: ACTIVE_POINTER_SCHEMA,
clusterIdentityDigest: this.#clusterIdentityDigest,
intent,
receipt,
});
const serialized = `${JSON.stringify(pointer)}\n`;
if (Buffer.byteLength(serialized, 'utf8') > MAX_ACTIVE_POINTER_BYTES) {
throw new PluginPackageActivationUnavailableError();
}
return Object.freeze({
apiVersion: 'v1',
kind: 'ConfigMap',
immutable: false,
metadata: Object.freeze({
name: this.#name(intent),
namespace: this.options.namespace,
...(current ? { resourceVersion: current.resourceVersion } : {}),
labels: Object.freeze({
[MANAGED_BY_LABEL]: MANAGED_BY_VALUE,
[ACTIVE_LABEL]: 'v2',
[TARGET_LABEL]: Buffer.from(targetDigest, 'hex').toString(
'base64url',
),
}),
annotations: Object.freeze({
[INTENT_ANNOTATION]: intent.intentDigest,
}),
}),
data: Object.freeze({ [ACTIVE_POINTER_KEY]: serialized }),
});
}
async inspect(
value: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationObservation>> {
try {
return await this.#observe(normalizeIntent(value));
} catch (error) {
return preserveDomainError(error);
}
}
async findActiveResourceGeneration(
projectId: string,
packageName: string,
): Promise<Readonly<PluginPackageResourceGeneration> | null> {
if (
typeof projectId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(projectId) ||
typeof packageName !== 'string' ||
!DNS_LABEL.test(packageName)
) {
throw new TypeError('Plugin Package active resource identity is invalid');
}
try {
return (
(await this.#optionalPointer(Object.freeze({ projectId, packageName })))
?.intent.resourceGeneration ?? null
);
} catch (error) {
return preserveDomainError(error);
}
}
async publish(
value: Readonly<PluginPackageActivationIntent>,
): Promise<Readonly<PluginPackageActivationReceipt>> {
const intent = normalizeIntent(value);
try {
const first = await this.#observe(intent);
if (first.status === 'published') return first.receipt;
const current = await this.#optionalPointer(intent);
if (current && same(current.intent, intent)) return current.receipt;
if (
(!current && intent.previousActiveLockDigest !== null) ||
(current &&
(current.intent.projectId !== intent.projectId ||
current.intent.packageName !== intent.packageName ||
current.intent.lockDigest !== intent.previousActiveLockDigest))
) {
throw new PluginPackageActivationConflictError();
}
const activatedAtMs = await this.options.now();
if (!Number.isSafeInteger(activatedAtMs) || activatedAtMs < 0) {
throw new PluginPackageActivationUnavailableError();
}
const receipt = createPluginPackageActivationReceipt({
activationRef: `k8s-configmap:${this.#targetDigest(intent)}`,
intentDigest: intent.intentDigest,
generation: intent.targetGeneration,
contentDigest: intent.contentDigest,
activatedAtMs,
});
const body = this.#body(intent, receipt, current);
try {
if (current) {
await this.api.replaceNamespacedConfigMap({
name: this.#name(intent),
namespace: this.options.namespace,
body,
fieldManager: FIELD_MANAGER,
fieldValidation: 'Strict',
});
} else {
await this.api.createNamespacedConfigMap({
namespace: this.options.namespace,
body,
fieldManager: FIELD_MANAGER,
fieldValidation: 'Strict',
});
}
} catch (error) {
if (apiStatus(error) !== 409) return preserveDomainError(error);
const winner = await this.#observe(intent);
if (winner.status === 'published') return winner.receipt;
throw new PluginPackageActivationConflictError();
}
const final = await this.#observe(intent);
if (final.status !== 'published') {
throw new PluginPackageActivationUnavailableError();
}
return final.receipt;
} catch (error) {
return preserveDomainError(error);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,450 @@
// Cluster Plugin Package recovery boundary; keep recovery coordination authority explicit.
import type {
OpenPostgresDatabase,
PostgresDatabaseResource,
PostgresPool,
} from '@qinglong/runtime-core';
import {
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
assertPluginPackageInstallMatchesLock,
} from '@qinglong/runtime-core/plugin-package-install';
import {
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
PluginPackageRecoveryCoordinator,
type PluginPackageRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-recovery';
import {
PluginPackageAutomationPublicationCoordinator,
PluginPackageAutomationPublicationRecoveryCoordinator,
type PluginPackageAutomationPublicationRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-automation-publication';
import type { PluginPackageResourceByteSource } from '@qinglong/runtime-core/plugin-package-resource-materialization';
import {
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGES,
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGE_SIZE,
PluginPackageTaskPublicationCoordinator,
PluginPackageTaskPublicationRecoveryCoordinator,
type PluginPackageTaskPublicationRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-task-publication';
import {
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGES,
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGE_SIZE,
MAX_PROJECT_TOOL_SNAPSHOT_SOURCE_PAGE_SIZE,
ProjectToolDefinitionSnapshotPublicationCoordinator,
ProjectToolDefinitionSnapshotRecoveryCoordinator,
type ProjectToolDefinitionSnapshotRecoveryCycleResult,
} from '@qinglong/runtime-core/project-tool-definition-snapshot';
import { createBuiltInTaskSpecSemanticRegistry } from '@qinglong/runtime-core/task-spec-semantic';
import {
assertPostgresPackageExecutorSchemaReady,
PostgresPluginPackageAutomationPublicationRepository,
PostgresPluginPackageMaterializedRevisionRepository,
PostgresPluginPackagePublisherProvenanceRepository,
PostgresPluginPackageTaskReconciliationRepository,
PostgresProjectToolDefinitionSnapshotRepository,
type PostgresSchemaReadinessReport,
} from '@qinglong/cluster-postgres/package-executor';
import { PostgresPluginPackageInstallRepository } from '@qinglong/cluster-postgres/plugin-package-install';
import {
PluginPackageKubernetesActivationPublisher,
type PluginPackageKubernetesConfigMapApi,
} from './pluginPackageKubernetesActivation';
import {
ClusterPluginPackageOciResourceByteSource,
ClusterPluginPackageOciStageAuthority,
clusterPluginPackageActivationEvidence,
pluginPackageStageVerificationFailure,
type ClusterPluginPackageStageAuthority,
} from './pluginPackageOciStage';
import {
ClusterPluginPackageProvenanceInstallRepository,
recoverClusterPluginPackagePublisherProvenance,
type ClusterPluginPackagePublisherProvenanceRecoveryResult,
} from '../publisher/pluginPackagePublisherProvenanceRecovery';
export interface ClusterPluginPackageRecoveryOptions {
readonly openDatabase: OpenPostgresDatabase;
readonly api: PluginPackageKubernetesConfigMapApi;
readonly stageAuthority?: ClusterPluginPackageStageAuthority;
readonly stageAuthorityFactory?: (
pool: PostgresPool,
) =>
| ClusterPluginPackageStageAuthority
| Promise<ClusterPluginPackageStageAuthority>;
readonly resourceByteSource?: PluginPackageResourceByteSource;
readonly trustAuthorityId: string;
readonly clusterIdentity: string;
readonly namespace: string;
readonly now: () => number | Promise<number>;
readonly pageSize?: number;
readonly maxPages?: number;
}
export interface ClusterPluginPackageRecoveryResult {
readonly evidence: PostgresSchemaReadinessReport;
readonly provenanceRecovery: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>;
readonly recovery: Readonly<PluginPackageRecoveryCycleResult>;
readonly taskPublicationRecovery: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>;
readonly automationPublicationRecovery: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>;
readonly toolSnapshotRecovery: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>;
}
export class ClusterPluginPackageRecoveryRequiredError extends Error {
constructor(readonly recovery: Readonly<PluginPackageRecoveryCycleResult>) {
super('Cluster has unresolved Plugin Package recovery work');
this.name = 'ClusterPluginPackageRecoveryRequiredError';
}
}
export class ClusterPluginPackagePublisherProvenanceRecoveryRequiredError extends Error {
constructor(
readonly recovery: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>,
) {
super('Cluster has unresolved Plugin Package publisher provenance work');
this.name =
'ClusterPluginPackagePublisherProvenanceRecoveryRequiredError';
}
}
export class ClusterPluginPackageTaskPublicationRequiredError extends Error {
constructor(
readonly recovery: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>,
) {
super('Cluster has unresolved Plugin Package Task publication work');
this.name = 'ClusterPluginPackageTaskPublicationRequiredError';
}
}
export class ClusterPluginPackageAutomationPublicationRequiredError extends Error {
constructor(
readonly recovery: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>,
) {
super(
'Cluster has unresolved Plugin Package Workflow/Prompt publication work',
);
this.name = 'ClusterPluginPackageAutomationPublicationRequiredError';
}
}
export class ClusterPluginPackageToolSnapshotRequiredError extends Error {
constructor(
readonly recovery: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>,
) {
super('Cluster has unresolved Plugin Package Tool snapshot work');
this.name = 'ClusterPluginPackageToolSnapshotRequiredError';
}
}
function assertOptions(options: ClusterPluginPackageRecoveryOptions): void {
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) =>
![
'openDatabase',
'api',
'stageAuthority',
'stageAuthorityFactory',
'resourceByteSource',
'trustAuthorityId',
'clusterIdentity',
'namespace',
'now',
'pageSize',
'maxPages',
].includes(key),
) ||
typeof options.openDatabase !== 'function' ||
(options.stageAuthority === undefined) ===
(options.stageAuthorityFactory === undefined) ||
(options.stageAuthorityFactory !== undefined &&
typeof options.stageAuthorityFactory !== 'function') ||
(options.resourceByteSource !== undefined &&
(!options.resourceByteSource ||
typeof options.resourceByteSource.open !== 'function')) ||
typeof options.trustAuthorityId !== 'string' ||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(
options.trustAuthorityId,
) ||
typeof options.now !== 'function' ||
(options.pageSize !== undefined &&
(!Number.isSafeInteger(options.pageSize) ||
options.pageSize < 1 ||
options.pageSize >
Math.min(
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGE_SIZE,
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGE_SIZE,
))) ||
(options.maxPages !== undefined &&
(!Number.isSafeInteger(options.maxPages) ||
options.maxPages < 1 ||
options.maxPages >
Math.min(
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
MAX_PLUGIN_PACKAGE_TASK_PUBLICATION_RECOVERY_PAGES,
MAX_PROJECT_TOOL_SNAPSHOT_RECOVERY_PAGES,
)))
) {
throw new TypeError(
'Cluster Plugin Package recovery configuration is invalid',
);
}
}
function assertStageAuthority(
stageAuthority: ClusterPluginPackageStageAuthority,
hasResourceByteSource: boolean,
): void {
if (
!stageAuthority ||
typeof stageAuthority.stage !== 'function' ||
typeof stageAuthority.publisherEvidence !== 'function' ||
typeof stageAuthority.verify !== 'function' ||
(!hasResourceByteSource &&
!(stageAuthority instanceof ClusterPluginPackageOciStageAuthority))
) {
throw new TypeError(
'Cluster Plugin Package recovery stage authority is invalid',
);
}
}
/**
* One-shot admin Job composition. The database is always closed before this
* function settles, and no repository or Kubernetes authority escapes.
*/
export async function recoverClusterPluginPackages(
options: ClusterPluginPackageRecoveryOptions,
): Promise<Readonly<ClusterPluginPackageRecoveryResult>> {
assertOptions(options);
let database: PostgresDatabaseResource | undefined;
let result: Readonly<ClusterPluginPackageRecoveryResult> | undefined;
let failure: unknown;
try {
database = await options.openDatabase();
const evidence = await assertPostgresPackageExecutorSchemaReady(
database.pool,
);
const stageAuthority =
options.stageAuthority ??
(await options.stageAuthorityFactory!(database.pool));
assertStageAuthority(
stageAuthority,
options.resourceByteSource !== undefined,
);
const installRepository = new PostgresPluginPackageInstallRepository(
database.pool,
);
const provenanceRepository =
new PostgresPluginPackagePublisherProvenanceRepository(database.pool);
const provenanceRecovery =
await recoverClusterPluginPackagePublisherProvenance(
installRepository,
provenanceRepository,
stageAuthority,
{
trustAuthorityId: options.trustAuthorityId,
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
},
);
if (!provenanceRecovery.safeToAdmit) {
throw new ClusterPluginPackagePublisherProvenanceRecoveryRequiredError(
provenanceRecovery,
);
}
const repository = new ClusterPluginPackageProvenanceInstallRepository(
installRepository,
provenanceRepository,
stageAuthority,
options.trustAuthorityId,
);
const publisher = new PluginPackageKubernetesActivationPublisher(
options.api,
{
async verify(intent) {
try {
const [record, lock] = await Promise.all([
repository.find(intent.projectId, intent.packageName),
repository.findLock(intent.lockDigest),
]);
if (
!record ||
!lock ||
record.installationId !== intent.installationId ||
record.lockDigest !== intent.lockDigest ||
record.stageReceipt === null ||
record.stageReceipt.stageRef !== intent.stageRef ||
record.stageReceipt.receiptDigest !== intent.stageReceiptDigest ||
record.stageReceipt.evidenceDigest !==
intent.stageEvidenceDigest ||
record.stageReceipt.contentDigest !== intent.contentDigest
) {
return pluginPackageStageVerificationFailure(
new Error('durable stage identity conflict'),
);
}
assertPluginPackageInstallMatchesLock(lock, record);
await stageAuthority.verify(lock, record.stageReceipt);
await provenanceRepository.assertInstallationNotRevoked(
record.installationId,
);
return clusterPluginPackageActivationEvidence(intent);
} catch (error) {
return pluginPackageStageVerificationFailure(error);
}
},
},
{
clusterIdentity: options.clusterIdentity,
namespace: options.namespace,
now: options.now,
},
);
const resourceByteSource =
options.resourceByteSource ??
new ClusterPluginPackageOciResourceByteSource({
authority:
stageAuthority as ClusterPluginPackageOciStageAuthority,
lockSource: repository,
});
const recovery = await new PluginPackageRecoveryCoordinator({
repository,
stageProvider: stageAuthority,
publisher,
now: options.now,
}).recover({
...(options.pageSize === undefined ? {} : { pageSize: options.pageSize }),
...(options.maxPages === undefined ? {} : { maxPages: options.maxPages }),
});
if (!recovery.safeToAdmit) {
throw new ClusterPluginPackageRecoveryRequiredError(recovery);
}
const taskSpecSemanticRegistry = createBuiltInTaskSpecSemanticRegistry();
const taskReconciliationRepository =
new PostgresPluginPackageTaskReconciliationRepository(
database.pool,
taskSpecSemanticRegistry,
);
const materializedRepository =
new PostgresPluginPackageMaterializedRevisionRepository(
database.pool,
taskSpecSemanticRegistry,
);
const taskPublicationRecovery =
await new PluginPackageTaskPublicationRecoveryCoordinator({
source: taskReconciliationRepository,
publisher: new PluginPackageTaskPublicationCoordinator({
generationSource: publisher,
lockSource: repository,
byteSource: resourceByteSource,
materializedRepository,
reconciliationRepository: taskReconciliationRepository,
taskSpecSemanticRegistry,
}),
}).recover({
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
});
if (!taskPublicationRecovery.safeToAdmit) {
throw new ClusterPluginPackageTaskPublicationRequiredError(
taskPublicationRecovery,
);
}
const automationPublicationRepository =
new PostgresPluginPackageAutomationPublicationRepository(database.pool);
const automationPublicationRecovery =
await new PluginPackageAutomationPublicationRecoveryCoordinator({
source: automationPublicationRepository,
publisher: new PluginPackageAutomationPublicationCoordinator({
generationSource: publisher,
materializedRepository,
repository: automationPublicationRepository,
taskSpecSemanticRegistry,
now: options.now,
}),
}).recover({
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
});
if (!automationPublicationRecovery.safeToAdmit) {
throw new ClusterPluginPackageAutomationPublicationRequiredError(
automationPublicationRecovery,
);
}
const toolSnapshotRepository =
new PostgresProjectToolDefinitionSnapshotRepository(database.pool);
const toolSnapshotRecovery =
await new ProjectToolDefinitionSnapshotRecoveryCoordinator({
source: toolSnapshotRepository,
publisher: new ProjectToolDefinitionSnapshotPublicationCoordinator({
source: toolSnapshotRepository,
materializedRepository,
repository: toolSnapshotRepository,
taskSpecSemanticRegistry,
pageSize: Math.min(
options.pageSize ?? 16,
MAX_PROJECT_TOOL_SNAPSHOT_SOURCE_PAGE_SIZE,
),
}),
}).recover({
...(options.pageSize === undefined
? {}
: { pageSize: options.pageSize }),
...(options.maxPages === undefined
? {}
: { maxPages: options.maxPages }),
});
if (!toolSnapshotRecovery.safeToAdmit) {
throw new ClusterPluginPackageToolSnapshotRequiredError(
toolSnapshotRecovery,
);
}
result = Object.freeze({
evidence,
provenanceRecovery,
recovery,
taskPublicationRecovery,
automationPublicationRecovery,
toolSnapshotRecovery,
});
} catch (error) {
failure = error;
}
if (database) {
try {
await database.close();
} catch (closeError) {
if (failure !== undefined) {
throw new AggregateError(
[failure, closeError],
'Cluster Plugin Package recovery failed and PostgreSQL did not close',
);
}
throw closeError;
}
}
if (failure !== undefined) throw failure;
if (!result) {
throw new Error('Cluster Plugin Package recovery produced no result');
}
return result;
}
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// Cluster Plugin Package recovery boundary; keep the operational CLI explicit.
import { runClusterPluginPackageRecoveryProcess } from './pluginPackageRecoveryProcess';
const USAGE = 'Usage: ql3-plugin-package-recover';
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
const candidate = error as {
readonly name?: unknown;
readonly code?: unknown;
};
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-plugin-package-recovery',
event: 'recovery_failed',
name:
typeof candidate?.name === 'string' && candidate.name.length <= 128
? candidate.name
: 'Error',
...(typeof candidate?.code === 'string' && candidate.code.length <= 128
? { code: candidate.code }
: {}),
});
}
async function main(argv: readonly string[]): Promise<void> {
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
process.stdout.write(`${USAGE}\n`);
return;
}
if (argv.length !== 0) {
process.stderr.write(
`${JSON.stringify({
code: 'QL3_PLUGIN_PACKAGE_RECOVERY_CLI_USAGE_INVALID',
message: USAGE,
})}\n`,
);
process.exitCode = 64;
return;
}
try {
await runClusterPluginPackageRecoveryProcess({
environment: process.env,
emit(record) {
process.stdout.write(`${JSON.stringify(record)}\n`);
},
});
} catch (error) {
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
process.exitCode = 1;
}
}
void main(process.argv.slice(2));
@@ -0,0 +1,885 @@
// Cluster Plugin Package recovery boundary; keep process composition explicit.
import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs';
import { isAbsolute } from 'node:path';
import type {
OpenPostgresDatabase,
PostgresPool,
} from '@qinglong/runtime-core';
import { MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE } from '@qinglong/runtime-core/plugin-package-install';
import {
PluginPackagePublisherTrustRegistry,
type PluginPackagePublisherKeyDefinition,
} from '@qinglong/runtime-core/plugin-package-bundle';
import {
createPluginPackagePublisherTrustSnapshot,
createPluginPackagePublisherEffectiveTrustRegistry,
type PluginPackagePublisherTrustSnapshot,
} from '@qinglong/runtime-core/plugin-package-publisher-trust';
import {
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
type PluginPackageRecoveryCycleResult,
} from '@qinglong/runtime-core/plugin-package-recovery';
import type { PluginPackageAutomationPublicationRecoveryCycleResult } from '@qinglong/runtime-core/plugin-package-automation-publication';
import type { PluginPackageResourceByteSource } from '@qinglong/runtime-core/plugin-package-resource-materialization';
import type { PluginPackageTaskPublicationRecoveryCycleResult } from '@qinglong/runtime-core/plugin-package-task-publication';
import type { ProjectToolDefinitionSnapshotRecoveryCycleResult } from '@qinglong/runtime-core/project-tool-definition-snapshot';
import {
createPostgresDatabaseOpener,
isPostgresTlsDnsServername,
loadPostgresCertificateAuthorityFile,
loadPostgresConnectionEnvironment,
PostgresPluginPackagePublisherTrustAuthorityRepository,
type PostgresConnectionOptions,
type PostgresPoolOptions,
} from '@qinglong/cluster-postgres/package-executor';
import {
recoverClusterPluginPackages,
type ClusterPluginPackageRecoveryResult,
} from './pluginPackageRecovery';
import type { ClusterPluginPackagePublisherProvenanceRecoveryResult } from '../publisher/pluginPackagePublisherProvenanceRecovery';
import {
ClusterPluginPackageOciStageAuthority,
type ClusterPluginPackageOciFetch,
type ClusterPluginPackageRegistryCredentialProvider,
type ClusterPluginPackageStageAuthority,
} from './pluginPackageOciStage';
import type { PluginPackageKubernetesConfigMapApi } from './pluginPackageKubernetesActivation';
export type ClusterPluginPackageRecoveryProcessEnvironment = Readonly<
Record<string, string | undefined>
>;
export interface ClusterPluginPackageRecoveryProcessConfig {
readonly clusterIdentity: string;
readonly namespace: string;
readonly allowedRegistries: readonly string[];
readonly publisherTrustFile: string;
readonly publisherTrustAuthorityId: string;
readonly registryCredentialFile?: string;
readonly requestTimeoutMs: number;
readonly pageSize: number;
readonly maxPages: number;
readonly database: Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}>;
}
export interface ClusterPluginPackageRecoveryProcessEvent {
readonly schemaVersion: 1;
readonly component: 'qinglong3-plugin-package-recovery';
readonly event: 'recovery_started' | 'recovery_completed';
readonly clusterIdentity: string;
readonly provenanceRecovery?: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>;
readonly recovery?: Readonly<PluginPackageRecoveryCycleResult>;
readonly taskPublicationRecovery?: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>;
readonly automationPublicationRecovery?: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>;
readonly toolSnapshotRecovery?: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>;
}
export interface RunClusterPluginPackageRecoveryProcessOptions {
readonly environment: ClusterPluginPackageRecoveryProcessEnvironment;
readonly emit?: (
event: ClusterPluginPackageRecoveryProcessEvent,
) => void | Promise<void>;
readonly openDatabase?: OpenPostgresDatabase;
readonly api?: PluginPackageKubernetesConfigMapApi;
readonly stageAuthority?: ClusterPluginPackageStageAuthority;
readonly resourceByteSource?: PluginPackageResourceByteSource;
readonly trust?: PluginPackagePublisherTrustRegistry;
readonly fetch?: ClusterPluginPackageOciFetch;
}
export interface ClusterPluginPackageRegistryCredentialFile
extends ClusterPluginPackageRegistryCredentialProvider {
dispose(): void;
}
export interface ClusterPluginPackagePublisherTrustFileEvidence {
readonly registry: PluginPackagePublisherTrustRegistry;
readonly snapshot: Readonly<PluginPackagePublisherTrustSnapshot>;
readonly definitions: readonly Readonly<PluginPackagePublisherKeyDefinition>[];
}
export class ClusterPluginPackageRecoveryProcessConfigError extends TypeError {
readonly code = 'QL3_PLUGIN_PACKAGE_RECOVERY_PROCESS_CONFIG_INVALID';
constructor(message: string) {
super(
`Plugin Package recovery process configuration is invalid: ${message}`,
);
this.name = 'ClusterPluginPackageRecoveryProcessConfigError';
}
}
const TRUST_SCHEMA = 'qinglong/plugin-package-publisher-trust@v1';
const REGISTRY_CREDENTIAL_SCHEMA =
'qinglong/plugin-package-registry-credentials@v1';
const MAX_TRUST_FILE_BYTES = 256 * 1024;
const MAX_REGISTRY_CREDENTIAL_FILE_BYTES = 256 * 1024;
const MAX_REGISTRY_CREDENTIALS = 32;
const SAFE_IDENTITY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
const DNS_LABEL = /^[a-z0-9](?:[-a-z0-9]{0,61}[a-z0-9])?$/;
const REGISTRY =
/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*)(?::([1-9][0-9]{0,4}))?$/;
const BEARER_TOKEN = /^[A-Za-z0-9._~+/-]+={0,2}$/;
class LoadedClusterPluginPackageRegistryCredentialFile
implements ClusterPluginPackageRegistryCredentialFile
{
readonly #authorizations: Map<string, Buffer>;
constructor(authorizations: Map<string, Buffer>) {
this.#authorizations = authorizations;
}
authorizationFor(registry: string): string | undefined {
if (typeof registry !== 'string' || !REGISTRY.test(registry)) {
return undefined;
}
return this.#authorizations.get(registry)?.toString('ascii');
}
dispose(): void {
for (const authorization of this.#authorizations.values()) {
authorization.fill(0);
}
this.#authorizations.clear();
}
}
function boundedValue(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
name: string,
maximumLength: number,
required = false,
): string | undefined {
const value = environment[name];
if (value === undefined || value === '') {
if (required) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} is required`,
);
}
return undefined;
}
if (value.length > maximumLength || /[\0\r\n]/.test(value)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} is invalid`,
);
}
return value;
}
function booleanValue(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
name: string,
): boolean {
const value = environment[name];
if (value === undefined || value === '') return false;
if (value === 'true') return true;
if (value === 'false') return false;
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} must be true or false`,
);
}
function integerValue(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
name: string,
defaultValue: number,
minimum: number,
maximum: number,
): number {
const value = environment[name];
if (value === undefined || value === '') return defaultValue;
if (!/^\d+$/.test(value)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} must be an integer`,
);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${name} must be between ${minimum} and ${maximum}`,
);
}
return parsed;
}
function loadConnection(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
): Readonly<{
connection: PostgresConnectionOptions;
pool: PostgresPoolOptions;
}> {
let connection: PostgresConnectionOptions;
try {
connection = loadPostgresConnectionEnvironment(environment, {
connectionString: 'QL3_POSTGRES_PACKAGE_EXECUTOR_URL',
host: 'QL3_POSTGRES_PACKAGE_EXECUTOR_HOST',
port: 'QL3_POSTGRES_PACKAGE_EXECUTOR_PORT',
database: 'QL3_POSTGRES_PACKAGE_EXECUTOR_DATABASE',
user: 'QL3_POSTGRES_PACKAGE_EXECUTOR_USER',
password: 'QL3_POSTGRES_PACKAGE_EXECUTOR_PASSWORD',
});
} catch (error) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
error instanceof Error
? error.message
: 'PostgreSQL Package executor connection is invalid',
);
}
const mode = environment.QL3_POSTGRES_TLS_MODE ?? 'verify-full';
if (mode !== 'verify-full' && mode !== 'disable') {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_TLS_MODE must be verify-full or disable',
);
}
if (
mode === 'disable' &&
!booleanValue(environment, 'QL3_POSTGRES_ALLOW_INSECURE')
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'disabling PostgreSQL TLS requires QL3_POSTGRES_ALLOW_INSECURE=true',
);
}
const servername = boundedValue(
environment,
'QL3_POSTGRES_TLS_SERVERNAME',
253,
);
if (mode === 'verify-full' && !isPostgresTlsDnsServername(servername)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_TLS_SERVERNAME must be an explicit DNS name for verify-full',
);
}
const certificateAuthorityFile = boundedValue(
environment,
'QL3_POSTGRES_TLS_CA_FILE',
4096,
);
if (mode === 'disable' && certificateAuthorityFile !== undefined) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_TLS_CA_FILE cannot be used when TLS is disabled',
);
}
let certificateAuthority: string | undefined;
if (certificateAuthorityFile !== undefined) {
try {
certificateAuthority = loadPostgresCertificateAuthorityFile(
certificateAuthorityFile,
);
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_TLS_CA_FILE must contain a bounded trusted CA bundle',
);
}
}
const applicationName =
boundedValue(environment, 'QL3_POSTGRES_APPLICATION_NAME', 63) ??
'qinglong3-plugin-package-recovery';
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,62}$/.test(applicationName)) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'QL3_POSTGRES_APPLICATION_NAME is invalid',
);
}
return Object.freeze({
connection: Object.freeze({
...connection,
tls:
mode === 'disable'
? Object.freeze({ mode: 'disable' as const })
: Object.freeze({
mode: 'verify-full' as const,
...(certificateAuthority === undefined
? {}
: { ca: certificateAuthority }),
servername: servername!,
}),
}),
pool: Object.freeze({
applicationName,
maxConnections: 1,
connectionTimeoutMs: 15_000,
}),
});
}
export function loadClusterPluginPackageRecoveryProcessConfig(
environment: ClusterPluginPackageRecoveryProcessEnvironment,
): Readonly<ClusterPluginPackageRecoveryProcessConfig> {
if (
!environment ||
typeof environment !== 'object' ||
Array.isArray(environment)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'environment must be an object',
);
}
const clusterIdentity = boundedValue(
environment,
'QL3_CLUSTER_IDENTITY',
256,
true,
)!;
const namespace = boundedValue(
environment,
'QL3_KUBERNETES_NAMESPACE',
63,
true,
)!;
const registryValue = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_OCI_REGISTRIES',
4096,
true,
)!;
const allowedRegistries = registryValue.split(',');
const publisherTrustFile = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_PUBLISHER_TRUST_FILE',
4096,
true,
)!;
const registryCredentialFile = boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_REGISTRY_CREDENTIAL_FILE',
4096,
);
if (
!SAFE_IDENTITY.test(clusterIdentity) ||
!DNS_LABEL.test(namespace) ||
allowedRegistries.length < 1 ||
allowedRegistries.length > 32 ||
allowedRegistries.some((registry) => !REGISTRY.test(registry)) ||
new Set(allowedRegistries).size !== allowedRegistries.length ||
!isAbsolute(publisherTrustFile) ||
(registryCredentialFile !== undefined &&
!isAbsolute(registryCredentialFile))
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'cluster, namespace, registry or publisher trust binding is invalid',
);
}
return Object.freeze({
clusterIdentity,
namespace,
allowedRegistries: Object.freeze(allowedRegistries),
publisherTrustFile,
publisherTrustAuthorityId:
boundedValue(
environment,
'QL3_PLUGIN_PACKAGE_TRUST_AUTHORITY_ID',
128,
) ?? 'cluster',
...(registryCredentialFile === undefined ? {} : { registryCredentialFile }),
requestTimeoutMs: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_OCI_TIMEOUT_MS',
15_000,
1_000,
60_000,
),
pageSize: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_RECOVERY_PAGE_SIZE',
16,
1,
MAX_PLUGIN_PACKAGE_INSTALL_RECOVERY_PAGE_SIZE,
),
maxPages: integerValue(
environment,
'QL3_PLUGIN_PACKAGE_RECOVERY_MAX_PAGES',
16,
1,
MAX_PLUGIN_PACKAGE_RECOVERY_PAGES,
),
database: loadConnection(environment),
});
}
function readClusterPluginPackageRegistryCredentialFile(
filePath: string,
): Buffer {
if (
typeof filePath !== 'string' ||
filePath.length < 1 ||
filePath.length > 4096 ||
/[\0\r\n]/.test(filePath) ||
!isAbsolute(filePath)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file path is invalid',
);
}
let descriptor: number;
try {
descriptor = openSync(filePath, constants.O_RDONLY);
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file is unavailable',
);
}
try {
const stat = fstatSync(descriptor);
if (
!stat.isFile() ||
(stat.mode & 0o027) !== 0 ||
!Number.isSafeInteger(stat.size) ||
stat.size < 1 ||
stat.size > MAX_REGISTRY_CREDENTIAL_FILE_BYTES
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file is not a bounded private regular file',
);
}
const bytes = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < bytes.byteLength) {
const count = readSync(
descriptor,
bytes,
offset,
bytes.byteLength - offset,
null,
);
if (count === 0) break;
offset += count;
}
if (offset !== stat.size) {
bytes.fill(0);
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file changed while reading',
);
}
return bytes.subarray(0, offset);
} finally {
closeSync(descriptor);
}
}
function credentialRecord(
value: unknown,
label: string,
): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.getPrototypeOf(value) !== Object.prototype
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`${label} must be an object`,
);
}
return value as Record<string, unknown>;
}
function scrubCredentialSource(value: unknown): void {
if (!value || typeof value !== 'object') return;
const credentials = (value as { credentials?: unknown }).credentials;
if (!Array.isArray(credentials)) return;
for (const candidate of credentials) {
if (!candidate || typeof candidate !== 'object') continue;
const record = candidate as Record<string, unknown>;
if (typeof record.password === 'string') record.password = '';
if (typeof record.token === 'string') record.token = '';
}
}
function zeroAuthorizations(authorizations: Map<string, Buffer>): void {
for (const authorization of authorizations.values()) {
authorization.fill(0);
}
authorizations.clear();
}
export function loadClusterPluginPackageRegistryCredentialFile(
filePath: string,
allowedRegistries: readonly string[],
): ClusterPluginPackageRegistryCredentialFile {
if (
!Array.isArray(allowedRegistries) ||
allowedRegistries.length < 1 ||
allowedRegistries.length > MAX_REGISTRY_CREDENTIALS ||
allowedRegistries.some(
(registry) => typeof registry !== 'string' || !REGISTRY.test(registry),
) ||
new Set(allowedRegistries).size !== allowedRegistries.length
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential allowlist is invalid',
);
}
const bytes = readClusterPluginPackageRegistryCredentialFile(filePath);
let value: unknown;
try {
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file is not valid JSON',
);
} finally {
bytes.fill(0);
}
const authorizations = new Map<string, Buffer>();
try {
const root = credentialRecord(value, 'registry credential file');
if (
Object.keys(root).sort().join(',') !== 'credentials,schema' ||
root.schema !== REGISTRY_CREDENTIAL_SCHEMA ||
!Array.isArray(root.credentials) ||
root.credentials.length < 1 ||
root.credentials.length > MAX_REGISTRY_CREDENTIALS
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'registry credential file shape is invalid',
);
}
const allowed = new Set(allowedRegistries);
for (const [index, candidate] of root.credentials.entries()) {
const entry = credentialRecord(candidate, `registry credential ${index}`);
const registry = entry.registry;
const scheme = entry.scheme;
if (
typeof registry !== 'string' ||
!REGISTRY.test(registry) ||
!allowed.has(registry) ||
authorizations.has(registry)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} binding is invalid`,
);
}
let authorization: Buffer;
if (scheme === 'basic') {
if (
Object.keys(entry).sort().join(',') !==
'password,registry,scheme,username' ||
typeof entry.username !== 'string' ||
Buffer.byteLength(entry.username, 'utf8') < 1 ||
Buffer.byteLength(entry.username, 'utf8') > 256 ||
/[\0-\x1f\x7f:]/.test(entry.username) ||
typeof entry.password !== 'string' ||
Buffer.byteLength(entry.password, 'utf8') < 1 ||
Buffer.byteLength(entry.password, 'utf8') > 4096 ||
/[\0-\x1f\x7f]/.test(entry.password)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} basic value is invalid`,
);
}
const userPassword = Buffer.from(
`${entry.username}:${entry.password}`,
'utf8',
);
try {
authorization = Buffer.from(
`Basic ${userPassword.toString('base64')}`,
'ascii',
);
} finally {
userPassword.fill(0);
}
} else if (scheme === 'bearer') {
if (
Object.keys(entry).sort().join(',') !== 'registry,scheme,token' ||
typeof entry.token !== 'string' ||
entry.token.length < 1 ||
entry.token.length > 8192 ||
!BEARER_TOKEN.test(entry.token)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} bearer value is invalid`,
);
}
authorization = Buffer.from(`Bearer ${entry.token}`, 'ascii');
} else {
throw new ClusterPluginPackageRecoveryProcessConfigError(
`registry credential ${index} scheme is invalid`,
);
}
authorizations.set(registry, authorization);
}
return new LoadedClusterPluginPackageRegistryCredentialFile(authorizations);
} catch (error) {
zeroAuthorizations(authorizations);
throw error;
} finally {
scrubCredentialSource(value);
}
}
export function loadClusterPluginPackagePublisherTrustFileEvidence(
filePath: string,
): Readonly<ClusterPluginPackagePublisherTrustFileEvidence> {
if (
typeof filePath !== 'string' ||
filePath.length < 1 ||
filePath.length > 4096 ||
/[\0\r\n]/.test(filePath) ||
!isAbsolute(filePath)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file path is invalid',
);
}
let descriptor: number;
try {
descriptor = openSync(filePath, constants.O_RDONLY);
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file is unavailable',
);
}
let bytes: Buffer;
try {
const stat = fstatSync(descriptor);
if (
!stat.isFile() ||
(stat.mode & 0o022) !== 0 ||
!Number.isSafeInteger(stat.size) ||
stat.size < 1 ||
stat.size > MAX_TRUST_FILE_BYTES
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file is not a bounded read-only regular file',
);
}
bytes = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < bytes.byteLength) {
const count = readSync(
descriptor,
bytes,
offset,
bytes.byteLength - offset,
null,
);
if (count === 0) break;
offset += count;
}
if (offset !== stat.size) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file changed while reading',
);
}
bytes = bytes.subarray(0, offset);
} finally {
closeSync(descriptor);
}
let value: unknown;
try {
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file is not valid JSON',
);
} finally {
bytes.fill(0);
}
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).sort().join(',') !== 'keys,schema' ||
(value as { schema?: unknown }).schema !== TRUST_SCHEMA ||
!Array.isArray((value as { keys?: unknown }).keys)
) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust file shape is invalid',
);
}
try {
const definitions = (
value as { keys: PluginPackagePublisherKeyDefinition[] }
).keys;
const frozenDefinitions = Object.freeze(
definitions.map((definition) => Object.freeze({ ...definition })),
);
return Object.freeze({
registry: new PluginPackagePublisherTrustRegistry(frozenDefinitions),
snapshot:
createPluginPackagePublisherTrustSnapshot(frozenDefinitions),
definitions: frozenDefinitions,
});
} catch {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust keys are invalid',
);
}
}
export function loadClusterPluginPackagePublisherTrustFile(
filePath: string,
): PluginPackagePublisherTrustRegistry {
return loadClusterPluginPackagePublisherTrustFileEvidence(filePath).registry;
}
async function productionKubernetesApi(): Promise<PluginPackageKubernetesConfigMapApi> {
const kubernetes = await import('@kubernetes/client-node');
const config = new kubernetes.KubeConfig();
config.loadFromCluster();
return config.makeApiClient(
kubernetes.CoreV1Api,
) as unknown as PluginPackageKubernetesConfigMapApi;
}
function processEvent(
config: Readonly<ClusterPluginPackageRecoveryProcessConfig>,
event: ClusterPluginPackageRecoveryProcessEvent['event'],
provenanceRecovery?: Readonly<ClusterPluginPackagePublisherProvenanceRecoveryResult>,
recovery?: Readonly<PluginPackageRecoveryCycleResult>,
taskPublicationRecovery?: Readonly<PluginPackageTaskPublicationRecoveryCycleResult>,
automationPublicationRecovery?: Readonly<PluginPackageAutomationPublicationRecoveryCycleResult>,
toolSnapshotRecovery?: Readonly<ProjectToolDefinitionSnapshotRecoveryCycleResult>,
): Readonly<ClusterPluginPackageRecoveryProcessEvent> {
return Object.freeze({
schemaVersion: 1,
component: 'qinglong3-plugin-package-recovery',
event,
clusterIdentity: config.clusterIdentity,
...(provenanceRecovery === undefined ? {} : { provenanceRecovery }),
...(recovery === undefined ? {} : { recovery }),
...(taskPublicationRecovery === undefined
? {}
: { taskPublicationRecovery }),
...(automationPublicationRecovery === undefined
? {}
: { automationPublicationRecovery }),
...(toolSnapshotRecovery === undefined ? {} : { toolSnapshotRecovery }),
});
}
async function emit(
sink: RunClusterPluginPackageRecoveryProcessOptions['emit'],
value: Readonly<ClusterPluginPackageRecoveryProcessEvent>,
): Promise<void> {
if (!sink) return;
try {
await sink(value);
} catch {
// Diagnostics cannot replace recovery or database close outcomes.
}
}
/** Runs exactly one admin recovery cycle and owns no resident authority. */
export async function runClusterPluginPackageRecoveryProcess(
options: RunClusterPluginPackageRecoveryProcessOptions,
): Promise<Readonly<ClusterPluginPackageRecoveryResult>> {
if (
!options ||
typeof options !== 'object' ||
(options.emit !== undefined && typeof options.emit !== 'function') ||
(options.openDatabase !== undefined &&
typeof options.openDatabase !== 'function') ||
(options.resourceByteSource !== undefined &&
(!options.resourceByteSource ||
typeof options.resourceByteSource.open !== 'function')) ||
(options.fetch !== undefined && typeof options.fetch !== 'function')
) {
throw new TypeError('Plugin Package recovery process options are invalid');
}
const config = loadClusterPluginPackageRecoveryProcessConfig(
options.environment,
);
const trustEvidence =
options.trust === undefined && options.stageAuthority === undefined
? loadClusterPluginPackagePublisherTrustFileEvidence(
config.publisherTrustFile,
)
: undefined;
const registryCredentials =
options.stageAuthority !== undefined ||
config.registryCredentialFile === undefined
? undefined
: loadClusterPluginPackageRegistryCredentialFile(
config.registryCredentialFile,
config.allowedRegistries,
);
try {
const api = options.api ?? (await productionKubernetesApi());
const openDatabase =
options.openDatabase ??
createPostgresDatabaseOpener({
role: 'package-executor',
connection: config.database.connection,
pool: config.database.pool,
onPoolError() {
// Awaited recovery queries and final close remain authoritative.
},
});
await emit(options.emit, processEvent(config, 'recovery_started'));
const result = await recoverClusterPluginPackages({
openDatabase,
api,
...(options.stageAuthority === undefined
? {
stageAuthorityFactory: async (pool: PostgresPool) => {
let effectiveTrust = options.trust;
if (effectiveTrust === undefined && trustEvidence !== undefined) {
const authority =
await new PostgresPluginPackagePublisherTrustAuthorityRepository(
pool,
).findAuthority(config.publisherTrustAuthorityId);
if (!authority) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'durable publisher trust authority is unavailable',
);
}
effectiveTrust =
createPluginPackagePublisherEffectiveTrustRegistry(
trustEvidence.definitions,
authority.effectiveSnapshot,
);
}
if (effectiveTrust === undefined) {
throw new ClusterPluginPackageRecoveryProcessConfigError(
'publisher trust evidence is unavailable',
);
}
return new ClusterPluginPackageOciStageAuthority({
allowedRegistries: config.allowedRegistries,
trust: effectiveTrust,
...(registryCredentials === undefined
? {}
: { credentialProvider: registryCredentials }),
...(options.fetch === undefined
? {}
: { fetch: options.fetch }),
requestTimeoutMs: config.requestTimeoutMs,
});
},
}
: { stageAuthority: options.stageAuthority }),
...(options.resourceByteSource === undefined
? {}
: { resourceByteSource: options.resourceByteSource }),
trustAuthorityId: config.publisherTrustAuthorityId,
clusterIdentity: config.clusterIdentity,
namespace: config.namespace,
now: Date.now,
pageSize: config.pageSize,
maxPages: config.maxPages,
});
await emit(
options.emit,
processEvent(
config,
'recovery_completed',
result.provenanceRecovery,
result.recovery,
result.taskPublicationRecovery,
result.automationPublicationRecovery,
result.toolSnapshotRecovery,
),
);
return result;
} finally {
registryCredentials?.dispose();
}
}