94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
// Responsibility: expose the YAML-owned sentinel internal publish capability without claiming provenance.
|
|
import { rootPath } from "./config";
|
|
import type { CicdDeliveryAuthority } from "./cicd-delivery-authority";
|
|
import type { WebProbeSentinelOptions } from "./hwlab-node-web-sentinel-cicd-shared";
|
|
import { readYamlRecord } from "./platform-infra-ops-library";
|
|
import { materializeYamlComposition } from "./yaml-composition";
|
|
|
|
const configPath = "config/platform-infra/pipelines-as-code.yaml";
|
|
const configRef = `${configPath}#capabilities.sentinelInternalPublish`;
|
|
|
|
export interface SentinelPacInternalPublishCapability {
|
|
readonly enabled: boolean;
|
|
readonly admissionProvenance: "unavailable";
|
|
readonly trackingIssue: string;
|
|
readonly configRef: string;
|
|
readonly valuesPrinted: false;
|
|
}
|
|
|
|
export type SentinelPacInternalPublishAuthorization = {
|
|
readonly allowed: false;
|
|
readonly mode: "sentinel-pac-internal-publish-blocked";
|
|
readonly reason:
|
|
| "operator-entrypoint"
|
|
| "delivery-authority-not-pac"
|
|
| "capability-disabled"
|
|
| "admission-provenance-unavailable";
|
|
readonly capability: SentinelPacInternalPublishCapability;
|
|
readonly valuesPrinted: false;
|
|
};
|
|
|
|
export function readSentinelPacInternalPublishCapability(): SentinelPacInternalPublishCapability {
|
|
const raw = readYamlRecord<Record<string, unknown>>(rootPath(...configPath.split("/")), "platform-infra-pipelines-as-code");
|
|
const composed = materializeYamlComposition(raw, { label: configPath }).value;
|
|
const capabilities = record(composed.capabilities, `${configPath}#capabilities`);
|
|
const capability = record(capabilities.sentinelInternalPublish, configRef);
|
|
const enabled = booleanField(capability.enabled, `${configRef}.enabled`);
|
|
const admissionProvenance = stringField(capability.admissionProvenance, `${configRef}.admissionProvenance`);
|
|
if (admissionProvenance !== "unavailable") {
|
|
throw new Error(`${configRef}.admissionProvenance must remain unavailable until the admission contract is implemented`);
|
|
}
|
|
const trackingIssue = stringField(capability.trackingIssue, `${configRef}.trackingIssue`);
|
|
if (!/^https:\/\/github\.com\/pikasTech\/unidesk\/issues\/\d+$/u.test(trackingIssue)) {
|
|
throw new Error(`${configRef}.trackingIssue must reference the owning GitHub issue`);
|
|
}
|
|
return {
|
|
enabled,
|
|
admissionProvenance,
|
|
trackingIssue,
|
|
configRef,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function authorizeSentinelPacInternalPublish(
|
|
options: WebProbeSentinelOptions,
|
|
authority: CicdDeliveryAuthority,
|
|
capability = readSentinelPacInternalPublishCapability(),
|
|
): SentinelPacInternalPublishAuthorization {
|
|
if (options.kind !== "publish" || options.internalExecution !== "pac-controller") {
|
|
return denied("operator-entrypoint", capability);
|
|
}
|
|
if (authority.kind !== "pac-pr-merge") return denied("delivery-authority-not-pac", capability);
|
|
if (!capability.enabled) return denied("capability-disabled", capability);
|
|
return denied("admission-provenance-unavailable", capability);
|
|
}
|
|
|
|
function denied(
|
|
reason: SentinelPacInternalPublishAuthorization["reason"],
|
|
capability: SentinelPacInternalPublishCapability,
|
|
): SentinelPacInternalPublishAuthorization {
|
|
return {
|
|
allowed: false,
|
|
mode: "sentinel-pac-internal-publish-blocked",
|
|
reason,
|
|
capability,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function record(value: unknown, path: string): Record<string, unknown> {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function booleanField(value: unknown, path: string): boolean {
|
|
if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`);
|
|
return value;
|
|
}
|
|
|
|
function stringField(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} must be a non-empty string`);
|
|
return value.trim();
|
|
}
|