Files
stack/packages/mosaic/src/credentials/credential-validate-service.ts
T

142 lines
4.6 KiB
TypeScript

import { CredentialAuditJournal, CredentialJournalError } from './audit-journal.js';
import type {
CredentialValidationDependencies,
GiteaReadValidationRequestDto,
GiteaWriteValidationRequestDto,
} from './credential-provider.dto.js';
import type {
CredentialValidationResultDto,
RepositoryPermission,
} from './credential-result.dto.js';
import { evaluateGiteaReadValidation, evaluateGiteaWriteValidation } from './validate.js';
export interface CredentialValidationServiceOptions {
readonly stateRoot: string;
readonly actor: string;
readonly operation?: 'validate' | 'whoami';
}
function permissionDecision(permission: RepositoryPermission): string {
if (permission === 'none') return 'permission-none';
if (permission === 'admin') return 'permission-admin';
if (permission === 'write') return 'permission-write';
return 'permission-read';
}
async function openValidationJournal(
request: GiteaReadValidationRequestDto,
options: CredentialValidationServiceOptions,
): Promise<CredentialAuditJournal> {
const journal = await CredentialAuditJournal.open(options.stateRoot, {
operation: options.operation ?? 'validate',
actor: options.actor,
identity: request.identity,
estate: request.estate,
host: request.host,
repo: request.repo,
});
await journal.recordIntent(
options.operation === 'whoami' ? 'whoami-requested' : 'validation-requested',
);
return journal;
}
async function recordAndSealValidation(
journal: CredentialAuditJournal,
validation: CredentialValidationResultDto,
): Promise<CredentialValidationResultDto> {
if (validation.evidence.providerIdentity !== null) {
await journal.recordProviderEvidence({
endpoint: validation.evidence.providerIdentity.endpoint,
contentType: validation.evidence.providerIdentity.contentType,
decision: 'identity-verified',
});
}
if (validation.evidence.repositoryPermission !== null) {
await journal.recordProviderEvidence({
endpoint: validation.evidence.repositoryPermission.endpoint,
contentType: validation.evidence.repositoryPermission.contentType,
decision: permissionDecision(validation.evidence.repositoryPermission.effective),
});
}
await journal.seal(validation.outcome, validation.reason.code);
return {
...validation,
audit: { journalId: journal.journalId(), state: 'sealed' },
};
}
function journalFailureResult(
request: GiteaReadValidationRequestDto,
journal: CredentialAuditJournal,
error: CredentialJournalError,
operation: 'validate' | 'whoami',
): CredentialValidationResultDto {
return {
schemaVersion: 1,
operation,
outcome: 'error',
exitCode: 20,
retryable: false,
subject: {
identity: request.identity,
estate: request.estate,
host: request.host,
repo: request.repo,
},
mutation: 'none',
reason: {
code: error.code,
message: 'Validation audit persistence failed; inspect the durable open journal.',
},
evidence: {
providerIdentity: null,
tokenCapabilities: {
state: 'not-measured',
scopes: [],
source: 'runtime-not-authorized',
},
repositoryPermission: null,
writeDifferential: null,
},
audit: { journalId: journal.journalId(), state: 'open' },
};
}
export async function runCredentialReadValidation(
request: GiteaReadValidationRequestDto,
dependencies: CredentialValidationDependencies,
options: CredentialValidationServiceOptions,
): Promise<CredentialValidationResultDto> {
const journal = await openValidationJournal(request, options);
try {
const validation = await evaluateGiteaReadValidation(request, dependencies);
return await recordAndSealValidation(journal, {
...validation,
operation: options.operation ?? 'validate',
});
} catch (error: unknown) {
if (error instanceof CredentialJournalError) {
return journalFailureResult(request, journal, error, options.operation ?? 'validate');
}
throw error;
}
}
export async function runCredentialValidation(
request: GiteaWriteValidationRequestDto,
dependencies: CredentialValidationDependencies,
options: CredentialValidationServiceOptions,
): Promise<CredentialValidationResultDto> {
const journal = await openValidationJournal(request, options);
try {
const validation = await evaluateGiteaWriteValidation(request, dependencies);
return await recordAndSealValidation(journal, validation);
} catch (error: unknown) {
if (error instanceof CredentialJournalError) {
return journalFailureResult(request, journal, error, 'validate');
}
throw error;
}
}