diff --git a/tests/Bifrost.Web.Tests/BridgeCliTests.cs b/tests/Bifrost.Web.Tests/BridgeCliTests.cs index 1464fd5..a8efe6d 100644 --- a/tests/Bifrost.Web.Tests/BridgeCliTests.cs +++ b/tests/Bifrost.Web.Tests/BridgeCliTests.cs @@ -6,6 +6,145 @@ namespace Bifrost.Web.Tests; public sealed class BridgeCliTests { + [Fact] + public async Task Authorize_release_requires_typed_command_provenance() + { + var result = await RunNodeAsync([ + "tools/bifrost-bridge.mjs", "authorize-release", + "--repository", "GameCult/Epiphany", + "--ref", "refs/heads/main", + "--commit-sha", new string('a', 40), + "--policy-decision-id", "policy-1", + ]); + + Assert.NotEqual(0, result.ExitCode); + Assert.Contains("BIFROST_CULTMESH_COMMAND_ID", result.Stderr, StringComparison.Ordinal); + } + + [Fact] + public async Task Authorize_release_rejects_remote_ref_mismatch() + { + var fakeGit = await WriteFakeGitAsync(new string('b', 40), "refs/heads/main"); + var result = await RunNodeAsync([ + "tools/bifrost-bridge.mjs", "authorize-release", + "--repository", "GameCult/Epiphany", + "--ref", "refs/heads/main", + "--commit-sha", new string('a', 40), + "--policy-decision-id", "policy-1", + "--authority-ref", "release-policy", + "--identity", "bifrost", + "--source-kind", "epiphany-release", + "--source-id", "release-1", + "--epiphany-run-id", "run-1", + "--epiphany-lane-id", "soul", + "--epiphany-agent-identity", "epiphany", + "--git-executable", "node", + "--git-exec-args", fakeGit, + ], TypedCommandEnv()); + + Assert.NotEqual(0, result.ExitCode); + Assert.Contains("does not exactly match", result.Stderr, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Authorize_then_revoke_release_preserves_decision_and_receipt_provenance() + { + var sha = new string('a', 40); + var fakeGit = await WriteFakeGitAsync(sha, "refs/heads/main"); + var store = Path.Combine(Path.GetTempPath(), $"bifrost-release-authority-{Guid.NewGuid():N}.cc"); + var receiptStore = Path.Combine(Path.GetTempPath(), $"bifrost-release-receipts-{Guid.NewGuid():N}.cc"); + var authorized = await RunNodeAsync([ + "tools/bifrost-bridge.mjs", "authorize-release", + "--repository", "GameCult/Epiphany", + "--ref", "refs/heads/main", + "--commit-sha", sha, + "--policy-decision-id", "policy-epiphany-main", + "--authority-ref", "bifrost-release-policy", + "--identity", "bifrost", + "--source-kind", "epiphany-release", + "--source-id", "epiphany-main-release", + "--epiphany-run-id", "run-epiphany-main", + "--epiphany-lane-id", "soul-release", + "--epiphany-agent-identity", "epiphany", + "--release-authority-store", store, + "--receipt-store", receiptStore, + "--git-executable", "node", + "--git-exec-args", fakeGit, + ], TypedCommandEnv()); + + Assert.True(authorized.ExitCode == 0, $"stdout:{authorized.Stdout}\nstderr:{authorized.Stderr}"); + using (var payload = JsonDocument.Parse(authorized.Stdout)) + { + var authority = payload.RootElement.GetProperty("authority"); + Assert.Equal("bifrost.repository_release_authority.v1", authority.GetProperty("schemaVersion").GetString()); + Assert.Equal($"release:GameCult/Epiphany:refs/heads/main:{sha}", authority.GetProperty("authorityId").GetString()); + Assert.Equal("GameCult/Epiphany", authority.GetProperty("repositoryFullName").GetString()); + Assert.Equal("authorize", authority.GetProperty("decision").GetString()); + Assert.Equal("authorized", authority.GetProperty("status").GetString()); + Assert.Equal("policy-epiphany-main", authority.GetProperty("policyDecisionId").GetString()); + Assert.Equal("crossing_test-cultmesh-command", authority.GetProperty("crossingReceiptId").GetString()); + Assert.Equal($"https://github.com/GameCult/Epiphany/commit/{sha}", authority.GetProperty("externalReceiptUrl").GetString()); + } + + using (var receipts = await ReadCrossingReceiptsAsync(receiptStore)) + { + var receipt = Assert.Single(receipts.RootElement.EnumerateArray()); + Assert.Equal("completed", receipt.GetProperty("status").GetString()); + Assert.True(receipt.GetProperty("ok").GetBoolean()); + Assert.Equal("github.repository_release_verification", receipt.GetProperty("crossingKind").GetString()); + Assert.Equal(sha, receipt.GetProperty("externalReceipt").GetProperty("id").GetString()); + Assert.Equal("refs/heads/main", receipt.GetProperty("externalReceipt").GetProperty("upstreamRef").GetString()); + } + + var competingSha = new string('b', 40); + var competingGit = await WriteFakeGitAsync(competingSha, "refs/heads/main"); + var competing = await RunNodeAsync([ + "tools/bifrost-bridge.mjs", "authorize-release", + "--repository", "GameCult/Epiphany", + "--ref", "refs/heads/main", + "--commit-sha", competingSha, + "--policy-decision-id", "policy-epiphany-main-2", + "--authority-ref", "bifrost-release-policy", + "--identity", "bifrost", + "--source-kind", "epiphany-release", + "--source-id", "epiphany-main-release-2", + "--epiphany-run-id", "run-epiphany-main-2", + "--epiphany-lane-id", "soul-release", + "--epiphany-agent-identity", "epiphany", + "--release-authority-store", store, + "--receipt-store", receiptStore, + "--git-executable", "node", + "--git-exec-args", competingGit, + ], TypedCommandEnv()); + + Assert.NotEqual(0, competing.ExitCode); + Assert.Contains("revoke it before authorizing another commit", competing.Stderr, StringComparison.OrdinalIgnoreCase); + + var revoked = await RunNodeAsync([ + "tools/bifrost-bridge.mjs", "revoke-release", + "--repository", "GameCult/Epiphany", + "--ref", "refs/heads/main", + "--commit-sha", sha, + "--reason", "release withdrawn", + "--release-authority-store", store, + ], TypedCommandEnv()); + + Assert.True(revoked.ExitCode == 0, $"stdout:{revoked.Stdout}\nstderr:{revoked.Stderr}"); + using var revokedPayload = JsonDocument.Parse(revoked.Stdout); + var revokedAuthority = revokedPayload.RootElement.GetProperty("authority"); + Assert.Equal("revoked", revokedAuthority.GetProperty("status").GetString()); + Assert.Equal("release withdrawn", revokedAuthority.GetProperty("revocationReason").GetString()); + Assert.Equal("policy-epiphany-main", revokedAuthority.GetProperty("policyDecisionId").GetString()); + Assert.Equal("crossing_test-cultmesh-command", revokedAuthority.GetProperty("crossingReceiptId").GetString()); + } + + private static async Task WriteFakeGitAsync(string sha, string gitRef) + { + var path = Path.Combine(Path.GetTempPath(), $"bifrost-fake-git-{Guid.NewGuid():N}.mjs"); + await File.WriteAllTextAsync(path, $"process.stdout.write('{sha}\\t{gitRef}\\n');\n", Encoding.UTF8); + return path; + } + [Fact] public async Task GitHub_bridge_command_fails_closed_without_bifrost_gate() { diff --git a/tools/bifrost-bridge.mjs b/tools/bifrost-bridge.mjs index 0385070..1e872e6 100644 --- a/tools/bifrost-bridge.mjs +++ b/tools/bifrost-bridge.mjs @@ -8,6 +8,15 @@ import { beginCrossingReceipt, crossingProvenanceFromOptions, } from "./bifrost-crossing-documents.mjs"; +import { + buildAuthorizedRelease, + canonicalCommitSha, + canonicalRef, + canonicalRepository, + openRepositoryReleaseAuthorityStore, + releaseAuthorityId, + revokeAuthorizedRelease, +} from "./bifrost-repository-release-authority.mjs"; const PERSONA_WEBHOOK_NAME = "Bifrost Persona Pipe"; const PERSONA_WEBHOOK_CACHE_PATH = resolve(".bifrost/discord-webhook-cache.json"); @@ -45,11 +54,101 @@ async function main() { case "other-request": await requestOtherBridgeAction(options); return; + case "authorize-release": + await authorizeRepositoryRelease(options); + return; + case "revoke-release": + await revokeRepositoryRelease(options); + return; default: throw new Error(`Unknown command "${command}". Run "node tools/bifrost-bridge.mjs help".`); } } +async function authorizeRepositoryRelease(options) { + ensureGitHubBridgeGate(options); + const commandId = optionalString(options["cultmesh-command-id"]) ?? optionalString(process.env.BIFROST_CULTMESH_COMMAND_ID); + if (!commandId) throw new Error("authorize-release requires BIFROST_CULTMESH_COMMAND_ID or --cultmesh-command-id."); + const repository = canonicalRepository(requireOption(options, "repository")); + const ref = canonicalRef(requireOption(options, "ref")); + const commitSha = canonicalCommitSha(requireOption(options, "commit-sha")); + const policyDecisionId = requireOption(options, "policy-decision-id"); + const authorityReference = requireOption(options, "authority-ref"); + const actorIdentity = requireOption(options, "identity"); + const sourceKind = requireOption(options, "source-kind"); + const sourceId = requireOption(options, "source-id"); + const epiphanyRunId = requireOption(options, "epiphany-run-id"); + const epiphanyLaneId = requireOption(options, "epiphany-lane-id"); + const epiphanyAgentIdentity = requireOption(options, "epiphany-agent-identity"); + const remoteUrl = options["remote-url"] ?? `https://github.com/${repository}.git`; + const gitCommand = optionalString(options["git-executable"]) ?? resolveGitCommand(); + const gitPrefixArgs = optionalString(options["git-exec-args"])?.split("\u001f").filter(Boolean) ?? []; + const remote = run(gitCommand, [...gitPrefixArgs, "ls-remote", "--refs", remoteUrl, ref], repoRoot).stdout.trim().split(/\r?\n/).filter(Boolean); + if (remote.length !== 1) throw new Error(`Remote ref ${repository}:${ref} did not resolve exactly once.`); + const [remoteSha, remoteRef] = remote[0].trim().split(/\s+/); + if (canonicalCommitSha(remoteSha) !== commitSha || remoteRef !== ref) { + throw new Error(`Remote ref ${repository}:${ref} does not exactly match requested commit ${commitSha}.`); + } + const storePath = resolve(options["release-authority-store"] ?? process.env.BIFROST_REPOSITORY_RELEASE_AUTHORITY_STORE ?? ".bifrost/repository-release-authority.cc"); + const store = await openRepositoryReleaseAuthorityStore(storePath); + const authorityId = releaseAuthorityId(repository, ref, commitSha); + const existing = store.get(authorityId); + if (existing) { + if (existing.commandId === commandId && existing.status === "authorized") { + printJson({ ok: true, action: "authorize-release", authority: existing, idempotent: true }); + return; + } + throw new Error(`Release authority ${authorityId} already exists; immutable authority records cannot be replaced.`); + } + const competing = store.getAll().find((authority) => + authority.repositoryFullName === repository && + authority.upstreamRef === ref && + authority.status === "authorized" + ); + if (competing) { + throw new Error( + `Release authority ${competing.authorityId} already authorizes ${repository}:${ref}; revoke it before authorizing another commit.`, + ); + } + const externalReceiptUrl = `https://github.com/${repository}/commit/${commitSha}`; + const crossing = await beginCrossingReceipt({ ...options, "receipt-id": undefined }, { + commandId, + crossingKind: "github.repository_release_verification", + actor: { kind: "service", name: actorIdentity, bifrostIdentity: actorIdentity }, + source: { kind: sourceKind, id: sourceId }, + authority: { authorityRef: authorityReference, policyDecisionId }, + epiphany: { runId: epiphanyRunId, laneId: epiphanyLaneId, agentIdentity: epiphanyAgentIdentity }, + target: { surface: "GitHub", repositoryFullName: repository, upstreamRef: ref, commitSha }, + }); + await crossing.start(); + await crossing.complete({ externalReceipt: { url: externalReceiptUrl, id: commitSha, repositoryFullName: repository, upstreamRef: ref, commitSha } }); + const authority = await store.put(buildAuthorizedRelease({ + repositoryFullName: repository, upstreamRef: ref, commitSha, commandId, + crossingReceiptId: crossing.receiptId, policyDecisionId, authorityReference, + actorIdentity, sourceKind, sourceId, epiphanyRunId, epiphanyLaneId, + epiphanyAgentIdentity, externalReceiptUrl, externalReceiptId: commitSha, + authorizedAt: new Date().toISOString(), expiresAt: optionalString(options["expires-at"]) ?? "", + })); + printJson({ ok: true, action: "authorize-release", authority }); +} + +async function revokeRepositoryRelease(options) { + ensureGitHubBridgeGate(options); + const commandId = optionalString(options["cultmesh-command-id"]) ?? optionalString(process.env.BIFROST_CULTMESH_COMMAND_ID); + if (!commandId) throw new Error("revoke-release requires BIFROST_CULTMESH_COMMAND_ID or --cultmesh-command-id."); + const repository = canonicalRepository(requireOption(options, "repository")); + const ref = canonicalRef(requireOption(options, "ref")); + const commitSha = canonicalCommitSha(requireOption(options, "commit-sha")); + const reason = requireOption(options, "reason"); + const storePath = resolve(options["release-authority-store"] ?? process.env.BIFROST_REPOSITORY_RELEASE_AUTHORITY_STORE ?? ".bifrost/repository-release-authority.cc"); + const store = await openRepositoryReleaseAuthorityStore(storePath); + const authorityId = releaseAuthorityId(repository, ref, commitSha); + const current = store.get(authorityId); + if (!current) throw new Error(`Release authority ${authorityId} does not exist.`); + const authority = await store.put(revokeAuthorizedRelease(current, { commandId, reason, revokedAt: new Date().toISOString() })); + printJson({ ok: true, action: "revoke-release", authority }); +} + function loadBifrostLocalEnv() { if (process.env.BIFROST_SKIP_LOCAL_ENV === "true") { return; @@ -1360,6 +1459,8 @@ Commands: discord-dm Send a Discord DM through Bifrost's bridge-owned bot token reddit-post Create a self-post in r/GameCultOrg through the Bifrost Reddit app other-request Record a Bifrost-gated request for a future outside-world surface + authorize-release Authorize one exact remote GitHub ref and commit for downstream deployment + revoke-release Revoke the current authorization for one GitHub repository ref Examples: node tools/bifrost-bridge.mjs github-draft-pr --repo-root E:/Projects/AetheriaLore --identity nibu --title "Nibu: Glitchcraft" --path Aetheria/Articles/Nibu/glitchcraft.md --content-file article.md diff --git a/tools/bifrost-repository-release-authority.mjs b/tools/bifrost-repository-release-authority.mjs new file mode 100644 index 0000000..f1bf4a2 --- /dev/null +++ b/tools/bifrost-repository-release-authority.mjs @@ -0,0 +1,169 @@ +import { createRequire } from "node:module"; +import { existsSync } from "node:fs"; +import { mkdir } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; + +const repoRoot = resolve(import.meta.dirname, ".."); +const projectsRoot = resolve(repoRoot, ".."); +const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib")); + +export const repositoryReleaseAuthorityDocumentType = "bifrost.repository_release_authority"; +export const repositoryReleaseAuthoritySchemaId = "bifrost.repository_release_authority.v1"; +export const defaultRepositoryReleaseAuthorityStorePath = resolve(repoRoot, ".bifrost", "repository-release-authority.cc"); + +let cachedRuntime; +let cachedDefinition; + +export function repositoryReleaseAuthorityDefinition(defineDocumentTypeInput) { + const defineDocumentType = defineDocumentTypeInput ?? loadCultCacheRuntime().defineDocumentType; + cachedDefinition ??= defineDocumentType({ + type: repositoryReleaseAuthorityDocumentType, + schemaId: repositoryReleaseAuthoritySchemaId, + schemaName: repositoryReleaseAuthorityDocumentType, + schemaVersion: repositoryReleaseAuthoritySchemaId, + contentHash: repositoryReleaseAuthoritySchemaId, + global: false, + name: "authorityId", + indexes: { repositoryFullName: "repositoryFullName", upstreamRef: "upstreamRef", commitSha: "commitSha", status: "status" }, + schema: { parse: parseRepositoryReleaseAuthority }, + members: [ + { slot: 1, memberName: "authorityId", typeName: "string", isName: true }, + { slot: 2, memberName: "commandId", typeName: "string" }, + { slot: 3, memberName: "crossingReceiptId", typeName: "string" }, + { slot: 4, memberName: "repositoryFullName", typeName: "string", indexAlias: "repositoryFullName" }, + { slot: 5, memberName: "upstreamRef", typeName: "string", indexAlias: "upstreamRef" }, + { slot: 6, memberName: "commitSha", typeName: "string", indexAlias: "commitSha" }, + { slot: 7, memberName: "decision", typeName: "string" }, + { slot: 8, memberName: "status", typeName: "string", indexAlias: "status" }, + { slot: 9, memberName: "policyDecisionId", typeName: "string" }, + { slot: 10, memberName: "authorityReference", typeName: "string" }, + { slot: 11, memberName: "actorIdentity", typeName: "string" }, + { slot: 12, memberName: "sourceKind", typeName: "string" }, + { slot: 13, memberName: "sourceId", typeName: "string" }, + { slot: 14, memberName: "epiphanyRunId", typeName: "string" }, + { slot: 15, memberName: "epiphanyLaneId", typeName: "string" }, + { slot: 16, memberName: "epiphanyAgentIdentity", typeName: "string" }, + { slot: 17, memberName: "externalReceiptUrl", typeName: "string" }, + { slot: 18, memberName: "externalReceiptId", typeName: "string" }, + { slot: 19, memberName: "authorizedAt", typeName: "string" }, + { slot: 20, memberName: "expiresAt", typeName: "string" }, + { slot: 21, memberName: "revokedAt", typeName: "string" }, + { slot: 22, memberName: "revocationReason", typeName: "string" }, + ], + }); + return cachedDefinition; +} + +export async function openRepositoryReleaseAuthorityStore(storePath = defaultRepositoryReleaseAuthorityStorePath) { + const { CultCache, SingleFileMessagePackBackingStore } = loadCultCacheRuntime(); + await mkdir(dirname(storePath), { recursive: true }); + const cache = CultCache.builder().withDocumentType(repositoryReleaseAuthorityDefinition()).withGenericStore(new SingleFileMessagePackBackingStore(storePath)).build(); + await cache.pullAllBackingStores(); + return { + storePath, + async put(document) { + const parsed = parseRepositoryReleaseAuthority(document); + await cache.put(repositoryReleaseAuthorityDefinition(), parsed.authorityId, parsed); + return parsed; + }, + get(authorityId) { return cache.get(repositoryReleaseAuthorityDefinition(), authorityId); }, + getAll() { return cache.getAll(repositoryReleaseAuthorityDefinition()); }, + }; +} + +export function releaseAuthorityId(repositoryFullName, upstreamRef, commitSha) { + return `release:${canonicalRepository(repositoryFullName)}:${canonicalRef(upstreamRef)}:${canonicalCommitSha(commitSha)}`; +} + +export function buildAuthorizedRelease(input) { + const repositoryFullName = canonicalRepository(input.repositoryFullName); + const upstreamRef = canonicalRef(input.upstreamRef); + const commitSha = canonicalCommitSha(input.commitSha); + return parseRepositoryReleaseAuthority({ + schemaName: repositoryReleaseAuthorityDocumentType, + schemaVersion: repositoryReleaseAuthoritySchemaId, + authorityId: releaseAuthorityId(repositoryFullName, upstreamRef, commitSha), + commandId: input.commandId, + crossingReceiptId: input.crossingReceiptId, + repositoryFullName, + upstreamRef, + commitSha, + decision: "authorize", + status: "authorized", + policyDecisionId: input.policyDecisionId, + authorityReference: input.authorityReference, + actorIdentity: input.actorIdentity, + sourceKind: input.sourceKind, + sourceId: input.sourceId, + epiphanyRunId: input.epiphanyRunId, + epiphanyLaneId: input.epiphanyLaneId, + epiphanyAgentIdentity: input.epiphanyAgentIdentity, + externalReceiptUrl: input.externalReceiptUrl, + externalReceiptId: input.externalReceiptId, + authorizedAt: input.authorizedAt, + expiresAt: input.expiresAt ?? "", + revokedAt: "", + revocationReason: "", + }); +} + +export function revokeAuthorizedRelease(current, input) { + const parsed = parseRepositoryReleaseAuthority(current); + if (parsed.status !== "authorized") throw new Error(`Release authority ${parsed.authorityId} is already revoked.`); + return parseRepositoryReleaseAuthority({ ...parsed, status: "revoked", revokedAt: requiredString(input.revokedAt, "revokedAt"), revocationReason: requiredString(input.reason, "revocationReason") }); +} + +export function canonicalRepository(value) { + const repository = requiredString(value, "repositoryFullName").replace(/^https?:\/\/github\.com\//i, "").replace(/\.git$/i, "").replace(/^\/+|\/+$/g, ""); + if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) throw new Error("repositoryFullName must be a canonical GitHub owner/name."); + return repository; +} + +export function canonicalRef(value) { + const ref = requiredString(value, "upstreamRef"); + if (!/^refs\/(heads|tags)\/[A-Za-z0-9._\/-]+$/.test(ref) || ref.includes("..") || ref.endsWith("/")) throw new Error("upstreamRef must be a canonical refs/heads/... or refs/tags/... name."); + return ref; +} + +export function canonicalCommitSha(value) { + const sha = requiredString(value, "commitSha").toLowerCase(); + if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error("commitSha must be an exact 40-character hexadecimal Git commit SHA."); + return sha; +} + +function parseRepositoryReleaseAuthority(input) { + if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Bifrost repository release authority must be an object."); + const repositoryFullName = canonicalRepository(input.repositoryFullName); + const upstreamRef = canonicalRef(input.upstreamRef); + const commitSha = canonicalCommitSha(input.commitSha); + const document = { + schemaName: repositoryReleaseAuthorityDocumentType, + schemaVersion: repositoryReleaseAuthoritySchemaId, + authorityId: requiredString(input.authorityId, "authorityId"), commandId: requiredString(input.commandId, "commandId"), + crossingReceiptId: requiredString(input.crossingReceiptId, "crossingReceiptId"), repositoryFullName, upstreamRef, commitSha, + decision: requiredString(input.decision, "decision"), status: requiredString(input.status, "status"), policyDecisionId: requiredString(input.policyDecisionId, "policyDecisionId"), + authorityReference: requiredString(input.authorityReference, "authorityReference"), actorIdentity: requiredString(input.actorIdentity, "actorIdentity"), + sourceKind: requiredString(input.sourceKind, "sourceKind"), sourceId: requiredString(input.sourceId, "sourceId"), + epiphanyRunId: requiredString(input.epiphanyRunId, "epiphanyRunId"), epiphanyLaneId: requiredString(input.epiphanyLaneId, "epiphanyLaneId"), epiphanyAgentIdentity: requiredString(input.epiphanyAgentIdentity, "epiphanyAgentIdentity"), + externalReceiptUrl: requiredString(input.externalReceiptUrl, "externalReceiptUrl"), externalReceiptId: requiredString(input.externalReceiptId, "externalReceiptId"), + authorizedAt: requiredString(input.authorizedAt, "authorizedAt"), expiresAt: optionalString(input.expiresAt) ?? "", revokedAt: optionalString(input.revokedAt) ?? "", revocationReason: optionalString(input.revocationReason) ?? "", + }; + if (document.authorityId !== releaseAuthorityId(repositoryFullName, upstreamRef, commitSha)) throw new Error("authorityId must be derived from repositoryFullName, upstreamRef, and commitSha."); + if (document.decision !== "authorize") throw new Error("Repository release authority decision must be authorize."); + if (!new Set(["authorized", "revoked"]).has(document.status)) throw new Error(`Repository release authority status "${document.status}" is not valid.`); + if (document.status === "authorized" && (document.revokedAt || document.revocationReason)) throw new Error("Authorized release authority cannot carry revocation state."); + if (document.status === "revoked" && (!document.revokedAt || !document.revocationReason)) throw new Error("Revoked release authority requires revocation time and reason."); + return document; +} + +function loadCultCacheRuntime() { + if (cachedRuntime) return cachedRuntime; + const candidates = [resolve(cultLibRoot, "packages", "cultcache-ts", "dist", "index.js"), resolve(projectsRoot, "CultCacheTS", "dist", "index.js")]; + const entryPoint = candidates.find(existsSync); + if (!entryPoint) throw new Error(`CultCache TypeScript runtime is unavailable. Tried: ${candidates.join(", ")}`); + cachedRuntime = createRequire(entryPoint)(entryPoint); + return cachedRuntime; +} + +function requiredString(value, field) { const result = optionalString(value); if (!result) throw new Error(`Bifrost repository release authority field "${field}" must be a non-empty string.`); return result; } +function optionalString(value) { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed || undefined; }