diff --git a/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs b/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs index 79bce6f..4d1d8cc 100644 --- a/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs +++ b/tests/Bifrost.Web.Tests/ProviderAdvertisementTests.cs @@ -63,9 +63,28 @@ public sealed class ProviderAdvertisementTests .Single(node => node.GetProperty("id").GetString() == "metric-bridge"); var bridgeLine = bridgeMetric.GetProperty("props").GetProperty("value").GetString(); - Assert.Contains("GitHub yes", bridgeLine, StringComparison.Ordinal); - Assert.Contains("Other yes", bridgeLine, StringComparison.Ordinal); - Assert.Contains("Patron yes", bridgeLine, StringComparison.Ordinal); + Assert.Contains("GitHub live", bridgeLine, StringComparison.Ordinal); + Assert.Contains("Discord prepared", bridgeLine, StringComparison.Ordinal); + Assert.Contains("Reddit prepared", bridgeLine, StringComparison.Ordinal); + Assert.Contains("Other live", bridgeLine, StringComparison.Ordinal); + Assert.Contains("Patron live", bridgeLine, StringComparison.Ordinal); + + Assert.Equal("warn", bridgeMetric.GetProperty("props").GetProperty("tone").GetString()); + + var readinessRows = root + .GetProperty("surface") + .GetProperty("root") + .GetProperty("children") + .EnumerateArray() + .SelectMany(panel => panel.GetProperty("children").EnumerateArray()) + .Single(node => node.GetProperty("id").GetString() == "list-bridge-readiness") + .GetProperty("children") + .EnumerateArray() + .Select(node => node.GetProperty("props").GetProperty("text").GetString()) + .ToArray(); + + Assert.Contains(readinessRows, row => row is not null && row.Contains("reddit: prepared", StringComparison.Ordinal)); + Assert.Contains(readinessRows, row => row is not null && row.Contains("patron: live", StringComparison.Ordinal) && row.Contains("Bifrost stores no provider tokens", StringComparison.Ordinal)); } private static void AssertEndpoint(JsonElement root, string id, string schemaId, string lowering) diff --git a/tools/provider-advertisement.mjs b/tools/provider-advertisement.mjs index 17df238..44ad37e 100644 --- a/tools/provider-advertisement.mjs +++ b/tools/provider-advertisement.mjs @@ -437,21 +437,7 @@ async function collectStats(options) { latestUpdatedAt: latestTimestamp(requests.map((item) => item.updatedAt)), }, witnesses, - bridge: { - discordPost: true, - discordDm: true, - redditPost: Boolean(process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN), - githubDraftPr: true, - githubPrComment: true, - githubWebhookSync: true, - otherRequest: true, - patronSupportIntake: true, - bridgeLedgerConfigured: Boolean(process.env.BIFROST_BRIDGE_BASE_URL && process.env.BIFROST_BRIDGE_TOKEN), - credentialSource: process.env.BIFROST_DISCORD_BOT_TOKEN ? "BIFROST_DISCORD_BOT_TOKEN" : process.env.DISCORD_BOT_TOKEN ? "DISCORD_BOT_TOKEN-fallback" : "missing", - redditCredentialSource: process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN ? "BIFROST_REDDIT_CLIENT_ID+BIFROST_REDDIT_REFRESH_TOKEN" : "missing", - bridgeLedgerCredentialSource: process.env.BIFROST_BRIDGE_BASE_URL && process.env.BIFROST_BRIDGE_TOKEN ? "BIFROST_BRIDGE_BASE_URL+BIFROST_BRIDGE_TOKEN" : "missing", - patronSupportAuthority: "Heimdall HMAC via /heimdall/patron-support/events", - }, + bridge: buildBridgeStats(), summary: { status: health.ok && ready.ok ? "ready" : "degraded", health: health.value ?? health.error, @@ -568,8 +554,96 @@ function witnessStat(relativePath) { }; } +function buildBridgeStats() { + const discordCredentialSource = process.env.BIFROST_DISCORD_BOT_TOKEN + ? "BIFROST_DISCORD_BOT_TOKEN" + : process.env.DISCORD_BOT_TOKEN + ? "DISCORD_BOT_TOKEN-fallback" + : "missing"; + const redditCredentialSource = process.env.BIFROST_REDDIT_CLIENT_ID && process.env.BIFROST_REDDIT_REFRESH_TOKEN + ? "BIFROST_REDDIT_CLIENT_ID+BIFROST_REDDIT_REFRESH_TOKEN" + : "missing"; + const bridgeLedgerCredentialSource = process.env.BIFROST_BRIDGE_BASE_URL && process.env.BIFROST_BRIDGE_TOKEN + ? "BIFROST_BRIDGE_BASE_URL+BIFROST_BRIDGE_TOKEN" + : "missing"; + + const surfaces = [ + { + id: "github", + label: "GitHub", + prepared: true, + ready: true, + authority: "Bifrost bridge gate plus GitHub webhook sync", + credentialSource: "GitHub app/OAuth/gh runtime", + note: "draft PRs, PR comments, and webhook work sync are hooked", + }, + { + id: "discord", + label: "Discord", + prepared: true, + ready: discordCredentialSource !== "missing", + authority: "Bifrost bridge gate plus Heimdall-linked account/capability reference", + credentialSource: discordCredentialSource, + note: discordCredentialSource === "missing" + ? "transport actuator token is not visible to this process" + : "transport actuator credential is visible", + }, + { + id: "reddit", + label: "Reddit", + prepared: true, + ready: redditCredentialSource !== "missing", + authority: "Bifrost bridge gate plus Heimdall-linked reddit capability reference", + credentialSource: redditCredentialSource, + note: redditCredentialSource === "missing" + ? "reddit transport credentials are not visible to this process" + : "reddit transport credentials are visible", + }, + { + id: "other", + label: "Other", + prepared: true, + ready: true, + authority: "Bifrost receipt-only future-surface gate with named Heimdall reference", + credentialSource: "not-required-for-receipt-only", + note: "records governed future-surface requests without provider transport", + }, + { + id: "patron", + label: "Patron", + prepared: true, + ready: true, + authority: "Heimdall HMAC via /heimdall/patron-support/events", + credentialSource: "Heimdall:PatronSupportIntakeSecret", + note: "consumes Heimdall-signed Patreon/PayPal support facts; Bifrost stores no provider tokens", + }, + ]; + + return { + discordPost: true, + discordDm: true, + redditPost: redditCredentialSource !== "missing", + githubDraftPr: true, + githubPrComment: true, + githubWebhookSync: true, + otherRequest: true, + patronSupportIntake: true, + bridgeLedgerConfigured: bridgeLedgerCredentialSource !== "missing", + credentialSource: discordCredentialSource, + redditCredentialSource, + bridgeLedgerCredentialSource, + patronSupportAuthority: "Heimdall HMAC via /heimdall/patron-support/events", + prepared: surfaces.every((surface) => surface.prepared), + ready: surfaces.every((surface) => surface.ready), + surfaces, + }; +} + function buildOperatorSurface(stats) { const statusTone = stats.summary.status === "ready" ? "ok" : "warn"; + const bridgeLine = stats.bridge.surfaces + .map((surface) => `${surface.label} ${surface.ready ? "live" : surface.prepared ? "prepared" : "no"}`) + .join(" / "); const children = [ panelNode("service", "Service", [ metricNode("status", "Status", stats.summary.status, statusTone), @@ -579,8 +653,15 @@ function buildOperatorSurface(stats) { metricNode( "bridge", "Bridge", - `GitHub ${yesNo(stats.bridge.githubDraftPr && stats.bridge.githubPrComment)} / Discord ${yesNo(stats.bridge.discordPost)} / Reddit ${yesNo(stats.bridge.redditPost)} / Other ${yesNo(stats.bridge.otherRequest)} / Patron ${yesNo(stats.bridge.patronSupportIntake)}`, - stats.bridge.githubDraftPr && stats.bridge.githubPrComment && stats.bridge.otherRequest && stats.bridge.patronSupportIntake ? "ok" : "warn", + bridgeLine, + stats.bridge.ready ? "ok" : "warn", + ), + listNode( + "bridge-readiness", + "Bridge Readiness", + stats.bridge.surfaces.map((surface) => + `${surface.id}: ${surface.ready ? "live" : surface.prepared ? "prepared" : "missing"}; authority=${surface.authority}; credential=${surface.credentialSource}; note=${surface.note}`, + ), ), ]), panelNode("activity", "Activity", [