mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
287 lines
9.0 KiB
TypeScript
287 lines
9.0 KiB
TypeScript
import { createHash } from 'crypto';
|
|
import { EXECUTOR_TYPES, type ExecutorType } from './execution';
|
|
import {
|
|
assertApprovalMutationId,
|
|
assertApprovalRequestId,
|
|
assertApprovalTimestamp,
|
|
} from './approvalRequest';
|
|
import { assertProjectPolicyProjectId } from './projectPolicy';
|
|
|
|
export const APPROVED_RUN_ACTION_TYPE = 'run.create';
|
|
export const APPROVED_RUN_RECEIPT_SCHEMA_VERSION = 1;
|
|
export const APPROVED_RUN_RECEIPT_RESULT_CODE = 'approved_run_created';
|
|
|
|
export interface ApprovedRunCreationPlan {
|
|
schemaVersion: 1;
|
|
actionRef: string;
|
|
projectId: string;
|
|
taskId: string;
|
|
taskRevision: string;
|
|
executorType: ExecutorType;
|
|
priority: number;
|
|
taskName?: string;
|
|
taskSnapshotRef?: string;
|
|
inputRef?: string;
|
|
}
|
|
|
|
export interface ApprovedRunCreationReceipt {
|
|
schemaVersion: 1;
|
|
dispatchId: string;
|
|
approvalRequestId: string;
|
|
projectId: string;
|
|
actionType: typeof APPROVED_RUN_ACTION_TYPE;
|
|
actionDigest: string;
|
|
executionAttempt: number;
|
|
executionVersion: number;
|
|
startedAtMs: number;
|
|
idempotencyKey: string;
|
|
outcome: 'succeeded';
|
|
resultCode: typeof APPROVED_RUN_RECEIPT_RESULT_CODE;
|
|
resourceType: 'run';
|
|
resourceId: string;
|
|
finishedAtMs: number;
|
|
evidenceDigest: string;
|
|
createdAtMs: number;
|
|
}
|
|
|
|
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
|
|
const MAX_EXECUTION_VERSION = 2_147_483_647;
|
|
|
|
export class InvalidApprovedRunActionError extends TypeError {
|
|
constructor(message: string) {
|
|
super(`Approved Run action is invalid: ${message}`);
|
|
this.name = 'InvalidApprovedRunActionError';
|
|
}
|
|
}
|
|
|
|
export class ApprovedRunActionBindingConflictError extends Error {
|
|
readonly code = 'APPROVED_RUN_ACTION_BINDING_CONFLICT';
|
|
|
|
constructor() {
|
|
super('Approved Run action identity does not match its durable receipt');
|
|
this.name = 'ApprovedRunActionBindingConflictError';
|
|
}
|
|
}
|
|
|
|
export class ApprovedRunActionRepositoryError extends Error {
|
|
readonly code = 'APPROVED_RUN_ACTION_REPOSITORY_ERROR';
|
|
|
|
constructor() {
|
|
super('Approved Run action repository is unavailable');
|
|
this.name = 'ApprovedRunActionRepositoryError';
|
|
}
|
|
}
|
|
|
|
function exactKeys(value: object, expected: readonly string[]): void {
|
|
const keys = Object.keys(value).sort();
|
|
const canonical = [...expected].sort();
|
|
if (
|
|
keys.length !== canonical.length ||
|
|
keys.some((key, index) => key !== canonical[index])
|
|
) {
|
|
throw new InvalidApprovedRunActionError('object shape is invalid');
|
|
}
|
|
}
|
|
|
|
function assertBoundedText(name: string, value: string, maximum: number): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > maximum ||
|
|
/[\u0000-\u001f\u007f]/.test(value)
|
|
) {
|
|
throw new InvalidApprovedRunActionError(`${name} is invalid`);
|
|
}
|
|
}
|
|
|
|
function assertIdentifier(name: string, value: string, maximum: number): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > maximum ||
|
|
!IDENTIFIER_PATTERN.test(value)
|
|
) {
|
|
throw new InvalidApprovedRunActionError(`${name} is invalid`);
|
|
}
|
|
}
|
|
|
|
function assertPositiveInteger(
|
|
name: string,
|
|
value: number,
|
|
maximum = MAX_EXECUTION_VERSION,
|
|
): void {
|
|
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
throw new InvalidApprovedRunActionError(`${name} is invalid`);
|
|
}
|
|
}
|
|
|
|
export function normalizeApprovedRunCreationPlan(
|
|
value: ApprovedRunCreationPlan,
|
|
): Readonly<ApprovedRunCreationPlan> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new InvalidApprovedRunActionError('plan must be an object');
|
|
}
|
|
exactKeys(value, [
|
|
'schemaVersion',
|
|
'actionRef',
|
|
'projectId',
|
|
'taskId',
|
|
'taskRevision',
|
|
'executorType',
|
|
'priority',
|
|
...(value.taskName === undefined ? [] : ['taskName']),
|
|
...(value.taskSnapshotRef === undefined ? [] : ['taskSnapshotRef']),
|
|
...(value.inputRef === undefined ? [] : ['inputRef']),
|
|
]);
|
|
if (value.schemaVersion !== 1) {
|
|
throw new InvalidApprovedRunActionError('schema version is unsupported');
|
|
}
|
|
assertIdentifier('actionRef', value.actionRef, 255);
|
|
assertProjectPolicyProjectId(value.projectId);
|
|
assertBoundedText('taskId', value.taskId, 255);
|
|
assertBoundedText('taskRevision', value.taskRevision, 128);
|
|
if (!EXECUTOR_TYPES.includes(value.executorType)) {
|
|
throw new InvalidApprovedRunActionError('executorType is unsupported');
|
|
}
|
|
if (
|
|
!Number.isSafeInteger(value.priority) ||
|
|
value.priority < -2_147_483_648 ||
|
|
value.priority > 2_147_483_647
|
|
) {
|
|
throw new InvalidApprovedRunActionError('priority is invalid');
|
|
}
|
|
if (value.taskName !== undefined) {
|
|
assertBoundedText('taskName', value.taskName, 255);
|
|
}
|
|
if (value.taskSnapshotRef !== undefined) {
|
|
assertBoundedText('taskSnapshotRef', value.taskSnapshotRef, 512);
|
|
}
|
|
if (value.inputRef !== undefined) {
|
|
assertBoundedText('inputRef', value.inputRef, 512);
|
|
}
|
|
return Object.freeze({ ...value });
|
|
}
|
|
|
|
export function digestApprovedRunCreationPlan(
|
|
value: ApprovedRunCreationPlan,
|
|
): string {
|
|
const plan = normalizeApprovedRunCreationPlan(value);
|
|
const canonical = JSON.stringify({
|
|
schemaVersion: plan.schemaVersion,
|
|
actionType: APPROVED_RUN_ACTION_TYPE,
|
|
actionRef: plan.actionRef,
|
|
projectId: plan.projectId,
|
|
taskId: plan.taskId,
|
|
taskRevision: plan.taskRevision,
|
|
executorType: plan.executorType,
|
|
priority: plan.priority,
|
|
taskName: plan.taskName ?? null,
|
|
taskSnapshotRef: plan.taskSnapshotRef ?? null,
|
|
inputRef: plan.inputRef ?? null,
|
|
});
|
|
return createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
}
|
|
|
|
function canonicalApprovedRunReceipt(
|
|
receipt: Omit<ApprovedRunCreationReceipt, 'evidenceDigest'>,
|
|
): string {
|
|
return JSON.stringify({
|
|
schemaVersion: receipt.schemaVersion,
|
|
dispatchId: receipt.dispatchId,
|
|
approvalRequestId: receipt.approvalRequestId,
|
|
projectId: receipt.projectId,
|
|
actionType: receipt.actionType,
|
|
actionDigest: receipt.actionDigest,
|
|
executionAttempt: receipt.executionAttempt,
|
|
executionVersion: receipt.executionVersion,
|
|
startedAtMs: receipt.startedAtMs,
|
|
idempotencyKey: receipt.idempotencyKey,
|
|
outcome: receipt.outcome,
|
|
resultCode: receipt.resultCode,
|
|
resourceType: receipt.resourceType,
|
|
resourceId: receipt.resourceId,
|
|
finishedAtMs: receipt.finishedAtMs,
|
|
createdAtMs: receipt.createdAtMs,
|
|
});
|
|
}
|
|
|
|
export function digestApprovedRunCreationReceipt(
|
|
receipt: Omit<ApprovedRunCreationReceipt, 'evidenceDigest'>,
|
|
): string {
|
|
return createHash('sha256')
|
|
.update(canonicalApprovedRunReceipt(receipt), 'utf8')
|
|
.digest('hex');
|
|
}
|
|
|
|
export function normalizeApprovedRunCreationReceipt(
|
|
value: ApprovedRunCreationReceipt,
|
|
): Readonly<ApprovedRunCreationReceipt> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new InvalidApprovedRunActionError('receipt must be an object');
|
|
}
|
|
exactKeys(value, [
|
|
'schemaVersion',
|
|
'dispatchId',
|
|
'approvalRequestId',
|
|
'projectId',
|
|
'actionType',
|
|
'actionDigest',
|
|
'executionAttempt',
|
|
'executionVersion',
|
|
'startedAtMs',
|
|
'idempotencyKey',
|
|
'outcome',
|
|
'resultCode',
|
|
'resourceType',
|
|
'resourceId',
|
|
'finishedAtMs',
|
|
'evidenceDigest',
|
|
'createdAtMs',
|
|
]);
|
|
if (value.schemaVersion !== APPROVED_RUN_RECEIPT_SCHEMA_VERSION) {
|
|
throw new InvalidApprovedRunActionError(
|
|
'receipt schema version is unsupported',
|
|
);
|
|
}
|
|
assertApprovalMutationId(value.dispatchId);
|
|
assertApprovalRequestId(value.approvalRequestId);
|
|
assertProjectPolicyProjectId(value.projectId);
|
|
if (value.actionType !== APPROVED_RUN_ACTION_TYPE) {
|
|
throw new InvalidApprovedRunActionError('actionType is invalid');
|
|
}
|
|
if (!DIGEST_PATTERN.test(value.actionDigest)) {
|
|
throw new InvalidApprovedRunActionError('actionDigest is invalid');
|
|
}
|
|
assertPositiveInteger('executionAttempt', value.executionAttempt, 16);
|
|
assertPositiveInteger('executionVersion', value.executionVersion);
|
|
assertApprovalTimestamp('startedAtMs', value.startedAtMs);
|
|
if (value.idempotencyKey !== value.dispatchId) {
|
|
throw new InvalidApprovedRunActionError('idempotency binding is invalid');
|
|
}
|
|
if (
|
|
value.outcome !== 'succeeded' ||
|
|
value.resultCode !== APPROVED_RUN_RECEIPT_RESULT_CODE ||
|
|
value.resourceType !== 'run'
|
|
) {
|
|
throw new InvalidApprovedRunActionError('result tuple is invalid');
|
|
}
|
|
assertIdentifier('resourceId', value.resourceId, 64);
|
|
assertApprovalTimestamp('finishedAtMs', value.finishedAtMs);
|
|
assertApprovalTimestamp('createdAtMs', value.createdAtMs);
|
|
if (
|
|
value.finishedAtMs < value.startedAtMs ||
|
|
value.createdAtMs !== value.finishedAtMs
|
|
) {
|
|
throw new InvalidApprovedRunActionError('receipt timestamps are invalid');
|
|
}
|
|
if (!DIGEST_PATTERN.test(value.evidenceDigest)) {
|
|
throw new InvalidApprovedRunActionError('evidenceDigest is invalid');
|
|
}
|
|
const { evidenceDigest, ...unsigned } = value;
|
|
if (digestApprovedRunCreationReceipt(unsigned) !== evidenceDigest) {
|
|
throw new InvalidApprovedRunActionError('evidenceDigest does not match');
|
|
}
|
|
return Object.freeze({ ...value });
|
|
}
|