fix(mosaic): bind delegated lifecycle evidence

This commit is contained in:
be-coder-06
2026-08-05 13:37:44 -05:00
parent 1391daae4c
commit 389a8d36f2
7 changed files with 124 additions and 19 deletions
@@ -1,4 +1,4 @@
import { CredentialAuditJournal } from './audit-journal.js';
import { CredentialAuditJournal, CredentialJournalError } from './audit-journal.js';
import type {
CredentialValidationDependencies,
GiteaReadValidationRequestDto,
@@ -66,17 +66,61 @@ async function recordAndSealValidation(
};
}
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);
const validation = await evaluateGiteaReadValidation(request, dependencies);
return recordAndSealValidation(journal, {
...validation,
operation: options.operation ?? 'validate',
});
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(
@@ -85,6 +129,13 @@ export async function runCredentialValidation(
options: CredentialValidationServiceOptions,
): Promise<CredentialValidationResultDto> {
const journal = await openValidationJournal(request, options);
const validation = await evaluateGiteaWriteValidation(request, dependencies);
return recordAndSealValidation(journal, validation);
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;
}
}
+28 -1
View File
@@ -292,6 +292,24 @@ export async function revokeCredential(
auditState: 'sealed',
});
}
const identity = await provider.readBasicIdentity(authority);
if (identity.login !== request.identity || authority.identity !== request.identity) {
await journal.seal('refused', 'provider-identity-mismatch');
return lifecycleResult('revoke', request, {
outcome: 'refused',
mutation: 'none',
code: 'provider-identity-mismatch',
message: 'Delegated Basic authority did not bind the requested principal.',
journalId: journal.journalId(),
auditState: 'sealed',
providerIdentity: identity.login,
});
}
await journal.recordProviderEvidence({
endpoint: identity.endpoint,
contentType: identity.contentType,
decision: 'identity-verified',
});
mutation = 'unknown';
await provider.revokeToken(authority, request.identity, binding.tokenName);
mutation = 'applied';
@@ -323,7 +341,16 @@ export async function revokeCredential(
auditState: 'sealed',
});
} catch (error: unknown) {
if (error instanceof CredentialJournalError) throw error;
if (error instanceof CredentialJournalError) {
return lifecycleResult('revoke', request, {
outcome: 'indeterminate',
mutation,
code: error.code,
message: 'Audit persistence failed; inspect the durable open journal before recovery.',
journalId: journal.journalId(),
auditState: 'open',
});
}
await journal.seal('indeterminate', 'mutation-state-unknown');
return lifecycleResult('revoke', request, {
outcome: 'indeterminate',
@@ -42,6 +42,15 @@ const loginSchema = z
.passthrough();
const configSchema = z.object({ logins: z.array(loginSchema).default([]) }).passthrough();
async function syncDirectory(path: string): Promise<void> {
const handle = await open(path, 'r');
try {
await handle.sync();
} finally {
await handle.close();
}
}
async function acquireLock(path: string): Promise<Awaited<ReturnType<typeof open>>> {
for (let attempt = 0; attempt < 500; attempt += 1) {
try {
@@ -122,6 +131,7 @@ export class TeaLoginStore {
await handle.close();
}
await rename(temp, this.configPath);
await syncDirectory(directory);
} finally {
await lock.close();
await unlink(lockPath).catch((): void => undefined);
@@ -192,6 +202,7 @@ export class TeaLoginStore {
await handle.close();
}
await rename(temp, this.configPath);
await syncDirectory(directory);
} finally {
await lock.close();
await unlink(lockPath).catch((): void => undefined);