mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
655 lines
20 KiB
TypeScript
655 lines
20 KiB
TypeScript
import type {
|
|
PostgresClient,
|
|
PostgresPool,
|
|
} from '@qinglong/runtime-core';
|
|
import {
|
|
ApprovalMutationConflictError,
|
|
ApprovalPolicyFenceConflictError,
|
|
ApprovalRequestNotFoundError,
|
|
ApprovalRequestStateConflictError,
|
|
ApprovalUnavailableError,
|
|
approvalRequestDigest,
|
|
approvedActionDispatchDigest,
|
|
consumeApprovalRequest,
|
|
decideApprovalRequest,
|
|
normalizeApprovalRequestRecord,
|
|
normalizeApprovedActionDispatchRecord,
|
|
type ApprovalRequestRecord,
|
|
type ApprovalRequestRepository,
|
|
type ApprovedActionDispatchRecord,
|
|
type ConsumeDurableApprovalRequestCommand,
|
|
type ConsumeDurableApprovalRequestResult,
|
|
type CreateApprovalRequestCommand,
|
|
type CreateApprovalRequestResult,
|
|
type DecideApprovalRequestResult,
|
|
type DecideDurableApprovalRequestCommand,
|
|
} from '@qinglong/runtime-core/approved-action';
|
|
import type {
|
|
SecurityPolicyFence,
|
|
SecuritySubject,
|
|
} from '@qinglong/runtime-core/security';
|
|
import {
|
|
normalizeSecurityAuditRecord,
|
|
type SecurityAuditRecord,
|
|
} from '@qinglong/runtime-core/security-audit';
|
|
|
|
import {
|
|
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES,
|
|
POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS,
|
|
configurePostgresDefinitionTransaction,
|
|
postgresRequiredBoolean,
|
|
postgresRequiredInteger,
|
|
postgresRequiredJsonObject,
|
|
postgresRequiredString,
|
|
postgresSqlState,
|
|
rollbackPostgresDefinitionTransaction,
|
|
} from '../repository/definitionRepositorySupport';
|
|
import {
|
|
findPostgresApprovedActionExecution,
|
|
insertPostgresApprovedActionExecutionBaseline,
|
|
} from './approvedActionExecutionRepository';
|
|
import { createApprovedActionExecution } from '@qinglong/runtime-core/approved-action-execution';
|
|
|
|
type Row = Record<string, unknown>;
|
|
type Queryable = Pick<PostgresPool, 'query'> | Pick<PostgresClient, 'query'>;
|
|
|
|
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
|
|
function unavailable(): ApprovalUnavailableError {
|
|
return new ApprovalUnavailableError();
|
|
}
|
|
|
|
function nullableString(value: unknown): string | null {
|
|
if (value === null) return null;
|
|
return postgresRequiredString(value, unavailable);
|
|
}
|
|
|
|
function nullableInteger(value: unknown): number | null {
|
|
if (value === null) return null;
|
|
return postgresRequiredInteger(value, unavailable);
|
|
}
|
|
|
|
function same(left: unknown, right: unknown): boolean {
|
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
}
|
|
|
|
function parseRequest(row: Row): Readonly<ApprovalRequestRecord> {
|
|
try {
|
|
const request = normalizeApprovalRequestRecord(
|
|
postgresRequiredJsonObject(
|
|
row.requestJson,
|
|
unavailable,
|
|
) as unknown as ApprovalRequestRecord,
|
|
);
|
|
if (
|
|
approvalRequestDigest(request) !==
|
|
postgresRequiredString(row.requestDigest, unavailable)
|
|
) {
|
|
throw unavailable();
|
|
}
|
|
return request;
|
|
} catch (error) {
|
|
if (error instanceof ApprovalUnavailableError) throw error;
|
|
throw unavailable();
|
|
}
|
|
}
|
|
|
|
function parseDispatch(row: Row): Readonly<ApprovedActionDispatchRecord> {
|
|
try {
|
|
const dispatch = normalizeApprovedActionDispatchRecord(
|
|
postgresRequiredJsonObject(
|
|
row.dispatchJson,
|
|
unavailable,
|
|
) as unknown as ApprovedActionDispatchRecord,
|
|
);
|
|
if (
|
|
approvedActionDispatchDigest(dispatch) !==
|
|
postgresRequiredString(row.dispatchDigest, unavailable)
|
|
) {
|
|
throw unavailable();
|
|
}
|
|
return dispatch;
|
|
} catch (error) {
|
|
if (error instanceof ApprovalUnavailableError) throw error;
|
|
throw unavailable();
|
|
}
|
|
}
|
|
|
|
function parseAudit(row: Row): Readonly<SecurityAuditRecord> {
|
|
try {
|
|
const subjectType = nullableString(row.subjectType);
|
|
const subjectId = nullableString(row.subjectId);
|
|
const fenceProjectVersion = nullableInteger(row.fenceProjectVersion);
|
|
const reasons = row.reasonsJson;
|
|
if (!Array.isArray(reasons)) throw unavailable();
|
|
return normalizeSecurityAuditRecord({
|
|
eventId: postgresRequiredString(row.eventId, unavailable),
|
|
requestId: postgresRequiredString(row.requestId, unavailable),
|
|
operationId: postgresRequiredString(row.operationId, unavailable),
|
|
projectId: nullableString(row.projectId),
|
|
subject:
|
|
subjectType === null || subjectId === null
|
|
? null
|
|
: {
|
|
type: subjectType as SecuritySubject['type'],
|
|
id: subjectId,
|
|
},
|
|
authenticationId: nullableString(row.authenticationId),
|
|
outcome: postgresRequiredString(
|
|
row.outcome,
|
|
unavailable,
|
|
) as SecurityAuditRecord['outcome'],
|
|
reasons: reasons as readonly string[],
|
|
fence:
|
|
fenceProjectVersion === null
|
|
? null
|
|
: {
|
|
projectVersion: fenceProjectVersion,
|
|
bindingVersion: nullableInteger(row.fenceBindingVersion),
|
|
},
|
|
occurredAtMs: postgresRequiredInteger(row.occurredAtMs, unavailable),
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof ApprovalUnavailableError) throw error;
|
|
throw unavailable();
|
|
}
|
|
}
|
|
|
|
function mappedError(error: unknown): Error {
|
|
if (
|
|
error instanceof ApprovalMutationConflictError ||
|
|
error instanceof ApprovalPolicyFenceConflictError ||
|
|
error instanceof ApprovalRequestNotFoundError ||
|
|
error instanceof ApprovalRequestStateConflictError ||
|
|
error instanceof ApprovalUnavailableError ||
|
|
(error instanceof Error &&
|
|
error.name.startsWith('Approval') &&
|
|
'code' in error)
|
|
) {
|
|
return error;
|
|
}
|
|
const state = postgresSqlState(error);
|
|
if (state === '23503' || state === '23505' || state === '23514') {
|
|
return new ApprovalMutationConflictError();
|
|
}
|
|
return new ApprovalUnavailableError({
|
|
cause: error instanceof Error ? error : undefined,
|
|
});
|
|
}
|
|
|
|
function auditMatches(
|
|
audit: Readonly<SecurityAuditRecord>,
|
|
expected: Readonly<{
|
|
operationId: string;
|
|
projectId: string;
|
|
subject: Readonly<SecuritySubject>;
|
|
authenticationId?: string;
|
|
outcome: SecurityAuditRecord['outcome'];
|
|
fence: Readonly<SecurityPolicyFence>;
|
|
}>,
|
|
): boolean {
|
|
return (
|
|
audit.operationId === expected.operationId &&
|
|
audit.projectId === expected.projectId &&
|
|
audit.subject?.type === expected.subject.type &&
|
|
audit.subject.id === expected.subject.id &&
|
|
(expected.authenticationId === undefined ||
|
|
audit.authenticationId === expected.authenticationId) &&
|
|
audit.outcome === expected.outcome &&
|
|
audit.fence?.projectVersion === expected.fence.projectVersion &&
|
|
audit.fence.bindingVersion === expected.fence.bindingVersion
|
|
);
|
|
}
|
|
|
|
async function requestById(
|
|
queryable: Queryable,
|
|
id: string,
|
|
): Promise<Readonly<ApprovalRequestRecord> | null> {
|
|
const result = await queryable.query<Row>(
|
|
`SELECT request_json AS "requestJson",
|
|
request_digest AS "requestDigest"
|
|
FROM "ql3"."approval_requests"
|
|
WHERE request_id = $1
|
|
LIMIT 2`,
|
|
[id],
|
|
);
|
|
if (result.rows.length === 0) return null;
|
|
if (result.rows.length !== 1) throw unavailable();
|
|
return parseRequest(result.rows[0]!);
|
|
}
|
|
|
|
async function dispatchById(
|
|
queryable: Queryable,
|
|
id: string,
|
|
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
|
|
const result = await queryable.query<Row>(
|
|
`SELECT dispatch_json AS "dispatchJson",
|
|
dispatch_digest AS "dispatchDigest"
|
|
FROM "ql3"."approved_action_dispatches"
|
|
WHERE dispatch_id = $1
|
|
LIMIT 2`,
|
|
[id],
|
|
);
|
|
if (result.rows.length === 0) return null;
|
|
if (result.rows.length !== 1) throw unavailable();
|
|
return parseDispatch(result.rows[0]!);
|
|
}
|
|
|
|
async function auditById(
|
|
queryable: Queryable,
|
|
id: string,
|
|
): Promise<Readonly<SecurityAuditRecord> | null> {
|
|
const result = await queryable.query<Row>(
|
|
`SELECT event_id AS "eventId", request_id AS "requestId",
|
|
operation_id AS "operationId", project_id AS "projectId",
|
|
subject_type AS "subjectType", subject_id AS "subjectId",
|
|
authentication_id AS "authenticationId", outcome AS "outcome",
|
|
reasons AS "reasonsJson",
|
|
project_version AS "fenceProjectVersion",
|
|
binding_version AS "fenceBindingVersion",
|
|
occurred_at_ms AS "occurredAtMs"
|
|
FROM "ql3"."security_audit_events"
|
|
WHERE event_id = $1
|
|
LIMIT 2`,
|
|
[id],
|
|
);
|
|
if (result.rows.length === 0) return null;
|
|
if (result.rows.length !== 1) throw unavailable();
|
|
return parseAudit(result.rows[0]!);
|
|
}
|
|
|
|
async function assertFence(
|
|
client: PostgresClient,
|
|
projectId: string,
|
|
subject: Readonly<SecuritySubject>,
|
|
fence: Readonly<SecurityPolicyFence>,
|
|
): Promise<void> {
|
|
const result = await client.query<Row>(
|
|
`SELECT "ql3"."lock_approval_policy_fence"(
|
|
$1::varchar, $2::varchar, $3::varchar, $4::integer, $5::integer
|
|
) AS "matches"`,
|
|
[
|
|
projectId,
|
|
subject.type,
|
|
subject.id,
|
|
fence.projectVersion,
|
|
fence.bindingVersion,
|
|
],
|
|
);
|
|
if (
|
|
result.rows.length !== 1 ||
|
|
!postgresRequiredBoolean(result.rows[0]!.matches, unavailable)
|
|
) {
|
|
throw new ApprovalPolicyFenceConflictError();
|
|
}
|
|
}
|
|
|
|
async function insertAudit(
|
|
client: PostgresClient,
|
|
value: SecurityAuditRecord,
|
|
): Promise<void> {
|
|
const audit = normalizeSecurityAuditRecord(value);
|
|
const result = await client.query(
|
|
`INSERT INTO "ql3"."security_audit_events" (
|
|
event_id, request_id, operation_id, project_id, subject_type,
|
|
subject_id, authentication_id, outcome, reasons,
|
|
project_version, binding_version, occurred_at_ms
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11, $12
|
|
)`,
|
|
[
|
|
audit.eventId,
|
|
audit.requestId,
|
|
audit.operationId,
|
|
audit.projectId,
|
|
audit.subject?.type ?? null,
|
|
audit.subject?.id ?? null,
|
|
audit.authenticationId,
|
|
audit.outcome,
|
|
JSON.stringify(audit.reasons),
|
|
audit.fence?.projectVersion ?? null,
|
|
audit.fence?.bindingVersion ?? null,
|
|
audit.occurredAtMs,
|
|
],
|
|
);
|
|
if (result.rowCount !== 1) throw unavailable();
|
|
}
|
|
|
|
async function insertRequest(
|
|
client: PostgresClient,
|
|
request: Readonly<ApprovalRequestRecord>,
|
|
): Promise<void> {
|
|
const result = await client.query(
|
|
`INSERT INTO "ql3"."approval_requests" (
|
|
request_id, project_id, version, state, action_type, action_ref,
|
|
action_digest, preview_digest, requested_by_type, requested_by_id,
|
|
decision_id, consumption_id, dispatch_id, expires_at_ms, request_json,
|
|
request_digest, updated_at_ms
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
|
|
$15::jsonb, $16, $17
|
|
)`,
|
|
[
|
|
request.id,
|
|
request.projectId,
|
|
request.version,
|
|
request.state,
|
|
request.action.actionType,
|
|
request.action.actionRef,
|
|
request.action.actionDigest,
|
|
request.action.previewDigest,
|
|
request.requestedBy.type,
|
|
request.requestedBy.id,
|
|
request.decisionId,
|
|
request.consumptionId,
|
|
request.dispatchId,
|
|
request.expiresAtMs,
|
|
JSON.stringify(request),
|
|
approvalRequestDigest(request),
|
|
request.consumedAtMs ?? request.decidedAtMs ?? request.requestedAtMs,
|
|
],
|
|
);
|
|
if (result.rowCount !== 1) throw unavailable();
|
|
}
|
|
|
|
async function updateRequest(
|
|
client: PostgresClient,
|
|
request: Readonly<ApprovalRequestRecord>,
|
|
expectedVersion: number,
|
|
): Promise<void> {
|
|
const result = await client.query(
|
|
`UPDATE "ql3"."approval_requests"
|
|
SET version = $1, state = $2, decision_id = $3, consumption_id = $4,
|
|
dispatch_id = $5, request_json = $6::jsonb, request_digest = $7,
|
|
updated_at_ms = $8
|
|
WHERE request_id = $9 AND version = $10`,
|
|
[
|
|
request.version,
|
|
request.state,
|
|
request.decisionId,
|
|
request.consumptionId,
|
|
request.dispatchId,
|
|
JSON.stringify(request),
|
|
approvalRequestDigest(request),
|
|
request.consumedAtMs ?? request.decidedAtMs ?? request.requestedAtMs,
|
|
request.id,
|
|
expectedVersion,
|
|
],
|
|
);
|
|
if (result.rowCount !== 1) throw new ApprovalRequestStateConflictError();
|
|
}
|
|
|
|
async function insertDispatch(
|
|
client: PostgresClient,
|
|
dispatch: Readonly<ApprovedActionDispatchRecord>,
|
|
): Promise<void> {
|
|
const result = await client.query(
|
|
`INSERT INTO "ql3"."approved_action_dispatches" (
|
|
dispatch_id, approval_request_id, project_id, action_type, action_ref,
|
|
action_digest, preview_digest, dispatch_json, dispatch_digest,
|
|
created_at_ms
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10
|
|
)`,
|
|
[
|
|
dispatch.id,
|
|
dispatch.approvalRequestId,
|
|
dispatch.projectId,
|
|
dispatch.action.actionType,
|
|
dispatch.action.actionRef,
|
|
dispatch.action.actionDigest,
|
|
dispatch.action.previewDigest,
|
|
JSON.stringify(dispatch),
|
|
approvedActionDispatchDigest(dispatch),
|
|
dispatch.createdAtMs,
|
|
],
|
|
);
|
|
if (result.rowCount !== 1) throw unavailable();
|
|
}
|
|
|
|
/** Administration-only PostgreSQL Approval/Approved Action authority. */
|
|
export class PostgresApprovalRequestRepository
|
|
implements ApprovalRequestRepository
|
|
{
|
|
constructor(private readonly pool: PostgresPool) {
|
|
if (
|
|
!pool ||
|
|
typeof pool.query !== 'function' ||
|
|
typeof pool.connect !== 'function'
|
|
) {
|
|
throw new TypeError('PostgreSQL Approval pool is invalid');
|
|
}
|
|
}
|
|
|
|
async #transaction<T>(
|
|
work: (client: PostgresClient) => Promise<T>,
|
|
): Promise<T> {
|
|
for (
|
|
let attempt = 0;
|
|
attempt < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS;
|
|
attempt += 1
|
|
) {
|
|
let client: PostgresClient;
|
|
try {
|
|
client = await this.pool.connect();
|
|
} catch (error) {
|
|
throw mappedError(error);
|
|
}
|
|
let began = false;
|
|
try {
|
|
await configurePostgresDefinitionTransaction(client);
|
|
began = true;
|
|
const result = await work(client);
|
|
await client.query('COMMIT');
|
|
began = false;
|
|
return result;
|
|
} catch (error) {
|
|
if (began) await rollbackPostgresDefinitionTransaction(client);
|
|
const state = postgresSqlState(error);
|
|
if (
|
|
state &&
|
|
POSTGRES_DEFINITION_RETRYABLE_SQL_STATES.has(state) &&
|
|
attempt + 1 < POSTGRES_DEFINITION_TRANSACTION_ATTEMPTS
|
|
) {
|
|
continue;
|
|
}
|
|
throw mappedError(error);
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
throw unavailable();
|
|
}
|
|
|
|
async findById(
|
|
id: string,
|
|
): Promise<Readonly<ApprovalRequestRecord> | null> {
|
|
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
|
|
throw new TypeError('Approval request lookup identity is invalid');
|
|
}
|
|
try {
|
|
return await requestById(this.pool, id);
|
|
} catch (error) {
|
|
throw mappedError(error);
|
|
}
|
|
}
|
|
|
|
async findDispatchById(
|
|
id: string,
|
|
): Promise<Readonly<ApprovedActionDispatchRecord> | null> {
|
|
if (typeof id !== 'string' || !IDENTIFIER_PATTERN.test(id)) {
|
|
throw new TypeError('Approved action dispatch identity is invalid');
|
|
}
|
|
try {
|
|
return await dispatchById(this.pool, id);
|
|
} catch (error) {
|
|
throw mappedError(error);
|
|
}
|
|
}
|
|
|
|
create(
|
|
command: CreateApprovalRequestCommand,
|
|
): Promise<CreateApprovalRequestResult> {
|
|
const request = normalizeApprovalRequestRecord(command.request);
|
|
const audit = normalizeSecurityAuditRecord(command.audit);
|
|
if (
|
|
request.state !== 'pending' ||
|
|
request.version !== 1 ||
|
|
!auditMatches(audit, {
|
|
operationId: 'approval.request',
|
|
projectId: request.projectId,
|
|
subject: request.requestedBy,
|
|
outcome: 'approval_required',
|
|
fence: request.requestFence,
|
|
})
|
|
) {
|
|
throw new ApprovalMutationConflictError();
|
|
}
|
|
return this.#transaction(async (client) => {
|
|
const existing = await requestById(client, request.id);
|
|
if (existing) {
|
|
const storedAudit = await auditById(client, audit.eventId);
|
|
if (
|
|
!same(existing, request) ||
|
|
!storedAudit ||
|
|
!same(storedAudit, audit)
|
|
) {
|
|
throw new ApprovalMutationConflictError();
|
|
}
|
|
return Object.freeze({ status: 'existing' as const, request });
|
|
}
|
|
await assertFence(
|
|
client,
|
|
request.projectId,
|
|
request.requestedBy,
|
|
request.requestFence,
|
|
);
|
|
await insertRequest(client, request);
|
|
await insertAudit(client, audit);
|
|
return Object.freeze({ status: 'created' as const, request });
|
|
});
|
|
}
|
|
|
|
decide(
|
|
command: DecideDurableApprovalRequestCommand,
|
|
): Promise<DecideApprovalRequestResult> {
|
|
const audit = normalizeSecurityAuditRecord(command.audit);
|
|
return this.#transaction(async (client) => {
|
|
const current = await requestById(client, command.requestId);
|
|
if (!current) throw new ApprovalRequestNotFoundError();
|
|
const request = decideApprovalRequest(current, {
|
|
expectedVersion: command.expectedVersion,
|
|
decisionId: command.decisionId,
|
|
decision: command.decision,
|
|
reasonCode: command.reasonCode,
|
|
principal: command.principal,
|
|
decidedAtMs: command.decidedAtMs,
|
|
authorizationFence: command.authorizationFence,
|
|
});
|
|
if (
|
|
!auditMatches(audit, {
|
|
operationId: 'approval.decide',
|
|
projectId: request.projectId,
|
|
subject: command.principal.subject,
|
|
authenticationId: command.principal.authenticationId,
|
|
outcome: 'allowed',
|
|
fence: command.authorizationFence,
|
|
})
|
|
) {
|
|
throw new ApprovalMutationConflictError();
|
|
}
|
|
if (current.version === 2 && current.decisionId === command.decisionId) {
|
|
const storedAudit = await auditById(client, audit.eventId);
|
|
if (!storedAudit || !same(storedAudit, audit)) {
|
|
throw new ApprovalMutationConflictError();
|
|
}
|
|
return Object.freeze({ status: 'existing' as const, request });
|
|
}
|
|
await assertFence(
|
|
client,
|
|
request.projectId,
|
|
command.principal.subject,
|
|
command.authorizationFence,
|
|
);
|
|
await updateRequest(client, request, command.expectedVersion);
|
|
await insertAudit(client, audit);
|
|
return Object.freeze({ status: 'decided' as const, request });
|
|
});
|
|
}
|
|
|
|
consume(
|
|
command: ConsumeDurableApprovalRequestCommand,
|
|
): Promise<ConsumeDurableApprovalRequestResult> {
|
|
const audit = normalizeSecurityAuditRecord(command.audit);
|
|
return this.#transaction(async (client) => {
|
|
const current = await requestById(client, command.requestId);
|
|
if (!current) throw new ApprovalRequestNotFoundError();
|
|
const result = consumeApprovalRequest(current, {
|
|
expectedVersion: command.expectedVersion,
|
|
consumptionId: command.consumptionId,
|
|
dispatchId: command.dispatchId,
|
|
action: command.action,
|
|
requestedBy: command.requestedBy,
|
|
consumedBy: command.consumedBy,
|
|
consumedAtMs: command.consumedAtMs,
|
|
authorizationFence: command.authorizationFence,
|
|
});
|
|
if (
|
|
!auditMatches(audit, {
|
|
operationId: 'approval.consume',
|
|
projectId: result.request.projectId,
|
|
subject: command.consumedBy,
|
|
outcome: 'allowed',
|
|
fence: command.authorizationFence,
|
|
})
|
|
) {
|
|
throw new ApprovalMutationConflictError();
|
|
}
|
|
if (
|
|
current.version === 3 &&
|
|
current.consumptionId === command.consumptionId
|
|
) {
|
|
const storedAudit = await auditById(client, audit.eventId);
|
|
const dispatch = await dispatchById(client, result.dispatch.id);
|
|
const execution = await findPostgresApprovedActionExecution(
|
|
client,
|
|
result.dispatch.id,
|
|
);
|
|
if (
|
|
!storedAudit ||
|
|
!same(storedAudit, audit) ||
|
|
!dispatch ||
|
|
!same(dispatch, result.dispatch) ||
|
|
!execution ||
|
|
!same(execution, createApprovedActionExecution(result.dispatch))
|
|
) {
|
|
throw new ApprovalMutationConflictError();
|
|
}
|
|
return Object.freeze({
|
|
status: 'existing' as const,
|
|
request: result.request,
|
|
dispatch: result.dispatch,
|
|
});
|
|
}
|
|
await assertFence(
|
|
client,
|
|
result.request.projectId,
|
|
command.requestedBy,
|
|
command.authorizationFence,
|
|
);
|
|
await insertDispatch(client, result.dispatch);
|
|
await insertPostgresApprovedActionExecutionBaseline(
|
|
client,
|
|
result.dispatch,
|
|
);
|
|
await updateRequest(client, result.request, command.expectedVersion);
|
|
await insertAudit(client, audit);
|
|
return Object.freeze({
|
|
status: 'consumed' as const,
|
|
request: result.request,
|
|
dispatch: result.dispatch,
|
|
});
|
|
});
|
|
}
|
|
}
|