/*
RFC-0558: identity.credential.issue — issue a SiteOwnershipCredential
or ActorDelegationCredential and append it to werkstatt.identity.json.
- Do not verify credentials — that is identity.credential.verify.
- Do not revoke credentials — that is identity.credential.revoke.
- RFC-0558: initial identity.credential.issue command handler.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { signIdentityCredential } from "@warpgogol/werkstatt-shared/passport/identity-sign";
import type {
WerkstattCredential,
SiteOwnershipCredentialSubject,
ActorDelegationCredentialSubject,
IdentityCredentialSubject,
} from "@warpgogol/werkstatt-shared/passport";
import { readIdentityConfig, writeIdentityConfig, generateCredentialId } from "./identity-io.ts";
export interface IdentityCredentialIssueData {
credentialId: string;
type: "SiteOwnershipCredential" | "ActorDelegationCredential";
subjectId: string;
siteId: string;
}
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
export async function runIdentityCredentialIssue(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot } = context;
const type = flagString(input, "type");
const subjectId = flagString(input, "subject");
const siteId = flagString(input, "site");
const scopesRaw = flagString(input, "scopes");
const expiresAt = flagString(input, "expires-at");
if (!type) throw new Error("[identity.credential.issue] --type is required");
if (!subjectId) throw new Error("[identity.credential.issue] --subject is required");
if (!siteId) throw new Error("[identity.credential.issue] --site is required");
const privateKeyHex = process.env["WERKSTATT_OPERATOR_SIGNING_KEY"];
if (!privateKeyHex) {
throw new Error(
"[identity.credential.issue] WERKSTATT_OPERATOR_SIGNING_KEY env var is required",
);
}
const config = await readIdentityConfig(workspaceRoot);
const issuedAt = new Date().toISOString();
let subject: IdentityCredentialSubject;
let credentialType: WerkstattCredential["type"];
if (type === "SiteOwnershipCredential") {
subject = { id: subjectId, siteId, role: "owner" } satisfies SiteOwnershipCredentialSubject;
credentialType = "SiteOwnershipCredential";
} else if (type === "ActorDelegationCredential") {
if (!expiresAt) {
throw new Error(
"[identity.credential.issue] --expires-at is required for ActorDelegationCredential",
);
}
const scopes = scopesRaw ? scopesRaw.split(",").map((s) => s.trim()) : ["*"];
subject = {
id: subjectId,
siteId,
delegatedBy:
config.issuedCredentials[0]?.subject.id ?? `did:web:${config.domain}#operator-v1`,
expiresAt,
scopes,
} satisfies ActorDelegationCredentialSubject;
credentialType = "ActorDelegationCredential";
} else {
throw new Error(
`[identity.credential.issue] --type must be SiteOwnershipCredential or ActorDelegationCredential, got: ${type}`,
);
}
const verificationMethod = `did:web:${config.domain}#${config.operatorKeyPair.keyVersion}`;
const proof = await signIdentityCredential(subject, privateKeyHex, verificationMethod, issuedAt);
const credentialId = generateCredentialId();
const credential: WerkstattCredential = {
credentialId,
type: credentialType,
subject,
proof,
issuedAt,
issuer: `did:web:${config.domain}`,
};
config.issuedCredentials.push(credential);
await writeIdentityConfig(workspaceRoot, config);
return {
data: {
credentialId,
type: credentialType,
subjectId,
siteId,
},
exitCode: 0,
summary: `[identity.credential.issue] ${credentialType} issued for ${subjectId}`,
};
}