Files
qinglong/scripts/ql3-worker-credential-management-live-ceremony.cjs
T

857 lines
24 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const net = require('node:net');
const os = require('node:os');
const path = require('node:path');
const { TextDecoder } = require('node:util');
const FIXTURE = 'qinglong/worker-credential-management-live-ceremony@v1';
const TYPE = 'ql3-worker-credential-management+jwt';
const PURPOSE = 'worker-credential-management';
const AUDIENCE = 'qinglong3-worker-credential-management';
const MAX_FILE_BYTES = 1024 * 1024;
const CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
const TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/;
const BANNED_KEYS = new Set([
'assertion',
'authorization',
'bearer',
'connectionstring',
'dsn',
'password',
'privatekey',
'secret',
'tlskey',
'token',
]);
class WorkerCredentialManagementLiveCeremonyError extends Error {
constructor(message) {
super(`Worker credential management live ceremony failed: ${message}`);
this.name = 'WorkerCredentialManagementLiveCeremonyError';
}
}
function fail(message) {
throw new WorkerCredentialManagementLiveCeremonyError(message);
}
function exactObject(value, keys, label) {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
JSON.stringify(Object.keys(value).sort()) !==
JSON.stringify([...keys].sort())
) {
fail(`${label} shape is invalid`);
}
return value;
}
function exactKeys(value, keys) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value).sort()) ===
JSON.stringify([...keys].sort())
);
}
function sha256(domain, value) {
return `sha256:${crypto
.createHash('sha256')
.update(domain)
.update('\0')
.update(value)
.digest('hex')}`;
}
function externalIssuer(value) {
if (typeof value !== 'string' || value.length > 512) return false;
let parsed;
try {
parsed = new URL(value);
} catch {
return false;
}
const hostname = parsed.hostname.toLowerCase();
return (
parsed.protocol === 'https:' &&
parsed.username === '' &&
parsed.password === '' &&
parsed.search === '' &&
parsed.hash === '' &&
parsed.toString() === value &&
net.isIP(hostname) === 0 &&
hostname !== 'localhost' &&
!hostname.endsWith('.localhost') &&
!hostname.endsWith('.local') &&
!hostname.endsWith('.test') &&
!hostname.endsWith('.invalid') &&
!hostname.endsWith('.example')
);
}
function canonicalFile(filePath, label, options = {}) {
if (
typeof filePath !== 'string' ||
!path.isAbsolute(filePath) ||
filePath.length > 4096 ||
CONTROL_PATTERN.test(filePath)
) {
fail(`${label} path is invalid`);
}
let stat;
try {
stat = fs.lstatSync(filePath);
} catch {
fail(`${label} is unavailable`);
}
const privateFile = options.private !== false;
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 1 ||
stat.size > (options.maximum ?? MAX_FILE_BYTES) ||
fs.realpathSync(filePath) !== filePath ||
(privateFile && (uid === null || stat.uid !== uid)) ||
(stat.mode & (privateFile ? 0o077 : 0o022)) !== 0
) {
fail(
`${label} must be a canonical bounded ${
privateFile ? 'private ' : ''
}file`,
);
}
return filePath;
}
function readPrivateText(filePath, label, maximum = MAX_FILE_BYTES) {
canonicalFile(filePath, label, { maximum });
const before = fs.lstatSync(filePath);
let descriptor = -1;
let bytes;
try {
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY |
(fs.constants.O_CLOEXEC ?? 0) |
(fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor);
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.uid !== before.uid ||
opened.mode !== before.mode ||
opened.size !== before.size
) {
fail(`${label} changed before it was opened`);
}
bytes = Buffer.alloc(opened.size);
let offset = 0;
while (offset < bytes.length) {
const count = fs.readSync(
descriptor,
bytes,
offset,
bytes.length - offset,
offset,
);
if (count < 1) fail(`${label} could not be read completely`);
offset += count;
}
const after = fs.fstatSync(descriptor);
if (
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.uid !== opened.uid ||
after.mode !== opened.mode ||
after.size !== opened.size
) {
fail(`${label} changed while it was read`);
}
return new TextDecoder('utf-8', { fatal: true }).decode(bytes).trim();
} catch (error) {
if (error instanceof WorkerCredentialManagementLiveCeremonyError) {
throw error;
}
fail(`${label} could not be read safely`);
} finally {
bytes?.fill(0);
if (descriptor >= 0) fs.closeSync(descriptor);
}
}
function readPrivateJson(filePath, label) {
try {
return JSON.parse(readPrivateText(filePath, label));
} catch (error) {
if (error instanceof WorkerCredentialManagementLiveCeremonyError) {
throw error;
}
fail(`${label} must contain JSON`);
}
}
function parseSegment(value, label, maximum) {
if (
typeof value !== 'string' ||
value.length < 1 ||
!BASE64URL_PATTERN.test(value)
) {
fail(`${label} encoding is invalid`);
}
const bytes = Buffer.from(value, 'base64url');
if (
bytes.length < 2 ||
bytes.length > maximum ||
bytes.toString('base64url') !== value
) {
fail(`${label} encoding is invalid`);
}
try {
const value = JSON.parse(bytes.toString('utf8'));
if (!value || typeof value !== 'object' || Array.isArray(value)) {
fail(`${label} must be an object`);
}
return value;
} catch (error) {
if (error instanceof WorkerCredentialManagementLiveCeremonyError) {
throw error;
}
fail(`${label} JSON is invalid`);
}
}
function assertionIdentity(assertion, nowMs) {
if (
typeof assertion !== 'string' ||
assertion.length > 16 * 1024 ||
CONTROL_PATTERN.test(assertion)
) {
fail('identity assertion is invalid');
}
const segments = assertion.split('.');
if (segments.length !== 3)
fail('identity assertion compact shape is invalid');
if (
!BASE64URL_PATTERN.test(segments[2]) ||
Buffer.from(segments[2], 'base64url').length < 32 ||
Buffer.from(segments[2], 'base64url').length > 512 ||
Buffer.from(segments[2], 'base64url').toString('base64url') !== segments[2]
) {
fail('identity signature encoding is invalid');
}
const header = parseSegment(segments[0], 'identity header', 1024);
exactObject(header, ['alg', 'kid', 'typ'], 'identity header');
const claims = parseSegment(segments[1], 'identity claims', 8 * 1024);
const claimKeys = [
'acr',
'amr',
'aud',
'auth_time',
'exp',
'iat',
'iss',
'jti',
'ql3_purpose',
'sub',
];
if (Object.hasOwn(claims, 'nbf')) claimKeys.push('nbf');
exactObject(claims, claimKeys, 'identity claims');
const now = Math.floor(nowMs / 1000);
if (
header.typ !== TYPE ||
typeof header.alg !== 'string' ||
header.alg === 'none' ||
!TOKEN_PATTERN.test(header.kid) ||
claims.aud !== AUDIENCE ||
claims.ql3_purpose !== PURPOSE ||
!externalIssuer(claims.iss) ||
!TOKEN_PATTERN.test(claims.sub) ||
!TOKEN_PATTERN.test(claims.jti) ||
typeof claims.acr !== 'string' ||
claims.acr.length < 1 ||
claims.acr.length > 256 ||
CONTROL_PATTERN.test(claims.acr) ||
!Array.isArray(claims.amr) ||
claims.amr.length < 1 ||
claims.amr.length > 8 ||
claims.amr.some((value) => !TOKEN_PATTERN.test(value)) ||
new Set(claims.amr).size !== claims.amr.length ||
![claims.iat, claims.auth_time, claims.exp].every(
(value) => Number.isSafeInteger(value) && value >= 0,
) ||
claims.exp <= now ||
claims.iat > now + 60 ||
claims.auth_time > now + 60 ||
claims.exp - claims.iat < 30 ||
claims.exp - claims.iat > 15 * 60 ||
now - claims.auth_time > 15 * 60 ||
(claims.nbf !== undefined &&
(!Number.isSafeInteger(claims.nbf) || claims.nbf > now + 60))
) {
fail(
'identity assertion does not bind one live strong Worker management identity',
);
}
return Object.freeze({
issuer: claims.iss,
subject: claims.sub,
jti: claims.jti,
kid: header.kid,
acr: claims.acr,
amr: Object.freeze([...claims.amr]),
});
}
function ceremonyCommands(value, normalize) {
exactObject(
value,
[
'schemaVersion',
'planRequest',
'approvalRequestId',
'approvalAuditEventId',
'requesterDecisionId',
'requesterDecisionAuditEventId',
'reviewerDecisionId',
'reviewerDecisionAuditEventId',
'decisionReasonCode',
'inspectionId',
],
'ceremony',
);
if (value.schemaVersion !== 1) fail('ceremony schemaVersion is invalid');
for (const name of [
'approvalRequestId',
'approvalAuditEventId',
'requesterDecisionId',
'requesterDecisionAuditEventId',
'reviewerDecisionId',
'reviewerDecisionAuditEventId',
'decisionReasonCode',
'inspectionId',
]) {
if (!TOKEN_PATTERN.test(value[name])) fail(`ceremony ${name} is invalid`);
}
if (
value.requesterDecisionId === value.reviewerDecisionId ||
value.requesterDecisionAuditEventId === value.reviewerDecisionAuditEventId
) {
fail('requester and reviewer decision identities must be distinct');
}
const shared = {
actionRef: value.planRequest?.actionRef,
authorityProjectId: value.planRequest?.authorityProjectId,
approvalRequestId: value.approvalRequestId,
};
const commands = {
plan: {
schemaVersion: 1,
operation: 'worker-credential.plan',
request: value.planRequest,
},
propose: {
schemaVersion: 1,
operation: 'worker-credential.propose',
request: {
...shared,
approvalAuditEventId: value.approvalAuditEventId,
},
},
requesterDecide: {
schemaVersion: 1,
operation: 'worker-credential.decide',
request: {
...shared,
expectedVersion: 1,
decisionId: value.requesterDecisionId,
auditEventId: value.requesterDecisionAuditEventId,
decision: 'approved',
reasonCode: value.decisionReasonCode,
},
},
reviewerDecide: {
schemaVersion: 1,
operation: 'worker-credential.decide',
request: {
...shared,
expectedVersion: 1,
decisionId: value.reviewerDecisionId,
auditEventId: value.reviewerDecisionAuditEventId,
decision: 'approved',
reasonCode: value.decisionReasonCode,
},
},
inspect: {
schemaVersion: 1,
operation: 'worker-credential.inspect',
request: { ...shared, inspectionId: value.inspectionId },
},
};
for (const command of Object.values(commands)) normalize(command);
return Object.freeze(commands);
}
function containsSensitiveMaterial(value, key = '') {
if (BANNED_KEYS.has(key.toLowerCase())) return true;
if (typeof value === 'string') {
return (
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/.test(value) ||
/postgres(?:ql)?:\/\/[^/\s]+:[^@\s]+@/i.test(value) ||
/\beyJ[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/.test(
value,
)
);
}
if (Array.isArray(value)) {
return value.some((entry) => containsSensitiveMaterial(entry));
}
if (value && typeof value === 'object') {
return Object.entries(value).some(([childKey, child]) =>
containsSensitiveMaterial(child, childKey),
);
}
return false;
}
function isIsoTime(value) {
return (
typeof value === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value) &&
Number.isFinite(Date.parse(value))
);
}
function validateWorkerCredentialManagementLiveCeremony(report) {
const findings = [];
const add = (code) => findings.push(Object.freeze({ code }));
if (
!report ||
typeof report !== 'object' ||
Array.isArray(report) ||
JSON.stringify(Object.keys(report).sort()) !==
JSON.stringify(
[
'schemaVersion',
'fixture',
'observedAt',
'identity',
'ceremony',
'gates',
].sort(),
) ||
report.schemaVersion !== 1 ||
report.fixture !== FIXTURE ||
!isIsoTime(report.observedAt)
) {
add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_SHAPE');
}
const identity = report?.identity;
if (
!exactKeys(identity, [
'providerKind',
'issuer',
'discoveryDocumentSha256',
'jwksSha256',
'audience',
'type',
'purpose',
'requesterSubjectSha256',
'reviewerSubjectSha256',
'requesterKeyIdSha256',
'reviewerKeyIdSha256',
]) ||
!externalIssuer(identity.issuer) ||
identity.providerKind !== 'external_oidc' ||
identity.audience !== AUDIENCE ||
identity.type !== TYPE ||
identity.purpose !== PURPOSE ||
!SHA256_PATTERN.test(identity.discoveryDocumentSha256) ||
!SHA256_PATTERN.test(identity.jwksSha256) ||
!SHA256_PATTERN.test(identity.requesterSubjectSha256) ||
!SHA256_PATTERN.test(identity.reviewerSubjectSha256) ||
identity.requesterSubjectSha256 === identity.reviewerSubjectSha256 ||
!SHA256_PATTERN.test(identity.requesterKeyIdSha256) ||
!SHA256_PATTERN.test(identity.reviewerKeyIdSha256)
) {
add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_IDENTITY');
}
const ceremony = report?.ceremony;
if (
!exactKeys(ceremony, [
'actionRefSha256',
'authorityProjectIdSha256',
'planStatus',
'approvalStatus',
'requesterSelfDecisionStatus',
'requesterSelfDecisionCode',
'reviewerDecisionStatus',
'approvalState',
'inspectionStale',
'dispatchCreated',
'approvalConsumed',
'planDigest',
'previewDigest',
'requestIdSha256',
]) ||
!['created', 'existing'].includes(ceremony.planStatus) ||
!['created', 'existing'].includes(ceremony.approvalStatus) ||
ceremony.requesterSelfDecisionStatus !== 403 ||
ceremony.requesterSelfDecisionCode !== 'forbidden' ||
!['decided', 'existing'].includes(ceremony.reviewerDecisionStatus) ||
ceremony.approvalState !== 'approved' ||
ceremony.inspectionStale !== false ||
ceremony.dispatchCreated !== false ||
ceremony.approvalConsumed !== false ||
!SHA256_PATTERN.test(ceremony.actionRefSha256) ||
!SHA256_PATTERN.test(ceremony.authorityProjectIdSha256) ||
!/^[a-f0-9]{64}$/.test(ceremony.planDigest) ||
!/^[a-f0-9]{64}$/.test(ceremony.previewDigest) ||
!Array.isArray(ceremony.requestIdSha256) ||
ceremony.requestIdSha256.length !== 5 ||
ceremony.requestIdSha256.some((value) => !SHA256_PATTERN.test(value)) ||
new Set(ceremony.requestIdSha256).size !== 5
) {
add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_FLOW');
}
const gates = report?.gates;
if (
!exactKeys(gates, [
'externalIdentity',
'workerPurposeBound',
'requesterAndReviewerDistinct',
'requesterSelfDecisionRejected',
'reviewerDecisionAccepted',
'inspectionAuthorized',
'noExecutionOrConsumption',
'passed',
]) ||
Object.values(gates).some((value) => value !== true)
) {
add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_GATES');
}
if (containsSensitiveMaterial(report)) {
add('QL3_WORKER_MANAGEMENT_LIVE_CEREMONY_SECRET_EXPOSURE');
}
return Object.freeze({
compatible: findings.length === 0,
findings: Object.freeze(findings),
});
}
function unusedOutput(filePath) {
if (
typeof filePath !== 'string' ||
!path.isAbsolute(filePath) ||
fs.existsSync(filePath) ||
fs.realpathSync(path.dirname(filePath)) !== path.dirname(filePath)
) {
fail('output must be one unused canonical absolute path');
}
return filePath;
}
function writeNoReplace(filePath, report) {
const descriptor = fs.openSync(filePath, 'wx', 0o600);
try {
fs.writeFileSync(descriptor, `${JSON.stringify(report, null, 2)}\n`);
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
async function withCommandFile(command, run) {
const directory = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-ceremony-')),
);
fs.chmodSync(directory, 0o700);
const commandFile = path.join(directory, 'command.json');
try {
fs.writeFileSync(commandFile, `${JSON.stringify(command)}\n`, {
mode: 0o600,
});
return await run(commandFile);
} finally {
fs.rmSync(directory, { recursive: true, force: true });
}
}
async function runWorkerCredentialManagementLiveCeremony(
options,
dependencies = {},
) {
exactObject(
options,
[
'configFile',
'requesterAssertionFile',
'reviewerAssertionFile',
'ceremonyFile',
'outputFile',
],
'options',
);
unusedOutput(options.outputFile);
canonicalFile(options.configFile, 'client config');
const now = dependencies.now ?? Date.now;
let requesterRaw = readPrivateText(
options.requesterAssertionFile,
'requester identity assertion',
16 * 1024,
);
let reviewerRaw = readPrivateText(
options.reviewerAssertionFile,
'reviewer identity assertion',
16 * 1024,
);
const requester = assertionIdentity(requesterRaw, now());
const reviewer = assertionIdentity(reviewerRaw, now());
if (
requester.issuer !== reviewer.issuer ||
requester.subject === reviewer.subject
) {
fail('requester and reviewer must be distinct Users from one issuer');
}
const normalize =
dependencies.normalize ??
require('@qinglong/cluster-admin/worker-credential-management-transport')
.normalizeClusterWorkerCredentialManagementCommand;
const commands = ceremonyCommands(
readPrivateJson(options.ceremonyFile, 'ceremony'),
normalize,
);
const collectOidc =
dependencies.collectOidc ??
require('./ql3-plugin-package-management-live-evidence-collect.cjs')
.collectOidcSnapshot;
const oidc = await collectOidc({ issuer: requester.issuer });
const execute =
dependencies.execute ??
require('@qinglong/cluster-admin/worker-credential-management-client')
.executeClusterWorkerCredentialManagementClient;
const invoke = (command, assertionFile) =>
withCommandFile(command, (commandFile) =>
execute({ configFile: options.configFile, commandFile, assertionFile }),
);
const plan = await invoke(commands.plan, options.requesterAssertionFile);
const proposed = await invoke(
commands.propose,
options.requesterAssertionFile,
);
let selfDecision;
try {
await invoke(commands.requesterDecide, options.requesterAssertionFile);
fail('requester self-decision was accepted');
} catch (error) {
if (
error instanceof WorkerCredentialManagementLiveCeremonyError ||
error?.statusCode !== 403 ||
error?.responseCode !== 'forbidden' ||
typeof error?.requestId !== 'string'
) {
throw error;
}
selfDecision = error;
}
const decided = await invoke(
commands.reviewerDecide,
options.reviewerAssertionFile,
);
const inspected = await invoke(
commands.inspect,
options.reviewerAssertionFile,
);
const planValue = plan.result.plan;
const proposedApproval = proposed.result.approval;
const decidedApproval = decided.result.approval;
const inspectedApproval = inspected.result.approval;
if (
planValue.requestedBy?.id !== requester.subject ||
proposed.result.plan.planDigest !== planValue.planDigest ||
proposedApproval.requestedBy?.id !== requester.subject ||
proposedApproval.version !== 1 ||
proposedApproval.state !== 'pending' ||
proposedApproval.actionDigest !== planValue.planDigest ||
proposedApproval.previewDigest !== planValue.previewDigest ||
decidedApproval.requestedBy?.id !== requester.subject ||
decidedApproval.decidedBy?.id !== reviewer.subject ||
decidedApproval.state !== 'approved' ||
inspected.result.stale !== false ||
!inspectedApproval ||
inspectedApproval.state !== 'approved' ||
inspectedApproval.decidedBy?.id !== reviewer.subject ||
inspectedApproval.dispatchId !== null ||
inspectedApproval.consumedAtMs !== null
) {
fail(
'management results do not prove the reviewed separation-of-duty flow',
);
}
const report = Object.freeze({
schemaVersion: 1,
fixture: FIXTURE,
observedAt: new Date(now()).toISOString(),
identity: Object.freeze({
providerKind: 'external_oidc',
issuer: requester.issuer,
discoveryDocumentSha256: oidc.discoveryDocumentSha256,
jwksSha256: oidc.jwksSha256,
audience: AUDIENCE,
type: TYPE,
purpose: PURPOSE,
requesterSubjectSha256: sha256(
'qinglong3.worker-management.subject.v1',
requester.subject,
),
reviewerSubjectSha256: sha256(
'qinglong3.worker-management.subject.v1',
reviewer.subject,
),
requesterKeyIdSha256: sha256(
'qinglong3.worker-management.kid.v1',
requester.kid,
),
reviewerKeyIdSha256: sha256(
'qinglong3.worker-management.kid.v1',
reviewer.kid,
),
}),
ceremony: Object.freeze({
actionRefSha256: sha256(
'qinglong3.worker-management.action-ref.v1',
planValue.actionRef,
),
authorityProjectIdSha256: sha256(
'qinglong3.worker-management.project.v1',
planValue.authorityProjectId,
),
planStatus: plan.result.status,
approvalStatus: proposed.result.approvalStatus,
requesterSelfDecisionStatus: 403,
requesterSelfDecisionCode: 'forbidden',
reviewerDecisionStatus: decided.result.status,
approvalState: inspectedApproval.state,
inspectionStale: inspected.result.stale,
dispatchCreated: inspectedApproval.dispatchId !== null,
approvalConsumed: inspectedApproval.consumedAtMs !== null,
planDigest: planValue.planDigest,
previewDigest: planValue.previewDigest,
requestIdSha256: Object.freeze(
[
plan.requestId,
proposed.requestId,
selfDecision.requestId,
decided.requestId,
inspected.requestId,
].map((value) =>
sha256('qinglong3.worker-management.request-id.v1', value),
),
),
}),
gates: Object.freeze({
externalIdentity: true,
workerPurposeBound: true,
requesterAndReviewerDistinct: true,
requesterSelfDecisionRejected: true,
reviewerDecisionAccepted: true,
inspectionAuthorized: true,
noExecutionOrConsumption: true,
passed: true,
}),
});
const audit = validateWorkerCredentialManagementLiveCeremony(report);
if (!audit.compatible) {
fail(
`assembled report failed audit: ${audit.findings
.map(({ code }) => code)
.join(',')}`,
);
}
writeNoReplace(options.outputFile, report);
requesterRaw = undefined;
reviewerRaw = undefined;
return report;
}
function parseArguments(argv) {
const values = {};
for (const argument of argv) {
if (argument === '--') continue;
const match = /^--([a-z-]+)=(.+)$/.exec(argument);
if (!match || Object.hasOwn(values, match[1]))
fail('arguments are invalid');
values[match[1]] = match[2];
}
const keys = [
'config',
'requester-assertion',
'reviewer-assertion',
'ceremony',
'output',
];
if (
JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(keys.sort())
) {
fail('arguments are invalid');
}
return Object.freeze({
configFile: values.config,
requesterAssertionFile: values['requester-assertion'],
reviewerAssertionFile: values['reviewer-assertion'],
ceremonyFile: values.ceremony,
outputFile: values.output,
});
}
async function runCli(argv) {
if (process.env.QL3_WORKER_CREDENTIAL_MANAGEMENT_LIVE_CEREMONY !== '1') {
fail('explicit live ceremony opt-in is required');
}
await runWorkerCredentialManagementLiveCeremony(parseArguments(argv));
process.stdout.write(
`${JSON.stringify({
schemaVersion: 1,
fixture: FIXTURE,
compatible: true,
})}\n`,
);
}
if (require.main === module) {
runCli(process.argv.slice(2)).catch((error) => {
process.stderr.write(
`${
error instanceof Error
? error.message
: 'Worker credential management live ceremony failed'
}\n`,
);
process.exitCode = 1;
});
}
module.exports = {
FIXTURE,
WorkerCredentialManagementLiveCeremonyError,
assertionIdentity,
ceremonyCommands,
parseArguments,
runWorkerCredentialManagementLiveCeremony,
validateWorkerCredentialManagementLiveCeremony,
};