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,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();
}
}