mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): prove first usable automation journey
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
readPrivateLocalCommandFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import {
|
||||
installLocalOwnerCredentialPresentation,
|
||||
LocalOwnerSecretDeliveryError,
|
||||
openLocalOwnerConsole,
|
||||
type ClaimLocalOwnerFromDeliveriesRequest,
|
||||
@@ -71,6 +72,16 @@ export interface AcknowledgeLocalOwnerDeliveryCommand {
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstallLocalOwnerCredentialPresentationCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.credential-presentation.install-from-delivery';
|
||||
readonly options: OpenLocalOwnerConsoleOptions;
|
||||
readonly request: {
|
||||
readonly credentialMutationId: string;
|
||||
readonly destinationFilePath: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IssueLocalOwnerCredentialRecoveryCommand {
|
||||
readonly schemaVersion: 1;
|
||||
readonly operation: 'owner.credential-recovery.issue';
|
||||
@@ -101,6 +112,7 @@ export type LocalOwnerCommand =
|
||||
| ClaimLocalOwnerCommand
|
||||
| InspectLocalOwnerDeliveryCommand
|
||||
| AcknowledgeLocalOwnerDeliveryCommand
|
||||
| InstallLocalOwnerCredentialPresentationCommand
|
||||
| IssueLocalOwnerCredentialRecoveryCommand
|
||||
| CompleteLocalOwnerCredentialRecoveryCommand;
|
||||
|
||||
@@ -153,6 +165,7 @@ function normalizeCommand(value: unknown): Readonly<LocalOwnerCommand> {
|
||||
'owner.claim.from-deliveries',
|
||||
'owner.delivery.inspect',
|
||||
'owner.delivery.acknowledge',
|
||||
'owner.credential-presentation.install-from-delivery',
|
||||
'owner.credential-recovery.issue',
|
||||
'owner.credential-recovery.complete',
|
||||
];
|
||||
@@ -222,6 +235,20 @@ function validateAcknowledgementRequest(
|
||||
}
|
||||
}
|
||||
|
||||
function validatePresentationInstallRequest(
|
||||
value: InstallLocalOwnerCredentialPresentationCommand['request'],
|
||||
): void {
|
||||
if (
|
||||
!exactKeys(value, ['credentialMutationId', 'destinationFilePath']) ||
|
||||
!UUID_V4_PATTERN.test(value.credentialMutationId) ||
|
||||
typeof value.destinationFilePath !== 'string'
|
||||
) {
|
||||
throw new LocalOwnerCliConfigurationError(
|
||||
'credential presentation install request is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function missing(error: unknown): boolean {
|
||||
let current = error;
|
||||
for (let depth = 0; depth < 4; depth += 1) {
|
||||
@@ -355,6 +382,22 @@ async function execute(
|
||||
ttlMs: acknowledgement.ttlMs,
|
||||
});
|
||||
}
|
||||
case 'owner.credential-presentation.install-from-delivery': {
|
||||
validatePresentationInstallRequest(command.request);
|
||||
const result = installLocalOwnerCredentialPresentation({
|
||||
deploymentRoot: command.options.deploymentRoot,
|
||||
deliveryFilePath: console.credentialDeliveryPath(
|
||||
command.request.credentialMutationId,
|
||||
),
|
||||
destinationFilePath: command.request.destinationFilePath,
|
||||
});
|
||||
return Object.freeze({
|
||||
schemaVersion: 1 as const,
|
||||
operation: command.operation,
|
||||
status: result.status,
|
||||
credentialMutationId: result.credentialMutationId,
|
||||
});
|
||||
}
|
||||
case 'owner.credential-recovery.issue': {
|
||||
const result = await console.credentialRecovery.issue(command.request);
|
||||
const delivery = deliveryAfterMutation(result.status, () =>
|
||||
|
||||
@@ -145,6 +145,64 @@ test('completes fresh Owner and credential recovery ceremonies without returning
|
||||
assert.equal(claimed.status, 'inserted');
|
||||
assert.equal(claimed.role, 'owner');
|
||||
assert.equal(JSON.stringify(claimed).includes('secret'), false);
|
||||
const credentialFilePath = path.join(
|
||||
state.options.deploymentRoot,
|
||||
'owner-credential.json',
|
||||
);
|
||||
const installed = await runner.run(
|
||||
commandFile(
|
||||
state,
|
||||
'owner.credential-presentation.install-from-delivery',
|
||||
{ credentialMutationId, destinationFilePath: credentialFilePath },
|
||||
'04b-install-presentation',
|
||||
),
|
||||
);
|
||||
assert.deepEqual(installed, {
|
||||
schemaVersion: 1,
|
||||
operation: 'owner.credential-presentation.install-from-delivery',
|
||||
status: 'installed',
|
||||
credentialMutationId,
|
||||
});
|
||||
const presentation = JSON.parse(fs.readFileSync(credentialFilePath, 'utf8'));
|
||||
assert.equal(
|
||||
presentation.kind,
|
||||
'qinglong3-local-identity-credential-presentation',
|
||||
);
|
||||
assert.match(
|
||||
presentation.token,
|
||||
/^ql3c_own_[A-Za-z0-9_-]{22}_[A-Za-z0-9_-]{43}$/,
|
||||
);
|
||||
assert.equal(fs.statSync(credentialFilePath).mode & 0o777, 0o600);
|
||||
const replayedInstall = await runner.run(
|
||||
commandFile(
|
||||
state,
|
||||
'owner.credential-presentation.install-from-delivery',
|
||||
{ credentialMutationId, destinationFilePath: credentialFilePath },
|
||||
'04c-replay-presentation',
|
||||
),
|
||||
);
|
||||
assert.equal(replayedInstall.status, 'existing');
|
||||
assertNoSecretFields(installed);
|
||||
assertNoSecretFields(replayedInstall);
|
||||
const escapedDestination = path.join(
|
||||
os.tmpdir(),
|
||||
`ql3-owner-credential-escape-${process.pid}.json`,
|
||||
);
|
||||
await assert.rejects(
|
||||
runner.run(
|
||||
commandFile(
|
||||
state,
|
||||
'owner.credential-presentation.install-from-delivery',
|
||||
{
|
||||
credentialMutationId,
|
||||
destinationFilePath: escapedDestination,
|
||||
},
|
||||
'04d-reject-escaped-presentation',
|
||||
),
|
||||
),
|
||||
/destinationFilePath must be a descendant of deploymentRoot/,
|
||||
);
|
||||
assert.equal(fs.existsSync(escapedDestination), false);
|
||||
for (const [purpose, mutationId, digest, suffix] of [
|
||||
[
|
||||
'credential-provisioning',
|
||||
|
||||
Reference in New Issue
Block a user