From 2e069dd1941b9526367961678fca7df234b8994f Mon Sep 17 00:00:00 2001 From: MagicCodingMan Date: Fri, 24 Jul 2026 12:37:18 -0400 Subject: [PATCH 01/98] This is currently very broken. --- Publish.CLI/Publish.CLI.csproj | 2 +- Test.TruthGate/Test.TruthGate.csproj | 22 +++++++----- TruthGate-Web/TruthGate-Web.Client/Cache.cs | 34 ++++++++++--------- .../TruthGate-Web.Client.csproj | 6 ++-- .../Components/Pages/Pinned.razor | 4 +-- .../Components/Pages/PinnedIpns.razor | 6 ++-- .../Components/Pages/Settings/ApiKeys.razor | 4 +-- .../Components/Pages/Settings/Domains.razor | 12 +++---- .../Pages/Settings/IpBansPage.razor | 4 +-- .../Pages/Settings/UserSettings.razor | 6 ++-- .../TruthGate-Web/TruthGate-Web.csproj | 18 +++++----- 11 files changed, 63 insertions(+), 55 deletions(-) diff --git a/Publish.CLI/Publish.CLI.csproj b/Publish.CLI/Publish.CLI.csproj index fd4bd08..ed9781c 100644 --- a/Publish.CLI/Publish.CLI.csproj +++ b/Publish.CLI/Publish.CLI.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable diff --git a/Test.TruthGate/Test.TruthGate.csproj b/Test.TruthGate/Test.TruthGate.csproj index 63bf931..a396ab3 100644 --- a/Test.TruthGate/Test.TruthGate.csproj +++ b/Test.TruthGate/Test.TruthGate.csproj @@ -1,23 +1,29 @@  - net9.0 + net10.0 enable enable false - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/TruthGate-Web/TruthGate-Web.Client/Cache.cs b/TruthGate-Web/TruthGate-Web.Client/Cache.cs index 0cedb0f..9ab4425 100644 --- a/TruthGate-Web/TruthGate-Web.Client/Cache.cs +++ b/TruthGate-Web/TruthGate-Web.Client/Cache.cs @@ -6,10 +6,10 @@ public static class Cache { public static MudTheme CustomTheme = new() { - PaletteLight = new() + PaletteLight = new PaletteLight { - Primary = "#2D2A5A", // Deep Indigo - Secondary = "#00E3C0", // Electric Mint + Primary = "#2D2A5A", + Secondary = "#00E3C0", Black = "#110e2d", AppbarText = "#424242", AppbarBackground = "rgba(255,255,255,0.8)", @@ -17,22 +17,24 @@ public static class Cache GrayLight = "#e8e8e8", GrayLighter = "#f9f9f9", }, - PaletteDark = new() + + PaletteDark = new PaletteDark { - Primary = "#00E3C0", // Electric Mint now the star + Primary = "#00E3C0", PrimaryContrastText = "#000000", - Secondary = "#3C3970", // Softer indigo accent (less oppressive than deep indigo) - Background = "#1E1F22", // Discord-ish near-black with a whisper of warmth - Surface = "#2B2D31", // Elevated panels (matches Discord card tone) - DrawerBackground = "#232428", // Sidebar tone - AppbarBackground = "#1B1C1F", // Slightly darker than background for separation - AppbarText = "#E0E0E0", // Soft but high-contrast - TextPrimary = "#FFFFFF", // True white for legibility - TextSecondary = "#A3A6AA", // Muted gray - ActionDefault = "#00E3C0", // Same as primary for cohesion - Divider = "#383A40" // Subtle panel separators + Secondary = "#3C3970", + Background = "#1E1F22", + Surface = "#2B2D31", + DrawerBackground = "#232428", + AppbarBackground = "#1B1C1F", + AppbarText = "#E0E0E0", + TextPrimary = "#FFFFFF", + TextSecondary = "#A3A6AA", + ActionDefault = "#00E3C0", + Divider = "#383A40" }, + LayoutProperties = new LayoutProperties() }; } -} +} \ No newline at end of file diff --git a/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj b/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj index 32d92b4..01abcaf 100644 --- a/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj +++ b/TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable true @@ -12,8 +12,8 @@ - - + + diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor index 2e8862b..c57db2a 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Pinned.razor @@ -282,7 +282,7 @@ MaxWidth = MaxWidth.Medium }; var parameters = new DialogParameters(); - var dialog = Dialogs.Show("Add Pinned Item", parameters, options); + var dialog = await Dialogs.ShowAsync("Add Pinned Item", parameters, options); var result = await dialog.Result; if (result.Canceled || result.Data is not AddPinnedItemDialog.AddPinnedResult r) return; @@ -331,7 +331,7 @@ { { x => x.Message, $"Unpin and delete '{(row.Name ?? row.Cid)}'?" } }; - var dialog = Dialogs.Show("Confirm Delete", parameters); + var dialog = await Dialogs.ShowAsync("Confirm Delete", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not bool ok || !ok) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor index 1e594cd..f7f1034 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/PinnedIpns.razor @@ -283,7 +283,7 @@ { x => x.ManagedRoot, ManagedRoot }, { x => x.IsEdit, false } }; - var dlg = Dialogs.Show("Add IPNS", parameters, + var dlg = await Dialogs.ShowAsync("Add IPNS", parameters, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Medium, CloseButton = true }); var res = await dlg.Result; if (res.Canceled || res.Data is not AddOrEditIpnsDialog.IpnsResult r) return; @@ -317,7 +317,7 @@ { x => x.InitialKeepOldCidPinned, row.KeepOld } }; - var dlg = Dialogs.Show($"Edit IPNS - {row.Name}", parameters, + var dlg = await Dialogs.ShowAsync($"Edit IPNS - {row.Name}", parameters, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Medium, CloseButton = true }); var res = await dlg.Result; if (res.Canceled || res.Data is not AddOrEditIpnsDialog.IpnsResult r) return; @@ -348,7 +348,7 @@ { { x => x.Message, $"Remove IPNS '{row.Name}' from tracking? (Pinned versions stay.)" } }; - var dlg = Dialogs.Show("Confirm Remove", confirmParams); + var dlg = await Dialogs.ShowAsync("Confirm Remove", confirmParams); var res = await dlg.Result; if (res.Canceled || res.Data is not bool ok || !ok) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor index 2aa622a..fedb91c 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor @@ -71,7 +71,7 @@ { x => x.ExistingNames, _keys.Select(k => k.Name).ToList() } }; - var dialog = Dialogs.Show("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var dialog = await Dialogs.ShowAsync("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); var result = await dialog.Result; if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; @@ -103,7 +103,7 @@ { x => x.Message, $"Delete API key '{key.Name}'?" } }; - var dialog = Dialogs.Show("Confirm Delete", p); + var dialog = await Dialogs.ShowAsync("Confirm Delete", p); var result = await dialog.Result; if (result.Canceled || result.Data is not bool yes || !yes) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor index 8e0e32a..daa683b 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor @@ -168,7 +168,7 @@ private async Task PublishDomain(EdgeDomain d) { var parms = new DialogParameters { { x => x.Domain, d.Domain } }; - var dlg = Dialogs.Show($"Publish: {d.Domain}", parms, + var dlg = await Dialogs.ShowAsync($"Publish: {d.Domain}", parms, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Large, CloseButton = true }); await dlg.Result; @@ -225,7 +225,7 @@ private async Task ImportDomain() { var parms = new DialogParameters(); - var dlg = Dialogs.Show("Import Domain Backup", parms, + var dlg = await Dialogs.ShowAsync("Import Domain Backup", parms, new DialogOptions { FullWidth = true, MaxWidth = MaxWidth.Medium, CloseButton = true }); await dlg.Result; } @@ -233,7 +233,7 @@ private async Task PromptAsync(string title, string message) { var p = new DialogParameters { { x => x.Message, message } }; - var dlg = Dialogs.Show(title, p); + var dlg = await Dialogs.ShowAsync(title, p); var res = await dlg.Result; return res.Canceled ? null : (res.Data as string); } @@ -271,7 +271,7 @@ => string.IsNullOrWhiteSpace(s) || s.Length <= head + tail + 1 ? s : $"{s[..head]}…{s[^tail..]}"; private async Task AddDomain() { - var dialog = Dialogs.Show("Add Domain"); + var dialog = await Dialogs.ShowAsync("Add Domain"); var result = await dialog.Result; if (result.Canceled || result.Data is not AddOrEditDomainDialog.DomainResult r) return; @@ -316,7 +316,7 @@ { x => x.IsEdit, true } }; - var dialog = Dialogs.Show($"Edit Domain: {domain.Domain}", parameters); + var dialog = await Dialogs.ShowAsync($"Edit Domain: {domain.Domain}", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not AddOrEditDomainDialog.DomainResult r) return; @@ -359,7 +359,7 @@ { x => x.Message, $"Delete domain '{domain.Domain}'?\n\nThis will remove its config entry, IPNS key, pins, and MFS folders." } }; - var dialog = Dialogs.Show("Confirm Delete", parameters); + var dialog = await Dialogs.ShowAsync("Confirm Delete", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not bool confirmed || !confirmed) return; diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index 8e1235a..ef05bc8 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -272,13 +272,13 @@ private async Task OpenSearchDialog() { var parameters = new DialogParameters { { x => x.InitialIp, _banFilters.IpFilter } }; - await Dialogs.Show("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + await await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; } // -------------------- ADD BAN DIALOG -------------------- private async Task OpenAddBanDialog() { - await Dialogs.Show("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; await ReloadBans(); } diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor index 053b3a4..7dd9d7e 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/UserSettings.razor @@ -83,7 +83,7 @@ private async Task AddUser() { - var dialog = Dialogs.Show("Add User"); + var dialog = await Dialogs.ShowAsync("Add User"); var result = await dialog.Result; if (result.Canceled || result.Data is not AddUserDialog.AddUserResult r) return; @@ -115,7 +115,7 @@ { x => x.TargetUserName, user.UserName } }; - var dialog = Dialogs.Show($"Change Password: {user.UserName}", parameters); + var dialog = await Dialogs.ShowAsync($"Change Password: {user.UserName}", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not ChangePasswordDialog.ChangePasswordResult r) return; @@ -147,7 +147,7 @@ { x => x.Message, $"Delete user '{user.UserName}'?" } }; - var dialog = Dialogs.Show("Confirm Delete", parameters); + var dialog = await Dialogs.ShowAsync("Confirm Delete", parameters); var result = await dialog.Result; if (result.Canceled || result.Data is not bool confirmed || !confirmed) return; diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index 624c429..e416567 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable TruthGate_Web @@ -9,20 +9,20 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - + + + + + + + From 656fc5cc06354008228ed2daf007b65e8ee7e7e1 Mon Sep 17 00:00:00 2001 From: MagicCodingMan Date: Fri, 24 Jul 2026 12:38:29 -0400 Subject: [PATCH 02/98] double await fix --- .../TruthGate-Web/Components/Pages/Settings/IpBansPage.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index ef05bc8..1c4f95a 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -272,7 +272,7 @@ private async Task OpenSearchDialog() { var parameters = new DialogParameters { { x => x.InitialIp, _banFilters.IpFilter } }; - await await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; } // -------------------- ADD BAN DIALOG -------------------- From 54bca09d0b542c23ba0760bad48c49b7782819f0 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:41:35 -0400 Subject: [PATCH 03/98] Add .NET 10 migration build workflow --- .github/workflows/net10-migration-build.yml | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/net10-migration-build.yml diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml new file mode 100644 index 0000000..97b8a50 --- /dev/null +++ b/.github/workflows/net10-migration-build.yml @@ -0,0 +1,28 @@ +name: .NET 10 migration build + +on: + push: + branches: + - agent/net10-mudblazor-migration + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore TruthGate-IPFS.sln + + - name: Build + run: dotnet build TruthGate-IPFS.sln --configuration Release --no-restore From ee37edf1f436a7a019f91159aac827af84e22f7c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:41:51 -0400 Subject: [PATCH 04/98] Run migration build for pull requests --- .github/workflows/net10-migration-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 97b8a50..3aeda81 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -4,6 +4,9 @@ on: push: branches: - agent/net10-mudblazor-migration + pull_request: + branches: + - magiccodingman/net10 workflow_dispatch: permissions: From b7fdea3b07fdcda0678dab631d19a9dc29078433 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:42:26 -0400 Subject: [PATCH 05/98] Update CI validation for .NET 10 --- .github/workflows/tls-lifecycle.yml | 58 +++++++++-------------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 714162a..2246ca2 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -3,17 +3,13 @@ name: TLS lifecycle tests on: pull_request: paths: - - "TruthGate-Web/TruthGate-Web/Configuration/**" - - "TruthGate-Web/TruthGate-Web/Services/ConfigWatchAndIssueService.cs" - - "TruthGate-Web/TruthGate-Web.Tests/**" + - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" push: branches: - master paths: - - "TruthGate-Web/TruthGate-Web/Configuration/**" - - "TruthGate-Web/TruthGate-Web/Services/ConfigWatchAndIssueService.cs" - - "TruthGate-Web/TruthGate-Web.Tests/**" + - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" jobs: @@ -25,37 +21,19 @@ jobs: - uses: actions/setup-dotnet@v4 with: - dotnet-version: "9.0.x" - - - name: Restore - run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj - - - name: Test - id: test - shell: bash - run: | - set +e - dotnet test \ - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ - --no-restore \ - --configuration Release \ - --logger "console;verbosity=normal" \ - > tls-test-output.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - exit 0 - - - name: Upload test diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: tls-test-output - path: tls-test-output.log - if-no-files-found: error - - - name: Report failure - if: steps.test.outputs.status != '0' - shell: bash - run: | - echo "TLS lifecycle tests failed. Download the tls-test-output artifact for the complete compiler/test diagnostics." - exit 1 + dotnet-version: "10.0.x" + + - name: Restore solution + run: dotnet restore TruthGate-IPFS.sln + + - name: Build solution + run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release + + - name: Test TLS lifecycle + run: >- + dotnet test + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + --no-restore + --no-build + --configuration Release + --logger "console;verbosity=normal" From 64159d2ff966dc32430ba37d2e7c0cc03b5db98e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:43:51 -0400 Subject: [PATCH 06/98] Capture complete .NET 10 build diagnostics --- .github/workflows/net10-migration-build.yml | 22 ++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 3aeda81..22c0ba3 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -28,4 +28,24 @@ jobs: run: dotnet restore TruthGate-IPFS.sln - name: Build - run: dotnet build TruthGate-IPFS.sln --configuration Release --no-restore + id: build + shell: bash + run: | + set +e + dotnet build TruthGate-IPFS.sln --configuration Release --no-restore > net10-build.log 2>&1 + status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + tail -n 200 net10-build.log + exit 0 + + - name: Upload build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: net10-build-log + path: net10-build.log + if-no-files-found: error + + - name: Report build failure + if: steps.build.outputs.status != '0' + run: exit 1 From 2278b010ae11b6b27dc9a666f69672ef5ec9fdf6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:46:25 -0400 Subject: [PATCH 07/98] Migrate metrics charts to MudBlazor 9 --- .../Components/Shared/DashboardMetrics.razor | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor index c105377..f9982a0 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor @@ -77,10 +77,9 @@ CPU % (System vs Process) @@ -91,10 +90,9 @@ System Memory Used (GB) @@ -105,10 +103,9 @@ Process Memory (MB) · Working Set & GC Heap @@ -119,11 +116,10 @@ ThreadPool · Threads & Queue + ChartOptions="@_chartOptions" /> @@ -156,38 +152,35 @@ private int _detailedWindowSeconds = 30; // Detailed: last 30s private bool _rotateXLabels = true; - // ===== Axis cosmetics ===== - private readonly AxisChartOptions _axisOptions = new() - { - MatchBoundsToSize = true, - XAxisLabelRotation = 45 - }; - private readonly ChartOptions _chartOptions = new() - { - YAxisTicks = 8 - }; + // ===== Chart cosmetics ===== + private readonly LineChartOptions _chartOptions = new() + { + MatchBoundsToSize = true, + XAxisLabelRotation = 45, + YAxisTicks = 8 + }; // ===== Series & labels ===== private string[] _xLabels = Array.Empty(); - private readonly List _cpuSeries = new() + private readonly List> _cpuSeries = new() { - new ChartSeries { Name = "System CPU %", Data = Array.Empty() }, - new ChartSeries { Name = "Process CPU %", Data = Array.Empty() } + new ChartSeries { Name = "System CPU %", Data = Array.Empty().AsChartDataSet() }, + new ChartSeries { Name = "Process CPU %", Data = Array.Empty().AsChartDataSet() } }; - private readonly List _sysMemSeries = new() + private readonly List> _sysMemSeries = new() { - new ChartSeries { Name = "System Used (GB)", Data = Array.Empty() } + new ChartSeries { Name = "System Used (GB)", Data = Array.Empty().AsChartDataSet() } }; - private readonly List _procMemSeries = new() + private readonly List> _procMemSeries = new() { - new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty() }, - new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty() } + new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty().AsChartDataSet() }, + new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty().AsChartDataSet() } }; - private readonly List _tpSeries = new() + private readonly List> _tpSeries = new() { - new ChartSeries { Name = "Threads", Data = Array.Empty() }, - new ChartSeries { Name = "Queue Length", Data = Array.Empty() } + new ChartSeries { Name = "Threads", Data = Array.Empty().AsChartDataSet() }, + new ChartSeries { Name = "Queue Length", Data = Array.Empty().AsChartDataSet() } }; // ===== Now-cards ===== @@ -214,7 +207,7 @@ { while (await _timer!.WaitForNextTickAsync(_cts.Token)) { - _axisOptions.XAxisLabelRotation = _rotateXLabels ? 45 : 0; + _chartOptions.XAxisLabelRotation = _rotateXLabels ? 45 : 0; RebuildSeries(); await InvokeAsync(StateHasChanged); } @@ -251,16 +244,16 @@ _xLabels = BuildLabels(window, _isDetailed); // Series fill - _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray(); - _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray(); + _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray().AsChartDataSet(); + _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray().AsChartDataSet(); - _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray(); + _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray().AsChartDataSet(); - _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray(); - _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray(); + _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray().AsChartDataSet(); + _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray().AsChartDataSet(); - _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray(); - _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray(); + _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray().AsChartDataSet(); + _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray().AsChartDataSet(); } private static IReadOnlyList SliceDetailed(IReadOnlyList pts, int windowSeconds) From e260818921dcdabb56f5f0be7024693524f47013 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:50:13 -0400 Subject: [PATCH 08/98] Automate the next MudBlazor migration fixes --- .github/workflows/net10-migration-build.yml | 64 ++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 22c0ba3..0a2d80d 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: read + contents: write jobs: build: @@ -19,6 +19,68 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Apply compiler-driven migration fixes + if: github.event_name == 'push' && github.ref == 'refs/heads/agent/net10-mudblazor-migration' + shell: bash + run: | + python3 <<'PY' + from pathlib import Path + + def replace(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text(encoding="utf-8-sig") + if old in text: + file.write_text(text.replace(old, new), encoding="utf-8") + + dashboard = "TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor" + replace(dashboard, " MatchBoundsToSize = true,\n", "") + replace(dashboard, ".AsChartDataSet()", "") + + api_keys = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor" + replace(api_keys, + 'Dialogs.ShowAsync("Add API Key", p,', + 'Dialogs.ShowAsync("Add API Key", p,') + replace(api_keys, + "if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;", + "if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;") + replace(api_keys, + "if (result.Canceled || result.Data is not bool yes || !yes) return;", + "if (result is null || result.Canceled || result.Data is not bool yes || !yes) return;") + + ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" + replace(ip_bans, + "LoadBansGridAsync(GridState state)", + "LoadBansGridAsync(GridState state, CancellationToken cancellationToken)") + replace(ip_bans, + "LoadWhitelistIpGridAsync(GridState state)", + "LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken)") + replace(ip_bans, + "LoadWhitelistPrefixGridAsync(GridState state)", + "LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken)") + replace(ip_bans, "Dialogs.ShowMessageBox(", "Dialogs.ShowMessageBoxAsync(") + replace(ip_bans, + 'await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', + 'var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') + replace(ip_bans, + 'await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', + 'var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') + replace(ip_bans, + "DbFactory.CreateDbContextAsync();", + "DbFactory.CreateDbContextAsync(cancellationToken);") + replace(ip_bans, "q.CountAsync();", "q.CountAsync(cancellationToken);") + replace(ip_bans, ".ToListAsync();", ".ToListAsync(cancellationToken);") + PY + + if ! git diff --quiet; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor + git commit -m "Fix MudBlazor 9 chart, dialog, and grid APIs" + git push origin "HEAD:${GITHUB_REF_NAME}" + fi + - name: Setup .NET 10 uses: actions/setup-dotnet@v4 with: From df5c3b5a4ef03c5d9573147337712e9ea490c982 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:51:05 +0000 Subject: [PATCH 09/98] Fix MudBlazor 9 chart, dialog, and grid APIs --- .../Components/Pages/Settings/ApiKeys.razor | 8 ++--- .../Pages/Settings/IpBansPage.razor | 36 ++++++++++--------- .../Components/Shared/DashboardMetrics.razor | 31 ++++++++-------- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor index fedb91c..5d7aaf9 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor @@ -1,4 +1,4 @@ -@page "/keys" +@page "/keys" @using TruthGate_Web.Components.Pages.Settings.Shared @using TruthGate_Web.Models @using TruthGate_Web.Utils @@ -71,10 +71,10 @@ { x => x.ExistingNames, _keys.Select(k => k.Name).ToList() } }; - var dialog = await Dialogs.ShowAsync("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); + var dialog = await Dialogs.ShowAsync("Add API Key", p, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }); var result = await dialog.Result; - if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; + if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return; // Final uniqueness check (server-side) if (_keys.Any(k => string.Equals(k.Name, r.Name, StringComparison.OrdinalIgnoreCase))) @@ -106,7 +106,7 @@ var dialog = await Dialogs.ShowAsync("Confirm Delete", p); var result = await dialog.Result; - if (result.Canceled || result.Data is not bool yes || !yes) return; + if (result is null || result.Canceled || result.Data is not bool yes || !yes) return; await ConfigSvc.UpdateAsync(cfg => { diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index 1c4f95a..0ab5bce 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -1,4 +1,4 @@ -@page "/ip-bans" +@page "/ip-bans" @using Microsoft.EntityFrameworkCore @using System.Net @using TruthGate_Web.Components.Pages.Settings.Shared @@ -212,7 +212,7 @@ } private BanFilters _banFilters = new(); - private async Task> LoadBansGridAsync(GridState state) + private async Task> LoadBansGridAsync(GridState state, CancellationToken cancellationToken) { var page = state.Page + 1; // Mud uses 0-based; your API is 1-based var pageSize = state.PageSize; @@ -237,7 +237,7 @@ private async Task ConfirmUnbanById(Guid id) { - var res = await Dialogs.ShowMessageBox("Confirm", "Unban this entry?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", "Unban this entry?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanByIdAsync(id); @@ -248,7 +248,7 @@ private async Task ConfirmUnbanIp(string ip) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Unban IP {ip} ?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Unban IP {ip} ?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanIpAsync(ip); @@ -259,7 +259,7 @@ private async Task ConfirmUnbanPrefix(string pfx) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Unban prefix {pfx} ?", yesText: "Unban", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Unban prefix {pfx} ?", yesText: "Unban", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.UnbanIpv6PrefixAsync(pfx); @@ -272,13 +272,15 @@ private async Task OpenSearchDialog() { var parameters = new DialogParameters { { x => x.InitialIp, _banFilters.IpFilter } }; - await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + await dialog.Result; } // -------------------- ADD BAN DIALOG -------------------- private async Task OpenAddBanDialog() { - await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result; + var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }); + await dialog.Result; await ReloadBans(); } @@ -289,9 +291,9 @@ private string? _newWhitelistReason; private int? _newWhitelistExpireHours; - private async Task> LoadWhitelistIpGridAsync(GridState state) + private async Task> LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken) { - await using var db = await DbFactory.CreateDbContextAsync(); + await using var db = await DbFactory.CreateDbContextAsync(cancellationToken); var q = db.Whitelists.AsNoTracking().Where(x => x.Ip != null); if (!string.IsNullOrWhiteSpace(_wlIpSearch)) @@ -300,12 +302,12 @@ q = q.Where(x => x.Ip!.Contains(s)); } - var total = await q.CountAsync(); + var total = await q.CountAsync(cancellationToken); var items = await q .OrderByDescending(x => x.CreatedUtc) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new GridData { Items = items, TotalItems = total }; } @@ -331,7 +333,7 @@ private async Task RemoveWhitelistIp(string ip) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Remove {ip} from whitelist?", yesText: "Remove", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Remove {ip} from whitelist?", yesText: "Remove", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.RemoveWhitelistIpAsync(ip); @@ -347,9 +349,9 @@ private string? _newWhitelistReasonPfx; private int? _newWhitelistExpireHoursPfx; - private async Task> LoadWhitelistPrefixGridAsync(GridState state) + private async Task> LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken) { - await using var db = await DbFactory.CreateDbContextAsync(); + await using var db = await DbFactory.CreateDbContextAsync(cancellationToken); var q = db.Whitelists.AsNoTracking().Where(x => x.Ipv6Prefix != null); if (!string.IsNullOrWhiteSpace(_wlPfxSearch)) @@ -358,12 +360,12 @@ q = q.Where(x => x.Ipv6Prefix!.Contains(s)); } - var total = await q.CountAsync(); + var total = await q.CountAsync(cancellationToken); var items = await q .OrderByDescending(x => x.CreatedUtc) .Skip(state.Page * state.PageSize) .Take(state.PageSize) - .ToListAsync(); + .ToListAsync(cancellationToken); return new GridData { Items = items, TotalItems = total }; } @@ -389,7 +391,7 @@ private async Task RemoveWhitelistPrefix(string pfx) { - var res = await Dialogs.ShowMessageBox("Confirm", $"Remove {pfx} from whitelist?", yesText: "Remove", cancelText: "Cancel"); + var res = await Dialogs.ShowMessageBoxAsync("Confirm", $"Remove {pfx} from whitelist?", yesText: "Remove", cancelText: "Cancel"); if (res == true) { var ok = await RateSvc.RemoveWhitelistIpv6PrefixAsync(pfx); diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor index f9982a0..821cb77 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor @@ -1,4 +1,4 @@ -@using System.Globalization +@using System.Globalization @using MudBlazor @using TruthGate_Web.Models.Metrics @* MetricPoint, MetricSnapshot, ThreadSpike *@ @inject IMetricService Metrics @@ -155,7 +155,6 @@ // ===== Chart cosmetics ===== private readonly LineChartOptions _chartOptions = new() { - MatchBoundsToSize = true, XAxisLabelRotation = 45, YAxisTicks = 8 }; @@ -165,22 +164,22 @@ private readonly List> _cpuSeries = new() { - new ChartSeries { Name = "System CPU %", Data = Array.Empty().AsChartDataSet() }, - new ChartSeries { Name = "Process CPU %", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "System CPU %", Data = Array.Empty() }, + new ChartSeries { Name = "Process CPU %", Data = Array.Empty() } }; private readonly List> _sysMemSeries = new() { - new ChartSeries { Name = "System Used (GB)", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "System Used (GB)", Data = Array.Empty() } }; private readonly List> _procMemSeries = new() { - new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty().AsChartDataSet() }, - new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "Working Set (MB)", Data = Array.Empty() }, + new ChartSeries { Name = "GC Heap (MB)", Data = Array.Empty() } }; private readonly List> _tpSeries = new() { - new ChartSeries { Name = "Threads", Data = Array.Empty().AsChartDataSet() }, - new ChartSeries { Name = "Queue Length", Data = Array.Empty().AsChartDataSet() } + new ChartSeries { Name = "Threads", Data = Array.Empty() }, + new ChartSeries { Name = "Queue Length", Data = Array.Empty() } }; // ===== Now-cards ===== @@ -244,16 +243,16 @@ _xLabels = BuildLabels(window, _isDetailed); // Series fill - _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray().AsChartDataSet(); - _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray().AsChartDataSet(); + _cpuSeries[0].Data = window.Select(p => p.SysCpuPct).ToArray(); + _cpuSeries[1].Data = window.Select(p => p.ProcCpuPct).ToArray(); - _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray().AsChartDataSet(); + _sysMemSeries[0].Data = window.Select(p => p.SysMemUsedMB / 1024.0).ToArray(); - _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray().AsChartDataSet(); - _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray().AsChartDataSet(); + _procMemSeries[0].Data = window.Select(p => p.ProcWorkingSetMB).ToArray(); + _procMemSeries[1].Data = window.Select(p => p.ProcGcHeapMB).ToArray(); - _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray().AsChartDataSet(); - _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray().AsChartDataSet(); + _tpSeries[0].Data = window.Select(p => (double)p.ThreadPoolThreads).ToArray(); + _tpSeries[1].Data = window.Select(p => (double)p.ThreadPoolQueueLength).ToArray(); } private static IReadOnlyList SliceDetailed(IReadOnlyList pts, int windowSeconds) From d86c43f03e705e4afa63c2a7b085c96e225dfaf2 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:52:19 -0400 Subject: [PATCH 10/98] Run migration patches against the repair branch --- .github/workflows/net10-migration-build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 0a2d80d..b536096 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -16,11 +16,12 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout repair branch uses: actions/checkout@v4 + with: + ref: agent/net10-mudblazor-migration - name: Apply compiler-driven migration fixes - if: github.event_name == 'push' && github.ref == 'refs/heads/agent/net10-mudblazor-migration' shell: bash run: | python3 <<'PY' @@ -78,7 +79,7 @@ jobs: TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor git commit -m "Fix MudBlazor 9 chart, dialog, and grid APIs" - git push origin "HEAD:${GITHUB_REF_NAME}" + git push origin HEAD:agent/net10-mudblazor-migration fi - name: Setup .NET 10 From aff0a368b59d44d3c81da7fef1db5e7e85d0a32c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:54:02 -0400 Subject: [PATCH 11/98] Capture complete .NET 10 test diagnostics --- .github/workflows/tls-lifecycle.yml | 34 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 2246ca2..e458f25 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -30,10 +30,30 @@ jobs: run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release - name: Test TLS lifecycle - run: >- - dotnet test - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj - --no-restore - --no-build - --configuration Release - --logger "console;verbosity=normal" + id: test + shell: bash + run: | + set +e + dotnet test \ + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ + --no-restore \ + --no-build \ + --configuration Release \ + --logger "console;verbosity=normal" \ + > tls-test-output.log 2>&1 + status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + tail -n 200 tls-test-output.log + exit 0 + + - name: Upload test diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: tls-test-output + path: tls-test-output.log + if-no-files-found: error + + - name: Report test failure + if: steps.test.outputs.status != '0' + run: exit 1 From 081c4539f797e0d995da9f8172d12a7063736ffa Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:55:19 -0400 Subject: [PATCH 12/98] Retarget TLS tests to .NET 10 --- TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj index 69e4f2d..825fa90 100644 --- a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj +++ b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable false From 00ef468a4d02182c1d6f8a53d31db96f9dccc3dc Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:56:38 -0400 Subject: [PATCH 13/98] Migrate domain backup file upload to MudBlazor 9 --- .../Settings/Shared/ImportDomainDialog.razor | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor index dfd94d7..3a6e7da 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ImportDomainDialog.razor @@ -14,15 +14,17 @@ Immediate="true" Required="true" /> -
- - +
+ + + StartIcon="@Icons.Material.Filled.VpnKey" + OnClick="@context.OpenFilePickerAsync"> Select Backup (.tgbackup) -
+ +
@@ -43,8 +45,8 @@ private IBrowserFile? _file; private string? _pass; - private void OnFileChanged(IReadOnlyList picked) - => _file = picked.FirstOrDefault(); + private void OnFileChanged(IBrowserFile picked) + => _file = picked; private async Task Submit() { From 74cbeb3124751acd2fb2ff2fa81945afe5c44b4f Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:57:11 -0400 Subject: [PATCH 14/98] Apply remaining MudBlazor 9 component migrations --- .github/workflows/net10-migration-build.yml | 142 +++++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index b536096..409a556 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -59,6 +59,7 @@ jobs: "LoadWhitelistPrefixGridAsync(GridState state)", "LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken)") replace(ip_bans, "Dialogs.ShowMessageBox(", "Dialogs.ShowMessageBoxAsync(") + replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') replace(ip_bans, 'await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', 'var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') @@ -70,6 +71,138 @@ jobs: "DbFactory.CreateDbContextAsync(cancellationToken);") replace(ip_bans, "q.CountAsync();", "q.CountAsync(cancellationToken);") replace(ip_bans, ".ToListAsync();", ".ToListAsync(cancellationToken);") + + simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" + replace(simple_prompt, '', '') + replace(simple_prompt, + 'Required="@Required"\n Lines=', + 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=') + replace(simple_prompt, + ''' private IEnumerable Validate() + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(_value)) + { + _error = "Please enter a value."; + yield return _error; + } + } +''', + ''' private string? ValidateValue(string? value) + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; + } +''') + replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") + + add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" + replace(add_pinned, + '', + '') + + add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" + replace(add_ipns, + '', + '') + replace(add_ipns, + 'Required="true"\n Disabled="_isEdit" />', + 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', + ) + # The second required/disabled field is the IPNS key; correct its validator after the global replacement. + replace(add_ipns, + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"') + replace(add_ipns, + ''' private IEnumerable Validate() + { + _error = null; + + if (string.IsNullOrWhiteSpace(_model.Name)) + yield return "Name is required."; + if (string.IsNullOrWhiteSpace(_model.Key)) + yield return "IPNS key is required."; + + var leaf = IpfsGateway.ToSafeLeaf(_model.Name); + if (leaf is null) + yield return "Invalid name."; + if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) + yield return "IPNS Key should look like k51… or /ipns/k51…"; + } +''', + ''' private string? ValidateName(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; + + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } + + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; + } +''') + replace(add_ipns, "private async void Submit()", "private async Task Submit()") + replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") + replace(add_ipns, + '', + '') + + search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" + replace(search_ip, '', '') + replace(search_ip, + 'Required="true"\n @onkeydown=', + 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=') + replace(search_ip, + ''' private IEnumerable Validate() + { + _error = null; + if (string.IsNullOrWhiteSpace(_ip)) + { + _error = "Please enter an IP address."; + yield return _error; + yield break; + } + if (!IPAddress.TryParse(_ip, out _)) + { + _error = "Invalid IP format (v4 or v6 required)."; + yield return _error; + } + } +''', + ''' private string? ValidateIp(string? value) + { + _error = null; + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; + } +''') + replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") + replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') + + domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" + replace(domains, 'Class="truncate-text" Title="@context.RedirectUrl"', 'Class="truncate-text" title="@context.RedirectUrl"') + replace(domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"') + replace(domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"') PY if ! git diff --quiet; then @@ -77,8 +210,13 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor \ TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor - git commit -m "Fix MudBlazor 9 chart, dialog, and grid APIs" + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor \ + TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor \ + TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor + git commit -m "Finish MudBlazor 9 form and component migrations" git push origin HEAD:agent/net10-mudblazor-migration fi From c7986e890273f629cd050756866a223f1b487e88 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:58:18 -0400 Subject: [PATCH 15/98] Add temporary compiler-driven migration script --- .github/scripts/apply-net10-migrations.py | 170 ++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/scripts/apply-net10-migrations.py diff --git a/.github/scripts/apply-net10-migrations.py b/.github/scripts/apply-net10-migrations.py new file mode 100644 index 0000000..957ec3d --- /dev/null +++ b/.github/scripts/apply-net10-migrations.py @@ -0,0 +1,170 @@ +from pathlib import Path + + +def replace(path: str, old: str, new: str) -> None: + file = Path(path) + text = file.read_text(encoding="utf-8-sig") + if old in text: + file.write_text(text.replace(old, new), encoding="utf-8") + + +simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" +replace(simple_prompt, '', '') +replace( + simple_prompt, + 'Required="@Required"\n Lines=', + 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=', +) +replace( + simple_prompt, + ''' private IEnumerable Validate() + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(_value)) + { + _error = "Please enter a value."; + yield return _error; + } + } +''', + ''' private string? ValidateValue(string? value) + { + _error = null; + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; + } +''', +) +replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") + +add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" +replace( + add_pinned, + '', + '', +) + +add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" +replace( + add_ipns, + '', + '', +) +replace( + add_ipns, + 'Required="true"\n Disabled="_isEdit" />', + 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', +) +replace( + add_ipns, + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', + 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"', +) +replace( + add_ipns, + ''' private IEnumerable Validate() + { + _error = null; + + if (string.IsNullOrWhiteSpace(_model.Name)) + yield return "Name is required."; + if (string.IsNullOrWhiteSpace(_model.Key)) + yield return "IPNS key is required."; + + var leaf = IpfsGateway.ToSafeLeaf(_model.Name); + if (leaf is null) + yield return "Invalid name."; + if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) + yield return "IPNS Key should look like k51… or /ipns/k51…"; + } +''', + ''' private string? ValidateName(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; + + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } + + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; + } +''', +) +replace(add_ipns, "private async void Submit()", "private async Task Submit()") +replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") +replace( + add_ipns, + '', + '', +) + +search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" +replace(search_ip, '', '') +replace( + search_ip, + 'Required="true"\n @onkeydown=', + 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=', +) +replace( + search_ip, + ''' private IEnumerable Validate() + { + _error = null; + if (string.IsNullOrWhiteSpace(_ip)) + { + _error = "Please enter an IP address."; + yield return _error; + yield break; + } + if (!IPAddress.TryParse(_ip, out _)) + { + _error = "Invalid IP format (v4 or v6 required)."; + yield return _error; + } + } +''', + ''' private string? ValidateIp(string? value) + { + _error = null; + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; + } +''', +) +replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") +replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') + +ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" +replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') + +domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" +replace( + domains, + 'Class="truncate-text" Title="@context.RedirectUrl"', + 'Class="truncate-text" title="@context.RedirectUrl"', +) +replace( + domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"', +) +replace( + domains, + 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', + 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"', +) From 1a6726c6cd92c2c71cff38baac7ef4c98f4fe1f5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:58:28 -0400 Subject: [PATCH 16/98] Run migration fixes from a standalone script --- .github/workflows/net10-migration-build.yml | 191 +------------------- 1 file changed, 2 insertions(+), 189 deletions(-) diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml index 409a556..fcdf3f0 100644 --- a/.github/workflows/net10-migration-build.yml +++ b/.github/workflows/net10-migration-build.yml @@ -24,198 +24,11 @@ jobs: - name: Apply compiler-driven migration fixes shell: bash run: | - python3 <<'PY' - from pathlib import Path - - def replace(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text(encoding="utf-8-sig") - if old in text: - file.write_text(text.replace(old, new), encoding="utf-8") - - dashboard = "TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor" - replace(dashboard, " MatchBoundsToSize = true,\n", "") - replace(dashboard, ".AsChartDataSet()", "") - - api_keys = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor" - replace(api_keys, - 'Dialogs.ShowAsync("Add API Key", p,', - 'Dialogs.ShowAsync("Add API Key", p,') - replace(api_keys, - "if (result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;", - "if (result is null || result.Canceled || result.Data is not AddApiKeyDialog.AddApiKeyResult r) return;") - replace(api_keys, - "if (result.Canceled || result.Data is not bool yes || !yes) return;", - "if (result is null || result.Canceled || result.Data is not bool yes || !yes) return;") - - ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" - replace(ip_bans, - "LoadBansGridAsync(GridState state)", - "LoadBansGridAsync(GridState state, CancellationToken cancellationToken)") - replace(ip_bans, - "LoadWhitelistIpGridAsync(GridState state)", - "LoadWhitelistIpGridAsync(GridState state, CancellationToken cancellationToken)") - replace(ip_bans, - "LoadWhitelistPrefixGridAsync(GridState state)", - "LoadWhitelistPrefixGridAsync(GridState state, CancellationToken cancellationToken)") - replace(ip_bans, "Dialogs.ShowMessageBox(", "Dialogs.ShowMessageBoxAsync(") - replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') - replace(ip_bans, - 'await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', - 'var dialog = await Dialogs.ShowAsync("Search IP", parameters, new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') - replace(ip_bans, - 'await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }).Result;', - 'var dialog = await Dialogs.ShowAsync("Add Ban", new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small });\n await dialog.Result;') - replace(ip_bans, - "DbFactory.CreateDbContextAsync();", - "DbFactory.CreateDbContextAsync(cancellationToken);") - replace(ip_bans, "q.CountAsync();", "q.CountAsync(cancellationToken);") - replace(ip_bans, ".ToListAsync();", ".ToListAsync(cancellationToken);") - - simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" - replace(simple_prompt, '', '') - replace(simple_prompt, - 'Required="@Required"\n Lines=', - 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=') - replace(simple_prompt, - ''' private IEnumerable Validate() - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } - } -''', - ''' private string? ValidateValue(string? value) - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(value)) - return _error = "Please enter a value."; - - return null; - } -''') - replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") - - add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" - replace(add_pinned, - '', - '') - - add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" - replace(add_ipns, - '', - '') - replace(add_ipns, - 'Required="true"\n Disabled="_isEdit" />', - 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', - ) - # The second required/disabled field is the IPNS key; correct its validator after the global replacement. - replace(add_ipns, - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"') - replace(add_ipns, - ''' private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; - - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; - } -''', - ''' private string? ValidateName(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "Name is required."; - - return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; - } - - private static string? ValidateKey(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "IPNS key is required."; - - return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || - value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) - ? null - : "IPNS Key should look like k51… or /ipns/k51…"; - } -''') - replace(add_ipns, "private async void Submit()", "private async Task Submit()") - replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") - replace(add_ipns, - '', - '') - - search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" - replace(search_ip, '', '') - replace(search_ip, - 'Required="true"\n @onkeydown=', - 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=') - replace(search_ip, - ''' private IEnumerable Validate() - { - _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } - } -''', - ''' private string? ValidateIp(string? value) - { - _error = null; - if (string.IsNullOrWhiteSpace(value)) - return _error = "Please enter an IP address."; - - if (!IPAddress.TryParse(value, out _)) - return _error = "Invalid IP format (v4 or v6 required)."; - - return null; - } -''') - replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") - replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') - - domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" - replace(domains, 'Class="truncate-text" Title="@context.RedirectUrl"', 'Class="truncate-text" title="@context.RedirectUrl"') - replace(domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"') - replace(domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"') - PY - + python3 .github/scripts/apply-net10-migrations.py if ! git diff --quiet; then git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add TruthGate-Web/TruthGate-Web/Components/Shared/DashboardMetrics.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/ApiKeys.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor \ - TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor \ - TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor + git add TruthGate-Web git commit -m "Finish MudBlazor 9 form and component migrations" git push origin HEAD:agent/net10-mudblazor-migration fi From db4d5c87fa7715169415d1e80f5f04648763f246 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:58:38 +0000 Subject: [PATCH 17/98] Finish MudBlazor 9 form and component migrations --- .../Components/Pages/Settings/Domains.razor | 8 ++-- .../Pages/Settings/IpBansPage.razor | 6 +-- .../Settings/Shared/SearchIpDialog.razor | 28 ++++++-------- .../Shared/AddOrEditIpnsDialog.razor | 37 ++++++++++--------- .../Shared/AddPinnedItemDialog.razor | 4 +- .../Shared/SimplePromptDialog.razor | 17 ++++----- 6 files changed, 49 insertions(+), 51 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor index daa683b..ffe2c93 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor @@ -1,4 +1,4 @@ -@page "/domains" +@page "/domains" @using System.Globalization @using System.Text.RegularExpressions @using MudBlazor @@ -47,7 +47,7 @@ @context.Domain @if (!string.IsNullOrWhiteSpace(context.RedirectUrl)) { - + @{ var u = context.RedirectUrl ?? string.Empty; } @@ -60,7 +60,7 @@ - + @($"{GetIpnsHost(context, useShort: true)}") @@ -88,7 +88,7 @@ @context.IpnsKeyName *@ - + IPNS: @Short(context.IpnsPeerId) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor index 0ab5bce..acb3adc 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor @@ -52,7 +52,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -118,7 +118,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> @@ -164,7 +164,7 @@ Hover="true" Dense="true" Bordered="true" - Sortable="false" + SortMode="SortMode.None" Filterable="false" RowsPerPage="20"> diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor index 997c8b8..142514b 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor @@ -1,4 +1,4 @@ -@using System.Net +@using System.Net @using Microsoft.EntityFrameworkCore @using MudBlazor @using TruthGate_Web.Security @@ -11,7 +11,7 @@ @Message } - + Recent ban records (DB, latest 10) - + @context.Item.CreatedUtc.ToString("u") @@ -106,25 +106,21 @@ _ip = InitialIp?.Trim() ?? ""; } - private IEnumerable Validate() + private string? ValidateIp(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please enter an IP address."; + + if (!IPAddress.TryParse(value, out _)) + return _error = "Invalid IP format (v4 or v6 required)."; + + return null; } private async Task RunSearch() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; try diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor index 11bd822..6f20b04 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor @@ -1,4 +1,4 @@ -@using MudBlazor +@using MudBlazor @using System.Text.Json @using TruthGate_Web.Utils @using TruthGate_Web.Endpoints @@ -6,7 +6,7 @@ - + - +
@@ -60,7 +60,7 @@ - +
@@ -193,20 +193,23 @@ _model.KeepOldCidPinned = InitialKeepOldCidPinned; } - private IEnumerable Validate() + private string? ValidateName(string? value) { - _error = null; + if (string.IsNullOrWhiteSpace(value)) + return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; + return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; + } - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; + private static string? ValidateKey(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "IPNS key is required."; + + return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) + ? null + : "IPNS Key should look like k51… or /ipns/k51…"; } private void Cancel() => MudDialog.Cancel(); @@ -293,9 +296,9 @@ return null; } - private async void Submit() + private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; // Name leaf (immutable on edit) diff --git a/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor index b473c89..76207eb 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor @@ -1,9 +1,9 @@ -@using MudBlazor +@using MudBlazor @using TruthGate_Web.Utils - + Add Pinned Item @@ -7,7 +7,7 @@ @Message } - + Validate() + private string? ValidateValue(string? value) { _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } + if (Required && string.IsNullOrWhiteSpace(value)) + return _error = "Please enter a value."; + + return null; } private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; MudDialog.Close(DialogResult.Ok(_value)); } From 190af2f24a6460d4674020d54ea8f1510aa1f25c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:01:14 -0400 Subject: [PATCH 18/98] Update .NET 10 test host packages --- .../TruthGate-Web.Tests/TruthGate-Web.Tests.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj index 825fa90..4b520df 100644 --- a/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj +++ b/TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj @@ -9,9 +9,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all From 19b9840f76824ed343b0ec3be57a2701590e1c52 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:03:25 -0400 Subject: [PATCH 19/98] Add .NET 10 test-host diagnostics --- .github/workflows/tls-lifecycle.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index e458f25..3f26f46 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -37,12 +37,14 @@ jobs: dotnet test \ TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ --no-restore \ - --no-build \ --configuration Release \ --logger "console;verbosity=normal" \ + --diag tls-vstest-diag.log \ > tls-test-output.log 2>&1 status=$? echo "status=$status" >> "$GITHUB_OUTPUT" + echo "Test output directory:" + find TruthGate-Web/TruthGate-Web.Tests/bin/Release/net10.0 -maxdepth 1 -type f -printf '%f\n' | sort tail -n 200 tls-test-output.log exit 0 @@ -51,7 +53,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: tls-test-output - path: tls-test-output.log + path: | + tls-test-output.log + tls-vstest-diag.log if-no-files-found: error - name: Report test failure From cbc586d7037b3c33577caec9034f8d3fa1678e1e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:05:31 -0400 Subject: [PATCH 20/98] Restore TLS test project explicitly --- .github/workflows/tls-lifecycle.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 3f26f46..9b1e657 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -26,6 +26,9 @@ jobs: - name: Restore solution run: dotnet restore TruthGate-IPFS.sln + - name: Restore TLS tests + run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + - name: Build solution run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release @@ -43,8 +46,6 @@ jobs: > tls-test-output.log 2>&1 status=$? echo "status=$status" >> "$GITHUB_OUTPUT" - echo "Test output directory:" - find TruthGate-Web/TruthGate-Web.Tests/bin/Release/net10.0 -maxdepth 1 -type f -printf '%f\n' | sort tail -n 200 tls-test-output.log exit 0 From cf0090430ec72bfc3063e7cfa3c7073d0194bcb5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:08:59 -0400 Subject: [PATCH 21/98] Migrate add-ban validation to async MudForm APIs --- .../Pages/Settings/Shared/AddBanDialog.razor | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor index 45e4ca4..55d30b2 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddBanDialog.razor @@ -10,7 +10,7 @@ @Message } - + @@ -19,6 +19,7 @@ Placeholder="@(_usePrefix ? "2001:db8::/64" : "203.0.113.42 or ::ffff:203.0.113.42")" Immediate="true" Required="true" + Validation="@(new Func(ValidateTarget))" @onkeydown="OnKeyDown" /> @@ -30,7 +31,13 @@ - + Validate() + private string? ValidateTarget(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_target)) - { - _error = "Please provide a target."; - yield return _error!; - yield break; - } - - var t = _target.Trim(); + if (string.IsNullOrWhiteSpace(value)) + return _error = "Please provide a target."; + var target = value.Trim(); if (_usePrefix) { - // Accept "2001:db8::" or "2001:db8::/64" — the service canonicalizes either - var bare = t.Contains('/') ? t[..t.IndexOf('/')] : t; - if (!IPAddress.TryParse(bare, out var parsed) || parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) + // Accept "2001:db8::" or "2001:db8::/64" — the service canonicalizes either. + var slash = target.IndexOf('/'); + var bare = slash >= 0 ? target[..slash] : target; + if (!IPAddress.TryParse(bare, out var parsed) || + parsed.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6) { - _error = "Invalid IPv6 prefix format."; - yield return _error!; + return _error = "Invalid IPv6 prefix format."; } } - else + else if (!IPAddress.TryParse(target, out _)) { - if (!IPAddress.TryParse(t, out _)) - { - _error = "Invalid IP address."; - yield return _error!; - } + return _error = "Invalid IP address."; } - if (_durationMinutes <= 0) - { - _error = "Duration must be at least 1 minute."; - yield return _error!; - } + return null; + } + + private string? ValidateDuration(int value) + { + if (value <= 0) + return _error = "Duration must be at least 1 minute."; + + return null; } private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; try { - var t = _target!.Trim(); - var dur = TimeSpan.FromMinutes(_durationMinutes); - var reason = string.IsNullOrWhiteSpace(_reason) ? (_usePrefix ? "MANUAL_PREFIX_BAN" : "MANUAL_BAN") : _reason; - - bool ok = _usePrefix - ? await RateSvc.BanIpv6PrefixAsync(t, dur, _scope, _trueBan, reason) - : await RateSvc.BanIpAsync(t, dur, _scope, _trueBan, reason); + var target = _target!.Trim(); + var duration = TimeSpan.FromMinutes(_durationMinutes); + var reason = string.IsNullOrWhiteSpace(_reason) + ? (_usePrefix ? "MANUAL_PREFIX_BAN" : "MANUAL_BAN") + : _reason; + + var ok = _usePrefix + ? await RateSvc.BanIpv6PrefixAsync(target, duration, _scope, _trueBan, reason) + : await RateSvc.BanIpAsync(target, duration, _scope, _trueBan, reason); if (ok) { From 362294cce97647de0a1088ce376ce259a5aa61cb Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:11 -0400 Subject: [PATCH 22/98] Migrate add-user validation to async MudForm APIs --- .../Pages/Settings/Shared/AddUserDialog.razor | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor index 53e1c30..8e2691c 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddUserDialog.razor @@ -2,10 +2,24 @@ - - - - + + + + @if (!string.IsNullOrEmpty(_error)) { @_error @@ -29,7 +43,7 @@ } private MudForm _form = default!; - private Model _model = new(); + private readonly Model _model = new(); private string? _error; private class Model @@ -39,34 +53,33 @@ public string Confirm { get; set; } = ""; } - private IEnumerable Validate() + private string? ValidateConfirmation(string? value) { _error = null; - if (string.IsNullOrWhiteSpace(_model.UserName)) - yield return "Username is required."; + if (string.IsNullOrEmpty(value)) + return _error = "Please confirm the password."; - if (string.IsNullOrEmpty(_model.Password)) - yield return "Password is required."; + if (_model.Password != value) + return _error = "Passwords must match."; - if (_model.Password != _model.Confirm) - yield return "Passwords must match."; + return null; } private void Cancel() => MudDialog.Cancel(); private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; - // normalize username to lowercase (your Config also normalizes) + // Normalize username to lowercase (Config also normalizes it). var result = new AddUserResult - { - UserName = _model.UserName.Trim().ToLowerInvariant(), - Password = _model.Password - }; + { + UserName = _model.UserName.Trim().ToLowerInvariant(), + Password = _model.Password + }; MudDialog.Close(DialogResult.Ok(result)); } -} \ No newline at end of file +} From baec649ae732facc4453bcc4a4ae596b546329e4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:24 -0400 Subject: [PATCH 23/98] Migrate password validation to async MudForm APIs --- .../Shared/ChangePasswordDialog.razor | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor index 07ba96d..06383f3 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/ChangePasswordDialog.razor @@ -2,10 +2,20 @@ - + User: @TargetUserName - - + + @if (!string.IsNullOrEmpty(_error)) { @_error @@ -29,7 +39,7 @@ } private MudForm _form = default!; - private Model _model = new(); + private readonly Model _model = new(); private string? _error; private class Model @@ -38,25 +48,27 @@ public string Confirm { get; set; } = ""; } - private IEnumerable Validate() + private string? ValidateConfirmation(string? value) { _error = null; - if (string.IsNullOrEmpty(_model.Password)) - yield return "Password is required."; + if (string.IsNullOrEmpty(value)) + return _error = "Please confirm the password."; - if (_model.Password != _model.Confirm) - yield return "Passwords must match."; + if (_model.Password != value) + return _error = "Passwords must match."; + + return null; } private void Cancel() => MudDialog.Cancel(); private async Task Submit() { - await _form.Validate(); + await _form.ValidateAsync(); if (!_form.IsValid) return; var result = new ChangePasswordResult { NewPassword = _model.Password }; MudDialog.Close(DialogResult.Ok(result)); } -} \ No newline at end of file +} From 9018affba1f98230f0f635efd267b91b82174ac3 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:09:38 -0400 Subject: [PATCH 24/98] Migrate domain validation to async MudForm APIs --- .../Shared/AddOrEditDomainDialog.razor | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor index ceeb101..d57b477 100644 --- a/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor +++ b/TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/AddOrEditDomainDialog.razor @@ -3,20 +3,20 @@ - + + @bind-Value="_model.Domain" + Immediate="true" + Required="true" + RequiredError="Domain is required." + For="@(() => _model.Domain)" />
+ @bind-Value="_model.UseSSL" + Color="Color.Primary" + Label="@(_model.UseSSL ? "Enabled" : "Disabled")" />
When enabled, TruthGate provisions a valid Let’s Encrypt certificate for this domain. @@ -25,16 +25,15 @@
+ @bind-Value="_model.RedirectUrl" + Immediate="true" + Required="false" + For="@(() => _model.RedirectUrl)" /> Leave empty to serve this domain normally. If a Redirect URL is set, all visitors will be instantly redirected there instead. - @if (!string.IsNullOrEmpty(_error)) { @_error @@ -85,14 +84,6 @@ public string? RedirectUrl { get; set; } } - private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Domain)) - yield return "Domain is required."; - } - private void Cancel() => MudDialog.Cancel(); [Inject] IHttpClientFactory HttpFactory { get; set; } = default!; @@ -100,38 +91,46 @@ private static string ToSafeFolderLeaf(string input) { - var s = (input ?? "").Trim().ToLowerInvariant(); - // kill pathy stuff — you only want a single folder leaf here - s = s.Replace('\\', '/').Trim('/'); - if (s.Contains("..")) throw new InvalidOperationException("Invalid domain."); - if (string.IsNullOrWhiteSpace(s)) throw new InvalidOperationException("Empty domain."); - return s; + var value = (input ?? "").Trim().ToLowerInvariant(); + // Kill path-like input — this must remain a single folder leaf. + value = value.Replace('\\', '/').Trim('/'); + if (value.Contains("..")) throw new InvalidOperationException("Invalid domain."); + if (string.IsNullOrWhiteSpace(value)) throw new InvalidOperationException("Empty domain."); + return value; } private async Task Submit() { - await _form.Validate(); + _error = null; + await _form.ValidateAsync(); if (!_form.IsValid) return; - var domainLeaf = ToSafeFolderLeaf(_model.Domain); - var basePath = "/production/sites"; - var fullPath = $"{basePath}/{domainLeaf}"; + try + { + var domainLeaf = ToSafeFolderLeaf(_model.Domain); + const string basePath = "/production/sites"; + var fullPath = $"{basePath}/{domainLeaf}"; - // Ensure the folder path exists (creates parents as needed), returns final CID - var cid = await IpfsGateway.EnsureMfsFolderExistsAsync(fullPath, HttpFactory); + // Ensure the folder path exists (creates parents as needed), returning the final CID. + _ = await IpfsGateway.EnsureMfsFolderExistsAsync(fullPath, HttpFactory); - // (Optional) warm/refresh your cached CID for this MFS path, so the rest of your app sees it: - var ttl = TimeSpan.FromHours(2); - _ = await IpfsGateway.GetCidForMfsPathAsync( + // Warm/refresh the cached CID so the rest of the app sees the newly created path. + var ttl = TimeSpan.FromHours(2); + _ = await IpfsGateway.GetCidForMfsPathAsync( fullPath, HttpFactory, Cache, ttl, IpfsGateway.CacheMode.Refresh); - var result = new DomainResult + var result = new DomainResult + { + Domain = _model.Domain, // Normalization happens on the page. + UseSSL = _model.UseSSL, + RedirectUrl = _model.RedirectUrl + }; + + MudDialog.Close(DialogResult.Ok(result)); + } + catch (Exception ex) { - Domain = _model.Domain, // normalization happens on the page - UseSSL = _model.UseSSL, - RedirectUrl = _model.RedirectUrl - }; - - MudDialog.Close(DialogResult.Ok(result)); + _error = ex.Message; + } } } From 10596be23e120b7ac7354088e9ffa1a300d24a7e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:10:27 -0400 Subject: [PATCH 25/98] Pin patched SQLite native bundle --- TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index e416567..d18a0fb 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -22,6 +22,7 @@ +
From 0b3fb9a01abf5d72d3d4f63e6d1f0e7d949b4171 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:12:08 -0400 Subject: [PATCH 26/98] Finalize .NET 10 TLS validation workflow --- .github/workflows/tls-lifecycle.yml | 35 +++++------------------------ 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 9b1e657..386d0bf 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -33,32 +33,9 @@ jobs: run: dotnet build TruthGate-IPFS.sln --no-restore --configuration Release - name: Test TLS lifecycle - id: test - shell: bash - run: | - set +e - dotnet test \ - TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj \ - --no-restore \ - --configuration Release \ - --logger "console;verbosity=normal" \ - --diag tls-vstest-diag.log \ - > tls-test-output.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - tail -n 200 tls-test-output.log - exit 0 - - - name: Upload test diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: tls-test-output - path: | - tls-test-output.log - tls-vstest-diag.log - if-no-files-found: error - - - name: Report test failure - if: steps.test.outputs.status != '0' - run: exit 1 + run: >- + dotnet test + TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj + --no-restore + --configuration Release + --logger "console;verbosity=normal" From 2b4b2e050dba4b86abb15be4df5a22d37283a93e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:13:14 -0400 Subject: [PATCH 27/98] Remove temporary migration build workflow --- .github/workflows/net10-migration-build.yml | 65 --------------------- 1 file changed, 65 deletions(-) delete mode 100644 .github/workflows/net10-migration-build.yml diff --git a/.github/workflows/net10-migration-build.yml b/.github/workflows/net10-migration-build.yml deleted file mode 100644 index fcdf3f0..0000000 --- a/.github/workflows/net10-migration-build.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: .NET 10 migration build - -on: - push: - branches: - - agent/net10-mudblazor-migration - pull_request: - branches: - - magiccodingman/net10 - workflow_dispatch: - -permissions: - contents: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repair branch - uses: actions/checkout@v4 - with: - ref: agent/net10-mudblazor-migration - - - name: Apply compiler-driven migration fixes - shell: bash - run: | - python3 .github/scripts/apply-net10-migrations.py - if ! git diff --quiet; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add TruthGate-Web - git commit -m "Finish MudBlazor 9 form and component migrations" - git push origin HEAD:agent/net10-mudblazor-migration - fi - - - name: Setup .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - - name: Restore - run: dotnet restore TruthGate-IPFS.sln - - - name: Build - id: build - shell: bash - run: | - set +e - dotnet build TruthGate-IPFS.sln --configuration Release --no-restore > net10-build.log 2>&1 - status=$? - echo "status=$status" >> "$GITHUB_OUTPUT" - tail -n 200 net10-build.log - exit 0 - - - name: Upload build diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: net10-build-log - path: net10-build.log - if-no-files-found: error - - - name: Report build failure - if: steps.build.outputs.status != '0' - run: exit 1 From c36e6a575e34253f905d1c6e4a66aff087cb036b Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:13:20 -0400 Subject: [PATCH 28/98] Remove temporary migration helper script --- .github/scripts/apply-net10-migrations.py | 170 ---------------------- 1 file changed, 170 deletions(-) delete mode 100644 .github/scripts/apply-net10-migrations.py diff --git a/.github/scripts/apply-net10-migrations.py b/.github/scripts/apply-net10-migrations.py deleted file mode 100644 index 957ec3d..0000000 --- a/.github/scripts/apply-net10-migrations.py +++ /dev/null @@ -1,170 +0,0 @@ -from pathlib import Path - - -def replace(path: str, old: str, new: str) -> None: - file = Path(path) - text = file.read_text(encoding="utf-8-sig") - if old in text: - file.write_text(text.replace(old, new), encoding="utf-8") - - -simple_prompt = "TruthGate-Web/TruthGate-Web/Components/Shared/SimplePromptDialog.razor" -replace(simple_prompt, '', '') -replace( - simple_prompt, - 'Required="@Required"\n Lines=', - 'Required="@Required"\n Validation="@(new Func(ValidateValue))"\n Lines=', -) -replace( - simple_prompt, - ''' private IEnumerable Validate() - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(_value)) - { - _error = "Please enter a value."; - yield return _error; - } - } -''', - ''' private string? ValidateValue(string? value) - { - _error = null; - if (Required && string.IsNullOrWhiteSpace(value)) - return _error = "Please enter a value."; - - return null; - } -''', -) -replace(simple_prompt, "await _form.Validate();", "await _form.ValidateAsync();") - -add_pinned = "TruthGate-Web/TruthGate-Web/Components/Shared/AddPinnedItemDialog.razor" -replace( - add_pinned, - '', - '', -) - -add_ipns = "TruthGate-Web/TruthGate-Web/Components/Shared/AddOrEditIpnsDialog.razor" -replace( - add_ipns, - '', - '', -) -replace( - add_ipns, - 'Required="true"\n Disabled="_isEdit" />', - 'Required="true"\n Validation="@(new Func(ValidateName))"\n Disabled="_isEdit" />', -) -replace( - add_ipns, - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateName))"', - 'Label="IPNS Key (k51… or /ipns/k51…)"\n @bind-Value="_model.Key"\n Immediate="true"\n Required="true"\n Validation="@(new Func(ValidateKey))"', -) -replace( - add_ipns, - ''' private IEnumerable Validate() - { - _error = null; - - if (string.IsNullOrWhiteSpace(_model.Name)) - yield return "Name is required."; - if (string.IsNullOrWhiteSpace(_model.Key)) - yield return "IPNS key is required."; - - var leaf = IpfsGateway.ToSafeLeaf(_model.Name); - if (leaf is null) - yield return "Invalid name."; - if (!_model.Key.StartsWith("k51") && !_model.Key.StartsWith("/ipns/")) - yield return "IPNS Key should look like k51… or /ipns/k51…"; - } -''', - ''' private string? ValidateName(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "Name is required."; - - return IpfsGateway.ToSafeLeaf(value) is null ? "Invalid name." : null; - } - - private static string? ValidateKey(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - return "IPNS key is required."; - - return value.StartsWith("k51", StringComparison.OrdinalIgnoreCase) || - value.StartsWith("/ipns/k51", StringComparison.OrdinalIgnoreCase) - ? null - : "IPNS Key should look like k51… or /ipns/k51…"; - } -''', -) -replace(add_ipns, "private async void Submit()", "private async Task Submit()") -replace(add_ipns, "await _form.Validate();", "await _form.ValidateAsync();") -replace( - add_ipns, - '', - '', -) - -search_ip = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Shared/SearchIpDialog.razor" -replace(search_ip, '', '') -replace( - search_ip, - 'Required="true"\n @onkeydown=', - 'Required="true"\n Validation="@(new Func(ValidateIp))"\n @onkeydown=', -) -replace( - search_ip, - ''' private IEnumerable Validate() - { - _error = null; - if (string.IsNullOrWhiteSpace(_ip)) - { - _error = "Please enter an IP address."; - yield return _error; - yield break; - } - if (!IPAddress.TryParse(_ip, out _)) - { - _error = "Invalid IP format (v4 or v6 required)."; - yield return _error; - } - } -''', - ''' private string? ValidateIp(string? value) - { - _error = null; - if (string.IsNullOrWhiteSpace(value)) - return _error = "Please enter an IP address."; - - if (!IPAddress.TryParse(value, out _)) - return _error = "Invalid IP format (v4 or v6 required)."; - - return null; - } -''', -) -replace(search_ip, "await _form.Validate();", "await _form.ValidateAsync();") -replace(search_ip, 'Sortable="false"', 'SortMode="SortMode.None"') - -ip_bans = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/IpBansPage.razor" -replace(ip_bans, 'Sortable="false"', 'SortMode="SortMode.None"') - -domains = "TruthGate-Web/TruthGate-Web/Components/Pages/Settings/Domains.razor" -replace( - domains, - 'Class="truncate-text" Title="@context.RedirectUrl"', - 'Class="truncate-text" title="@context.RedirectUrl"', -) -replace( - domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Link"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Link"', -) -replace( - domains, - 'Color="Color.Primary" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Fingerprint"', - 'Color="Color.Primary" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Fingerprint"', -) From 39adf11c5f9ac24782dd90e57c6cc4b74105631b Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:14:09 -0400 Subject: [PATCH 29/98] Document explicit TLS test restore --- .github/workflows/tls-lifecycle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 386d0bf..3f1191b 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -26,6 +26,7 @@ jobs: - name: Restore solution run: dotnet restore TruthGate-IPFS.sln + # This test project is intentionally outside TruthGate-IPFS.sln, so restore it explicitly. - name: Restore TLS tests run: dotnet restore TruthGate-Web/TruthGate-Web.Tests/TruthGate-Web.Tests.csproj From 00f19e8d24318c4fa468e4665f79a9da5a657860 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:57:31 -0400 Subject: [PATCH 30/98] Add multi-stage Docker appliance image --- Dockerfile | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dff6f72 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,110 @@ +# syntax=docker/dockerfile:1.7 + +ARG DOTNET_VERSION=10.0 +ARG KUBO_VERSION=v0.42.0 +ARG TRUTHGATE_UID=1000 +ARG TRUTHGATE_GID=1000 + +FROM ipfs/kubo:${KUBO_VERSION} AS kubo + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +WORKDIR /src + +COPY TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TruthGate-Web/TruthGate-Web/ +COPY TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj TruthGate-Web/TruthGate-Web.Client/ +RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + +COPY . . +RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + --configuration Release \ + --no-restore \ + --output /out \ + /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-resolute AS runtime-base +ARG TRUTHGATE_UID +ARG TRUTHGATE_GID + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gosu \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid "${TRUTHGATE_GID}" truthgate \ + && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /usr/sbin/nologin truthgate \ + && mkdir -p /home/truthgate/.aspnet \ + && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys + +COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint +COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck + +ENV ASPNETCORE_ENVIRONMENT=Production \ + DOTNET_ENVIRONMENT=Production \ + DOTNET_NOLOGO=true \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true \ + HOME=/home/truthgate \ + IPFS_PATH=/data/ipfs/repo \ + TMPDIR=/run/truthgate \ + TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ + TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ + TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_STATE_PATH=/data/truthgate/state \ + TRUTHGATE_HEALTH_URL=https://127.0.0.1:443/ + +EXPOSE 80 443 4001/tcp 4001/udp + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/truthgate-entrypoint"] +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD ["/usr/local/bin/truthgate-healthcheck"] + +FROM runtime-base AS production +WORKDIR /app +COPY --from=build /out . +CMD ["production"] + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS development +ARG TRUTHGATE_UID +ARG TRUTHGATE_GID + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gosu \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid "${TRUTHGATE_GID}" truthgate \ + && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /bin/bash truthgate \ + && mkdir -p /workspace /home/truthgate/.aspnet \ + && chown truthgate:truthgate /workspace \ + && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys + +COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint +COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck + +ENV ASPNETCORE_ENVIRONMENT=Development \ + DOTNET_ENVIRONMENT=Development \ + DOTNET_CLI_HOME=/tmp/dotnet \ + DOTNET_NOLOGO=true \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true \ + HOME=/home/truthgate \ + IPFS_PATH=/data/ipfs/repo \ + TMPDIR=/run/truthgate \ + TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ + TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ + TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_DEV_PROJECT=/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + TRUTHGATE_STATE_PATH=/data/truthgate/state \ + TRUTHGATE_HEALTH_URL=http://127.0.0.1:80/ + +WORKDIR /workspace +EXPOSE 80 4001/tcp 4001/udp +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/truthgate-entrypoint"] +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD ["/usr/local/bin/truthgate-healthcheck"] +CMD ["development"] From 56e8041955626a018db49c0a6f25a161bfbd74d2 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:57:48 -0400 Subject: [PATCH 31/98] Add appliance process supervisor --- docker/entrypoint.sh | 173 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 docker/entrypoint.sh diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..ed09d1e --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +mode="${1:-production}" +truthgate_user="truthgate" +truthgate_group="truthgate" + +log() { + printf '[truthgate] %s\n' "$*" +} + +fail() { + printf '[truthgate] ERROR: %s\n' "$*" >&2 + exit 1 +} + +as_truthgate() { + gosu "${truthgate_user}:${truthgate_group}" "$@" +} + +require_absolute_path() { + local name="$1" + local value="$2" + [[ "${value}" = /* ]] || fail "${name} must be an absolute path; received '${value}'." +} + +: "${IPFS_PATH:=/data/ipfs/repo}" +: "${TRUTHGATE_CONFIG_PATH:=/data/truthgate/config/config.json}" +: "${TRUTHGATE_DATABASE_PATH:=/data/truthgate/database}" +: "${TRUTHGATE_CERT_PATH:=/data/truthgate/certificates}" +: "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" +: "${TMPDIR:=/run/truthgate}" + +export IPFS_PATH TRUTHGATE_CONFIG_PATH TRUTHGATE_DATABASE_PATH +export TRUTHGATE_CERT_PATH TRUTHGATE_STATE_PATH TMPDIR + +require_absolute_path IPFS_PATH "${IPFS_PATH}" +require_absolute_path TRUTHGATE_CONFIG_PATH "${TRUTHGATE_CONFIG_PATH}" +require_absolute_path TRUTHGATE_DATABASE_PATH "${TRUTHGATE_DATABASE_PATH}" +require_absolute_path TRUTHGATE_CERT_PATH "${TRUTHGATE_CERT_PATH}" +require_absolute_path TRUTHGATE_STATE_PATH "${TRUTHGATE_STATE_PATH}" +require_absolute_path TMPDIR "${TMPDIR}" + +config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" +blocks_directory="${IPFS_PATH}/blocks" +data_protection_directory="/data/truthgate/secrets/data-protection-keys" + +install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ + "${config_directory}" \ + "${TRUTHGATE_DATABASE_PATH}" \ + "${TRUTHGATE_CERT_PATH}" \ + "${TRUTHGATE_STATE_PATH}" \ + "${data_protection_directory}" \ + "${IPFS_PATH}" \ + "${blocks_directory}" \ + "${TMPDIR}" + +# The default Compose layout mounts the repo and blockstore separately. Changing +# ownership on the mount roots is cheap and avoids recursively walking a large +# existing blockstore on every container start. +chown "${truthgate_user}:${truthgate_group}" \ + "${IPFS_PATH}" \ + "${blocks_directory}" \ + "${config_directory}" \ + "${TRUTHGATE_DATABASE_PATH}" \ + "${TRUTHGATE_CERT_PATH}" \ + "${TRUTHGATE_STATE_PATH}" \ + "${data_protection_directory}" \ + "${TMPDIR}" + +bootstrap_password_file="${TRUTHGATE_STATE_PATH}/bootstrap-admin-password" +if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then + if [[ -n "${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD:-}" ]]; then + bootstrap_password="${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD}" + elif [[ -s "${bootstrap_password_file}" ]]; then + bootstrap_password="$(<"${bootstrap_password_file}")" + else + bootstrap_password="$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')" + umask 077 + printf '%s' "${bootstrap_password}" > "${bootstrap_password_file}" + chown "${truthgate_user}:${truthgate_group}" "${bootstrap_password_file}" + fi + + export TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD="${bootstrap_password}" + log "First-run administrator account: admin" + log "First-run administrator password: ${bootstrap_password}" + log "The password is retained at ${bootstrap_password_file} until the TruthGate config is persisted." +else + rm -f "${bootstrap_password_file}" + unset TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD || true +fi + +if [[ ! -s "${IPFS_PATH}/config" ]]; then + log "Initializing a new Kubo repository at ${IPFS_PATH}." + as_truthgate ipfs init +else + log "Using the existing Kubo repository at ${IPFS_PATH}." +fi + +# TruthGate talks to Kubo over loopback. The RPC API is intentionally not +# published by Compose; the gateway is likewise kept private behind TruthGate. +as_truthgate ipfs config Addresses.API /ip4/127.0.0.1/tcp/5001 +as_truthgate ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/9010 + +log "Starting Kubo with automatic repository migrations enabled." +as_truthgate ipfs daemon --migrate=true & +ipfs_pid=$! + +stop_children() { + trap - TERM INT + + if [[ -n "${app_pid:-}" ]] && kill -0 "${app_pid}" 2>/dev/null; then + kill -TERM "${app_pid}" 2>/dev/null || true + fi + + if kill -0 "${ipfs_pid}" 2>/dev/null; then + kill -TERM "${ipfs_pid}" 2>/dev/null || true + fi + + [[ -z "${app_pid:-}" ]] || wait "${app_pid}" 2>/dev/null || true + wait "${ipfs_pid}" 2>/dev/null || true +} + +trap 'stop_children; exit 143' TERM +trap 'stop_children; exit 130' INT + +kubo_ready=false +for _ in $(seq 1 120); do + if as_truthgate ipfs --api=/ip4/127.0.0.1/tcp/5001 id >/dev/null 2>&1; then + kubo_ready=true + break + fi + + if ! kill -0 "${ipfs_pid}" 2>/dev/null; then + wait "${ipfs_pid}" || true + fail "Kubo exited before its RPC API became ready." + fi + + sleep 1 +done + +[[ "${kubo_ready}" == true ]] || fail "Kubo did not become ready within 120 seconds." +log "Kubo is ready." + +case "${mode}" in + production) + [[ -f /app/TruthGate-Web.dll ]] || fail "Production application was not found at /app/TruthGate-Web.dll." + log "Starting TruthGate in production mode." + as_truthgate dotnet /app/TruthGate-Web.dll & + ;; + development) + project="${TRUTHGATE_DEV_PROJECT:-/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj}" + [[ -f "${project}" ]] || fail "Development project was not found at ${project}. Is the repository mounted at /workspace?" + export ASPNETCORE_URLS="${ASPNETCORE_URLS:-http://0.0.0.0:80}" + export DOTNET_USE_POLLING_FILE_WATCHER="${DOTNET_USE_POLLING_FILE_WATCHER:-1}" + log "Starting TruthGate with dotnet watch (${project})." + as_truthgate dotnet watch --project "${project}" run --no-launch-profile --urls "${ASPNETCORE_URLS}" & + ;; + *) + fail "Unknown run mode '${mode}'. Expected 'production' or 'development'." + ;; +esac + +app_pid=$! + +set +e +wait -n "${ipfs_pid}" "${app_pid}" +exit_code=$? +set -e + +log "A managed process exited with status ${exit_code}; stopping the remaining process." +stop_children +exit "${exit_code}" From c74db8ad685c4246ad99697aeb3e5381bfe63644 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:57:55 -0400 Subject: [PATCH 32/98] Add container health check --- docker/healthcheck.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docker/healthcheck.sh diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 0000000..4172a47 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ipfs --api=/ip4/127.0.0.1/tcp/5001 diag healthy >/dev/null + +health_url="${TRUTHGATE_HEALTH_URL:-https://127.0.0.1:443/}" +http_status="$(curl \ + --silent \ + --show-error \ + --insecure \ + --max-time 5 \ + --output /dev/null \ + --write-out '%{http_code}' \ + "${health_url}")" + +[[ "${http_status}" =~ ^[0-9]{3}$ ]] +(( 10#${http_status} >= 200 && 10#${http_status} < 500 )) From 806dfd6496f5819ae370963ce6b2dfddc034d3e1 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:05 -0400 Subject: [PATCH 33/98] Add production Compose appliance --- compose.yaml | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 compose.yaml diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..dfee427 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,58 @@ +name: truthgate + +services: + truthgate: + image: ${TRUTHGATE_IMAGE:-ghcr.io/magiccodingman/truthgate-ipfs:master} + build: + context: . + dockerfile: Dockerfile + target: production + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + restart: unless-stopped + read_only: true + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Production + DOTNET_ENVIRONMENT: Production + IPFS_PATH: /data/ipfs/repo + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_HEALTH_URL: https://127.0.0.1:443/ + ports: + - "${TRUTHGATE_HTTP_PORT:-80}:80/tcp" + - "${TRUTHGATE_HTTPS_PORT:-443}:443/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s From 82a214298c5666ccae447425517faeb9272b18e6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:13 -0400 Subject: [PATCH 34/98] Add Docker development override --- compose.dev.yaml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 compose.dev.yaml diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..b1a9952 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,30 @@ +services: + truthgate: + image: truthgate-ipfs:dev + build: + target: development + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: !override + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + +volumes: + truthgate_nuget: From 983a8019431343ef8e515c4c5ba38eab77028d2c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:21 -0400 Subject: [PATCH 35/98] Add Docker environment template --- .env.example | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..659b47c --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Published image. Local builds use the same Compose file and can override this. +TRUTHGATE_IMAGE=ghcr.io/magiccodingman/truthgate-ipfs:master + +# Build inputs. Both official base images are multi-platform. +DOTNET_VERSION=10.0 +KUBO_VERSION=v0.42.0 +TRUTHGATE_UID=1000 +TRUTHGATE_GID=1000 + +# Host ports. +TRUTHGATE_HTTP_PORT=80 +TRUTHGATE_HTTPS_PORT=443 +TRUTHGATE_DEV_HTTP_PORT=8080 +IPFS_SWARM_PORT=4001 + +# Persistent host paths. The blockstore is always separate so it can later be +# moved to different storage without changing the container's Kubo layout. +TRUTHGATE_DATA_HOST_PATH=./data/truthgate +IPFS_REPO_HOST_PATH=./data/ipfs/repo +IPFS_BLOCKS_HOST_PATH=./data/ipfs/blocks + +# Certificate behavior. +TRUTHGATE_ACME_STAGING=false +# Optional comma-separated IP addresses for the self-signed fallback cert. +TRUTHGATE_CERT_IPS= From 1c6c54033c23c668b315b612d930bb6dc0b89067 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:28 -0400 Subject: [PATCH 36/98] Add Docker build exclusions --- .dockerignore | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b354484 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.github +.idea +.vs +.env +.env.* +!.env.example +**/bin +**/obj +**/TestResults +**/*.user +**/*.suo +coverage +artifacts +data +Test.TruthGate +TruthGate-Web/TruthGate-Web.Tests From 5d8a1a0cd860af71177e73fa7e0f61f4b1878e08 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:33 -0400 Subject: [PATCH 37/98] Ignore local appliance state --- data/.gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 data/.gitignore diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From a4615dd32078076db458aca5aa27c0dc95b08a59 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:44 -0400 Subject: [PATCH 38/98] Document Docker appliance architecture --- DOCKER.md | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 DOCKER.md diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..c02383e --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,130 @@ +# TruthGate Docker appliance + +TruthGate ships as one container containing the TruthGate ASP.NET application, +Kubo, and the matching `ipfs` CLI. Docker owns the software lifecycle; mounted +state survives image replacement. + +## Requirements + +- Docker Engine with the Compose v2 plugin +- Docker Compose 2.24.4 or newer for the development override's `!override` + merge directive +- TCP ports 80, 443, and 4001 plus UDP port 4001 available by default + +## Production quick start + +```bash +cp .env.example .env +docker compose up --build -d +``` + +Open `https://localhost` (the first connection uses TruthGate's self-signed +fallback certificate unless a configured domain has an issued certificate). + +On the first boot, retrieve the generated administrator password with: + +```bash +docker compose logs truthgate +``` + +The username is `admin`. Change its password in the TruthGate UI. Until the +configuration is persisted, the bootstrap password is also retained at +`data/truthgate/state/bootstrap-admin-password` with restrictive permissions. + +## Pulling the published image + +After the multi-platform image has been published by GitHub Actions: + +```bash +docker compose pull +docker compose up -d +``` + +The same `master` tag resolves to the correct `linux/amd64` or `linux/arm64` +image automatically. Replacing the container does not replace mounted state. + +## Persistent storage contract + +The default deployment keeps three host paths separate: + +```text +data/ +├── truthgate/ # config, database, certificates, secrets, app state +└── ipfs/ + ├── repo/ # Kubo identity, config, datastore, keystore, repo version + └── blocks/ # Kubo block files +``` + +They are mounted as: + +```text +/data/truthgate +/data/ipfs/repo +/data/ipfs/repo/blocks +``` + +Kubo still sees a conventional `$IPFS_PATH/blocks` directory, while the host +can later relocate only the blockstore by changing `IPFS_BLOCKS_HOST_PATH`. +No network filesystem, FUSE, JuiceFS, mount management, or mount health logic +is included in this initial implementation. + +The software inside the image is disposable. Backups should target the mounted +state, not the .NET runtime, Kubo binary, application binaries, or build output. + +## Kubo startup and migrations + +The entrypoint initializes Kubo only when `/data/ipfs/repo/config` is absent. +Existing repositories are preserved. Every start uses: + +```bash +ipfs daemon --migrate=true +``` + +This lets Kubo perform supported repository migrations when a newer image is +started against older persistent data. The RPC API listens only on container +loopback at port 5001, and the Kubo HTTP gateway listens only on loopback at +port 9010. Neither is published to the host. + +## Development + +Development uses the same Dockerfile and base Compose service: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +The override changes the image target to the .NET SDK development stage, +bind-mounts the repository at `/workspace`, adds a persistent NuGet cache, and +runs the web project with `dotnet watch`. TruthGate is available at +`http://localhost:8080` by default. + +Kubo initialization, repository migrations, persistent paths, startup order, +and process supervision are shared with production. This prevents development +from silently using a different node layout. + +Rider can attach to the running development container or use the container's +SDK. The source directory is mounted directly, so normal edits trigger +`dotnet watch`. + +## Configuration + +All ordinary host-facing settings live in `.env`. Important values include: + +- `TRUTHGATE_IMAGE` +- `KUBO_VERSION` +- `TRUTHGATE_HTTP_PORT` +- `TRUTHGATE_HTTPS_PORT` +- `IPFS_SWARM_PORT` +- `TRUTHGATE_DATA_HOST_PATH` +- `IPFS_REPO_HOST_PATH` +- `IPFS_BLOCKS_HOST_PATH` + +Use absolute host paths for advanced deployments. Relative defaults are rooted +at the directory containing `compose.yaml`. + +## Process model + +`tini` is PID 1. The entrypoint starts Kubo first, waits for its RPC API, then +starts TruthGate. If either process exits, the entrypoint terminates the other +and exits so Docker's restart policy can recover the appliance. SIGTERM and +SIGINT are forwarded to both processes for graceful shutdown. From 1769e83f8b7612059340ff40b8d8ca184fe553b4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:58:54 -0400 Subject: [PATCH 39/98] Document Docker quick start --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bb26024..ef04aca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # TruthGate - > The Secure, Self-Hosted Edge Gateway IPFS Always Needed, with Logins, API Keys, GUI Control, and Web3 Site Publishing. > **[truthgate.io](https://truthgate.io)** for full docs, guides, and live demos. @@ -19,17 +18,48 @@ --- +## Docker quick start + +TruthGate, Kubo, and the matching `ipfs` CLI run together as one appliance +container. Application state, Kubo repository metadata, and Kubo blocks are +persisted separately by default. + +```bash +cp .env.example .env +docker compose up --build -d +docker compose logs truthgate +``` + +The first-start logs contain a generated password for the `admin` account. +Open `https://localhost`, accept the temporary self-signed fallback certificate, +and change the password. + +For the full persistence contract, image update flow, ARM64/AMD64 publishing, +and Docker-based development setup, see **[DOCKER.md](DOCKER.md)**. + +Development with hot reload uses the production definition plus a small +override: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +Then open `http://localhost:8080`. + +--- + ## What Is It? TruthGate is a **secure edge layer for IPFS nodes**. -Think **Netlify, but for IPFS**, self-hosted, login-protected, and actually yours. +Think **Netlify, but for IPFS**, self-hosted, login-protected, and actually yours. + +It solves the problems developers hit with IPFS: -It solves the problems developers hit with IPFS: -- Node exposure risks -- SSL/domain linking headaches -- CLI-only publishing -- Zero protection for `/api` or `/webui` routes -- Reliance on public gateways +- Node exposure risks +- SSL/domain linking headaches +- CLI-only publishing +- Zero protection for `/api` or `/webui` routes +- Reliance on public gateways [See how it works](https://truthgate.io) @@ -37,15 +67,15 @@ It solves the problems developers hit with IPFS: ## Get Started -Head to **[truthgate.io](https://truthgate.io)** for installation, configuration, and publishing guides. +Head to **[truthgate.io](https://truthgate.io)** for installation, configuration, and publishing guides. --- ## A Note from the Creator -I built this out of frustration. +I built this out of frustration. -I wanted a way to serve Web3-native apps that *actually worked*, securely, reliably, and without selling my soul to centralized hosts. Now it’s real. +I wanted a way to serve Web3-native apps that *actually worked*, securely, reliably, and without selling my soul to centralized hosts. Now it’s real. If you’ve ever wrestled with IPFS routing, SSL certs, or gateway hacks just to get your site online, **TruthGate is for you.** @@ -55,11 +85,11 @@ If you’ve ever wrestled with IPFS routing, SSL certs, or gateway hacks just to Pull requests welcome. Stars encourage me. -Issues are sacred. +Issues are sacred. -Let’s fix decentralized hosting together. +Let’s fix decentralized hosting together. -[truthgate.io](https://truthgate.io) +[truthgate.io](https://truthgate.io) --- From 539a64a05cb307d272603607ef5f2a6412f6d32d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:59:08 -0400 Subject: [PATCH 40/98] Generate a secure Docker bootstrap admin password --- TruthGate-Web/TruthGate-Web/Models/Config.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Models/Config.cs b/TruthGate-Web/TruthGate-Web/Models/Config.cs index 4a88011..f5e6608 100644 --- a/TruthGate-Web/TruthGate-Web/Models/Config.cs +++ b/TruthGate-Web/TruthGate-Web/Models/Config.cs @@ -1,8 +1,12 @@ -namespace TruthGate_Web.Models +using TruthGate_Web.Utils; + +namespace TruthGate_Web.Models { public class Config { public const string DefaultAdminHash = "/EUxvODjrpkTKnal6nVEAh2m+52H4OgXGEBLcE3xilcgZ8gbeE5ay/CfzYr9PCJ0"; + public const string BootstrapAdminPasswordEnvironmentVariable = "TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD"; + private List? _users; public IpnsWildCardSubDomain IpnsWildCardSubDomain { get; set; } public List Domains { get; set; } = new List(); @@ -29,7 +33,7 @@ public List Users _users.Add(new UserAccount { UserName = "admin", - PasswordHashed = DefaultAdminHash + PasswordHashed = CreateInitialAdminHash() }); } @@ -41,6 +45,14 @@ public List Users _users = value is null ? new List() : new List(value); } } + + private static string CreateInitialAdminHash() + { + var bootstrapPassword = Environment.GetEnvironmentVariable(BootstrapAdminPasswordEnvironmentVariable); + return string.IsNullOrWhiteSpace(bootstrapPassword) + ? DefaultAdminHash + : StringHasher.HashString(bootstrapPassword); + } } public class IpnsWildCardSubDomain @@ -49,7 +61,7 @@ public class IpnsWildCardSubDomain public string UseSSL { get; set; } = "true"; } - public class EdgeDomain + public class EdgeDomain { public string Domain { get; set; } = ""; public string UseSSL { get; set; } = "false"; @@ -75,6 +87,7 @@ public class ApiKey public string Name { get; set; } public string KeyHashed { get; set; } } + public class UserAccount { public string UserName { get; set; } From e691336a822a25a8d3a93247bcf9ee77b689b0f0 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:59:16 -0400 Subject: [PATCH 41/98] Separate persistent database storage --- .../Security/RateLimiterRegistration.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs b/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs index 7da9060..d9141a8 100644 --- a/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs +++ b/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs @@ -1,4 +1,4 @@ -using TruthGate_Web.Middleware; +using TruthGate_Web.Middleware; using TruthGate_Web.Services; using Microsoft.EntityFrameworkCore; @@ -12,20 +12,38 @@ public static IServiceCollection AddTruthGateRateLimiter( { services.AddDbContextFactory((sp, b) => { - // Safe to resolve dependencies here var cfg = sp.GetRequiredService(); - var tet = cfg.Get(); - // Figure out SQLite location from cfg.ConfigPath (directory portion) - var dir = Path.GetDirectoryName(cfg.ConfigPath) ?? AppContext.BaseDirectory; - Directory.CreateDirectory(dir); - var dbPath = Path.Combine(dir, "ratelimiter.db"); + var configuredDatabasePath = Environment.GetEnvironmentVariable("TRUTHGATE_DATABASE_PATH"); + + string dbPath; + if (string.IsNullOrWhiteSpace(configuredDatabasePath)) + { + // Preserve the historical behavior for non-container installations. + var configDirectory = Path.GetDirectoryName(cfg.ConfigPath) ?? AppContext.BaseDirectory; + Directory.CreateDirectory(configDirectory); + dbPath = Path.Combine(configDirectory, "ratelimiter.db"); + } + else + { + var resolved = Path.GetFullPath(configuredDatabasePath); + if (string.Equals(Path.GetExtension(resolved), ".db", StringComparison.OrdinalIgnoreCase)) + { + Directory.CreateDirectory(Path.GetDirectoryName(resolved)!); + dbPath = resolved; + } + else + { + Directory.CreateDirectory(resolved); + dbPath = Path.Combine(resolved, "ratelimiter.db"); + } + } + var cs = connectionString ?? $"Data Source={dbPath};Cache=Shared"; b.UseSqlite(cs); b.EnableSensitiveDataLogging(false); }); - services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); @@ -37,5 +55,4 @@ public static IServiceCollection AddTruthGateRateLimiter( public static IApplicationBuilder UseTruthGateRateLimiter(this IApplicationBuilder app) => app.UseMiddleware(); } - } From e847ca57d157ec89729fcefcb334f9bee92b6291 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:59:30 -0400 Subject: [PATCH 42/98] Add multi-platform Docker CI and publishing --- .github/workflows/docker.yml | 123 +++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 .github/workflows/docker.yml diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..44be8e6 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,123 @@ +name: Docker appliance + +on: + pull_request: + push: + branches: + - master + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE_NAME: ghcr.io/magiccodingman/truthgate-ipfs + +jobs: + validate-compose: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Validate production Compose + run: docker compose -f compose.yaml config + - name: Validate development override + run: docker compose -f compose.yaml -f compose.dev.yaml config + + smoke-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build production image (AMD64) + run: docker build --target production --tag truthgate-ipfs:test . + + - name: Build development image (AMD64) + run: docker build --target development --tag truthgate-ipfs:dev-test . + + - name: Start appliance and wait for health + shell: bash + run: | + set -Eeuo pipefail + trap 'docker compose logs --no-color || true; docker compose down --remove-orphans || true' EXIT + + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks + + TRUTHGATE_IMAGE=truthgate-ipfs:test \ + TRUTHGATE_HTTP_PORT=18080 \ + TRUTHGATE_HTTPS_PORT=18443 \ + IPFS_SWARM_PORT=14001 \ + TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate \ + IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo \ + IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks \ + docker compose up --detach --no-build + + for attempt in $(seq 1 60); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + if [[ "${status}" == "healthy" ]]; then + docker compose logs --no-color + exit 0 + fi + + if [[ "${status}" == "unhealthy" ]]; then + echo "TruthGate became unhealthy." + exit 1 + fi + + sleep 5 + done + + echo "TruthGate did not become healthy in time." + exit 1 + + build-and-publish: + needs: + - validate-compose + - smoke-test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }} + type=sha,prefix=sha- + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and optionally publish AMD64 + ARM64 + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + sbom: true From 58cd02042aa3f4c6f37c89efa837bf8ea4d910fe Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:02:21 -0400 Subject: [PATCH 43/98] Preserve Docker build diagnostics --- .github/workflows/docker.yml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 44be8e6..c1fd7d5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -36,10 +36,34 @@ jobs: - uses: actions/checkout@v4 - name: Build production image (AMD64) - run: docker build --target production --tag truthgate-ipfs:test . + shell: bash + run: | + set -o pipefail + docker build --progress=plain --target production --tag truthgate-ipfs:test . 2>&1 \ + | tee production-image-build.log + + - name: Upload production build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: production-image-build + path: production-image-build.log + if-no-files-found: error - name: Build development image (AMD64) - run: docker build --target development --tag truthgate-ipfs:dev-test . + shell: bash + run: | + set -o pipefail + docker build --progress=plain --target development --tag truthgate-ipfs:dev-test . 2>&1 \ + | tee development-image-build.log + + - name: Upload development build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: development-image-build + path: development-image-build.log + if-no-files-found: ignore - name: Start appliance and wait for health shell: bash From 4ca64b417c2da63fa3ea3a7ecd0a7766ea56660f Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:03:51 -0400 Subject: [PATCH 44/98] Reuse existing base image groups safely --- Dockerfile | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index dff6f72..d739dd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,8 +32,12 @@ RUN apt-get update \ gosu \ tini \ && rm -rf /var/lib/apt/lists/* \ - && groupadd --gid "${TRUTHGATE_GID}" truthgate \ - && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /usr/sbin/nologin truthgate \ + && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ + && if existing_user="$(getent passwd "${TRUTHGATE_UID}" | cut -d: -f1)" && [ -n "${existing_user}" ]; then \ + usermod --login truthgate --home /home/truthgate --move-home --shell /usr/sbin/nologin "${existing_user}"; \ + else \ + useradd --uid "${TRUTHGATE_UID}" --gid "${TRUTHGATE_GID}" --create-home --shell /usr/sbin/nologin truthgate; \ + fi \ && mkdir -p /home/truthgate/.aspnet \ && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys @@ -77,10 +81,14 @@ RUN apt-get update \ gosu \ tini \ && rm -rf /var/lib/apt/lists/* \ - && groupadd --gid "${TRUTHGATE_GID}" truthgate \ - && useradd --uid "${TRUTHGATE_UID}" --gid truthgate --create-home --shell /bin/bash truthgate \ + && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ + && if existing_user="$(getent passwd "${TRUTHGATE_UID}" | cut -d: -f1)" && [ -n "${existing_user}" ]; then \ + usermod --login truthgate --home /home/truthgate --move-home --shell /bin/bash "${existing_user}"; \ + else \ + useradd --uid "${TRUTHGATE_UID}" --gid "${TRUTHGATE_GID}" --create-home --shell /bin/bash truthgate; \ + fi \ && mkdir -p /workspace /home/truthgate/.aspnet \ - && chown truthgate:truthgate /workspace \ + && chown "${TRUTHGATE_UID}:${TRUTHGATE_GID}" /workspace \ && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs From e0f63662622052a8f4e7588c1417811fa10eb124 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:04:17 -0400 Subject: [PATCH 45/98] Use the runtime user's numeric primary group --- docker/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index ed09d1e..6e53846 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail mode="${1:-production}" truthgate_user="truthgate" -truthgate_group="truthgate" +truthgate_group="$(id -g "${truthgate_user}")" log() { printf '[truthgate] %s\n' "$*" From 3ed41df14219468322cb8cf9d1f990d2b8ad9aa7 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:11:21 -0400 Subject: [PATCH 46/98] Cross-compile .NET efficiently for multi-arch images --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d739dd0..b446b17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,17 +7,22 @@ ARG TRUTHGATE_GID=1000 FROM ipfs/kubo:${KUBO_VERSION} AS kubo -FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +# Compile on the builder's native CPU while targeting the requested image +# architecture. This avoids running the full .NET build under QEMU for ARM64. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +ARG TARGETARCH WORKDIR /src COPY TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TruthGate-Web/TruthGate-Web/ COPY TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj TruthGate-Web/TruthGate-Web.Client/ -RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj --arch "${TARGETARCH}" COPY . . RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ --configuration Release \ + --arch "${TARGETARCH}" \ --no-restore \ + --no-self-contained \ --output /out \ /p:UseAppHost=false From 74b50b13f6aaf71beccb336d018a82086407d6d6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:17:41 -0400 Subject: [PATCH 47/98] Validate the appliance natively on AMD64 and ARM64 --- .github/workflows/docker.yml | 127 ++++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c1fd7d5..ca33b79 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,7 +22,7 @@ env: jobs: validate-compose: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Validate production Compose @@ -31,80 +31,130 @@ jobs: run: docker compose -f compose.yaml -f compose.dev.yaml config smoke-test: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - name: Build production image (AMD64) + - name: Build production image (${{ matrix.architecture }}) shell: bash run: | set -o pipefail - docker build --progress=plain --target production --tag truthgate-ipfs:test . 2>&1 \ - | tee production-image-build.log + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:test-${{ matrix.architecture }} \ + . 2>&1 | tee production-image-build-${{ matrix.architecture }}.log - name: Upload production build diagnostics if: always() uses: actions/upload-artifact@v4 with: - name: production-image-build - path: production-image-build.log + name: production-image-build-${{ matrix.architecture }} + path: production-image-build-${{ matrix.architecture }}.log if-no-files-found: error - - name: Build development image (AMD64) + - name: Build development image (${{ matrix.architecture }}) shell: bash run: | set -o pipefail - docker build --progress=plain --target development --tag truthgate-ipfs:dev-test . 2>&1 \ - | tee development-image-build.log + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target development \ + --tag truthgate-ipfs:dev-test-${{ matrix.architecture }} \ + . 2>&1 | tee development-image-build-${{ matrix.architecture }}.log - name: Upload development build diagnostics if: always() uses: actions/upload-artifact@v4 with: - name: development-image-build - path: development-image-build.log + name: development-image-build-${{ matrix.architecture }} + path: development-image-build-${{ matrix.architecture }}.log if-no-files-found: ignore - - name: Start appliance and wait for health + - name: Start, replace, and revalidate the appliance shell: bash run: | set -Eeuo pipefail - trap 'docker compose logs --no-color || true; docker compose down --remove-orphans || true' EXIT + + export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate + export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + wait_for_health() { + for attempt in $(seq 1 60); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + if [[ "${status}" == "healthy" ]]; then + return 0 + fi + + if [[ "${status}" == "unhealthy" ]]; then + echo "TruthGate became unhealthy." + return 1 + fi + + sleep 5 + done + + echo "TruthGate did not become healthy in time." + return 1 + } + + read_peer_id() { + docker exec truthgate \ + ipfs --api=/ip4/127.0.0.1/tcp/5001 id \ + | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' + } mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks - TRUTHGATE_IMAGE=truthgate-ipfs:test \ - TRUTHGATE_HTTP_PORT=18080 \ - TRUTHGATE_HTTPS_PORT=18443 \ - IPFS_SWARM_PORT=14001 \ - TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate \ - IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo \ - IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks \ docker compose up --detach --no-build + wait_for_health + + test -s .ci-data/ipfs/repo/config + test -s .ci-data/truthgate/state/bootstrap-admin-password + peer_id_before="$(read_peer_id)" + bootstrap_hash_before="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" - for attempt in $(seq 1 60); do - status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" - if [[ "${status}" == "healthy" ]]; then - docker compose logs --no-color - exit 0 - fi + # Replace the container while retaining only the documented bind-mounted + # state. The Kubo identity and first-run credential must remain stable. + docker compose down --remove-orphans + docker compose up --detach --no-build + wait_for_health - if [[ "${status}" == "unhealthy" ]]; then - echo "TruthGate became unhealthy." - exit 1 - fi + peer_id_after="$(read_peer_id)" + bootstrap_hash_after="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" - sleep 5 - done + test "${peer_id_before}" = "${peer_id_after}" + test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" - echo "TruthGate did not become healthy in time." - exit 1 + docker compose logs --no-color build-and-publish: + if: github.event_name != 'pull_request' needs: - validate-compose - smoke-test - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -113,7 +163,6 @@ jobs: - uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry - if: github.event_name != 'pull_request' uses: docker/login-action@v3 with: registry: ghcr.io @@ -132,13 +181,13 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - - name: Build and optionally publish AMD64 + ARM64 + - name: Publish AMD64 + ARM64 under one image tag uses: docker/build-push-action@v6 with: context: . target: production platforms: linux/amd64,linux/arm64 - push: ${{ github.event_name != 'pull_request' }} + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha From 07516190aa1210cb50a35d8af158bf812f58f0a4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:20:35 -0400 Subject: [PATCH 48/98] Read protected state inside the smoke-test container --- .github/workflows/docker.yml | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ca33b79..bffe3ea 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -84,6 +84,7 @@ jobs: - name: Start, replace, and revalidate the appliance shell: bash run: | + exec > >(tee runtime-smoke-${{ matrix.architecture }}.log) 2>&1 set -Eeuo pipefail export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} @@ -125,15 +126,23 @@ jobs: | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' } + read_bootstrap_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/state/bootstrap-admin-password \ + | cut -d' ' -f1 + } + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks docker compose up --detach --no-build wait_for_health - test -s .ci-data/ipfs/repo/config - test -s .ci-data/truthgate/state/bootstrap-admin-password + # Read protected state from inside the container. The host runner is + # intentionally unable to inspect the bootstrap secret directly. + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password peer_id_before="$(read_peer_id)" - bootstrap_hash_before="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" + bootstrap_hash_before="$(read_bootstrap_hash)" # Replace the container while retaining only the documented bind-mounted # state. The Kubo identity and first-run credential must remain stable. @@ -141,14 +150,24 @@ jobs: docker compose up --detach --no-build wait_for_health + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password peer_id_after="$(read_peer_id)" - bootstrap_hash_after="$(sha256sum .ci-data/truthgate/state/bootstrap-admin-password | cut -d' ' -f1)" + bootstrap_hash_after="$(read_bootstrap_hash)" test "${peer_id_before}" = "${peer_id_after}" test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" docker compose logs --no-color + - name: Upload runtime diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: runtime-smoke-${{ matrix.architecture }} + path: runtime-smoke-${{ matrix.architecture }}.log + if-no-files-found: ignore + build-and-publish: if: github.event_name != 'pull_request' needs: From 051d16e397a7500657658ba1932e577c889f0ad9 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:43:17 -0400 Subject: [PATCH 49/98] Add Rider-friendly merged development compose file --- compose.rider.yaml | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 compose.rider.yaml diff --git a/compose.rider.yaml b/compose.rider.yaml new file mode 100644 index 0000000..e33027c --- /dev/null +++ b/compose.rider.yaml @@ -0,0 +1,73 @@ +# Rider-friendly single-file development configuration. +# Keep this file synchronized with compose.yaml + compose.dev.yaml. +name: truthgate + +services: + truthgate: + image: truthgate-ipfs:dev + build: + context: . + dockerfile: Dockerfile + target: development + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + IPFS_PATH: /data/ipfs/repo + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s + +volumes: + truthgate_nuget: From 98412dbc8504407b3e11026f5025a50c33a90431 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:44:50 -0400 Subject: [PATCH 50/98] Add PowerShell Rider Compose generator --- docker/generate-rider-compose.ps1 | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docker/generate-rider-compose.ps1 diff --git a/docker/generate-rider-compose.ps1 b/docker/generate-rider-compose.ps1 new file mode 100644 index 0000000..ccf3070 --- /dev/null +++ b/docker/generate-rider-compose.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$composeFile = Join-Path $repositoryRoot 'compose.yaml' +$devComposeFile = Join-Path $repositoryRoot 'compose.dev.yaml' +$outputFile = Join-Path $repositoryRoot 'compose.rider.yaml' +$tempFile = Join-Path $repositoryRoot ('.compose.rider.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) + +try { + Push-Location $repositoryRoot + + & docker compose -f $composeFile -f $devComposeFile config | Set-Content -Path $tempFile -Encoding utf8 + + if ($LASTEXITCODE -ne 0) { + throw "docker compose config failed with exit code $LASTEXITCODE." + } + + if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { + throw 'Generated Compose file is empty.' + } + + Move-Item -Path $tempFile -Destination $outputFile -Force + Write-Host "Generated $outputFile" +} +finally { + if (Test-Path $tempFile) { + Remove-Item $tempFile -Force + } + + Pop-Location +} From 2fcdaea01c5cb113597a20d9a57dca54866abb31 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:44:57 -0400 Subject: [PATCH 51/98] Add shell Rider Compose generator --- docker/generate-rider-compose.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docker/generate-rider-compose.sh diff --git a/docker/generate-rider-compose.sh b/docker/generate-rider-compose.sh new file mode 100644 index 0000000..cff2252 --- /dev/null +++ b/docker/generate-rider-compose.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +compose_file="${repository_root}/compose.yaml" +dev_compose_file="${repository_root}/compose.dev.yaml" +output_file="${repository_root}/compose.rider.yaml" +temp_file="$(mktemp "${repository_root}/.compose.rider.XXXXXX.tmp")" + +cleanup() { + rm -f "${temp_file}" +} +trap cleanup EXIT + +cd "${repository_root}" + +docker compose \ + -f "${compose_file}" \ + -f "${dev_compose_file}" \ + config > "${temp_file}" + +if [[ ! -s "${temp_file}" ]]; then + echo "Generated Compose file is empty." >&2 + exit 1 +fi + +mv -f "${temp_file}" "${output_file}" +trap - EXIT + +echo "Generated ${output_file}" From c6b5f3a07d85c5a2cc625646c73f55befd1b0e09 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:45:14 -0400 Subject: [PATCH 52/98] Document Rider Compose generation --- DOCKER.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/DOCKER.md b/DOCKER.md index c02383e..ff7c476 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -106,6 +106,29 @@ Rider can attach to the running development container or use the container's SDK. The source directory is mounted directly, so normal edits trigger `dotnet watch`. +### Rider single-file Compose configuration + +Some Rider versions fail while discovering services from multiple Compose +files. `compose.rider.yaml` is the merged development configuration for that +case. Regenerate it from `compose.yaml` and `compose.dev.yaml` before launch. + +PowerShell 7 on Windows, Linux, or macOS: + +```powershell +pwsh -NoProfile -File ./docker/generate-rider-compose.ps1 +``` + +Bash on Linux or macOS: + +```bash +bash ./docker/generate-rider-compose.sh +``` + +Both scripts write to a temporary file and replace `compose.rider.yaml` only +after `docker compose config` succeeds. In Rider, use `compose.rider.yaml` as +the sole Compose file and add the appropriate command as an External Tool in +the run configuration's **Before launch** tasks. + ## Configuration All ordinary host-facing settings live in `.env`. Important values include: From aa8cd8790018a4964af2307c91a4b18c082e50fd Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:36 -0400 Subject: [PATCH 53/98] Rename merged development compose file --- compose.dev.generated.yaml | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 compose.dev.generated.yaml diff --git a/compose.dev.generated.yaml b/compose.dev.generated.yaml new file mode 100644 index 0000000..d8cbdd9 --- /dev/null +++ b/compose.dev.generated.yaml @@ -0,0 +1,73 @@ +# Generated single-file development configuration. +# Regenerate from compose.yaml + compose.dev.yaml; do not edit this file directly. +name: truthgate + +services: + truthgate: + image: truthgate-ipfs:dev + build: + context: . + dockerfile: Dockerfile + target: development + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + IPFS_PATH: /data/ipfs/repo + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s + +volumes: + truthgate_nuget: From 0f54cf91ff4c37ae1f4b69e3c0fae50626e26fe6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:43 -0400 Subject: [PATCH 54/98] Remove Rider-specific merged compose name --- compose.rider.yaml | 73 ---------------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 compose.rider.yaml diff --git a/compose.rider.yaml b/compose.rider.yaml deleted file mode 100644 index e33027c..0000000 --- a/compose.rider.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Rider-friendly single-file development configuration. -# Keep this file synchronized with compose.yaml + compose.dev.yaml. -name: truthgate - -services: - truthgate: - image: truthgate-ipfs:dev - build: - context: . - dockerfile: Dockerfile - target: development - args: - DOTNET_VERSION: ${DOTNET_VERSION:-10.0} - KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} - TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} - TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} - container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} - command: ["development"] - restart: "no" - read_only: false - working_dir: /workspace - security_opt: - - no-new-privileges:true - sysctls: - net.ipv4.ip_unprivileged_port_start: "0" - environment: - ASPNETCORE_ENVIRONMENT: Development - DOTNET_ENVIRONMENT: Development - ASPNETCORE_URLS: http://0.0.0.0:80 - DOTNET_USE_POLLING_FILE_WATCHER: "1" - IPFS_PATH: /data/ipfs/repo - TMPDIR: /run/truthgate - TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} - TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} - TRUTHGATE_CERT_PATH: /data/truthgate/certificates - TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json - TRUTHGATE_DATABASE_PATH: /data/truthgate/database - TRUTHGATE_STATE_PATH: /data/truthgate/state - TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj - TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ - ports: - - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/udp" - volumes: - - type: bind - source: . - target: /workspace - - type: volume - source: truthgate_nuget - target: /home/truthgate/.nuget/packages - - type: bind - source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} - target: /data/truthgate - - type: bind - source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} - target: /data/ipfs/repo - - type: bind - source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} - target: /data/ipfs/repo/blocks - tmpfs: - - /tmp:rw,noexec,nosuid,size=256m - - /run/truthgate:rw,noexec,nosuid,size=64m - healthcheck: - test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] - interval: 30s - timeout: 10s - start_period: 90s - retries: 3 - stop_grace_period: 45s - -volumes: - truthgate_nuget: From ddc144b363e2212eac29bd2ddf35015381a9a564 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:50 -0400 Subject: [PATCH 55/98] Generate generic merged development compose file --- docker/generate-rider-compose.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/generate-rider-compose.ps1 b/docker/generate-rider-compose.ps1 index ccf3070..f984077 100644 --- a/docker/generate-rider-compose.ps1 +++ b/docker/generate-rider-compose.ps1 @@ -4,8 +4,8 @@ Set-StrictMode -Version Latest $repositoryRoot = Split-Path -Parent $PSScriptRoot $composeFile = Join-Path $repositoryRoot 'compose.yaml' $devComposeFile = Join-Path $repositoryRoot 'compose.dev.yaml' -$outputFile = Join-Path $repositoryRoot 'compose.rider.yaml' -$tempFile = Join-Path $repositoryRoot ('.compose.rider.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) +$outputFile = Join-Path $repositoryRoot 'compose.dev.generated.yaml' +$tempFile = Join-Path $repositoryRoot ('.compose.dev.generated.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) try { Push-Location $repositoryRoot From bfa00d10877b5e9787d89181b74a60d757459689 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:46:57 -0400 Subject: [PATCH 56/98] Generate generic merged development compose file --- docker/generate-rider-compose.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/generate-rider-compose.sh b/docker/generate-rider-compose.sh index cff2252..5120707 100644 --- a/docker/generate-rider-compose.sh +++ b/docker/generate-rider-compose.sh @@ -4,8 +4,8 @@ set -Eeuo pipefail repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" compose_file="${repository_root}/compose.yaml" dev_compose_file="${repository_root}/compose.dev.yaml" -output_file="${repository_root}/compose.rider.yaml" -temp_file="$(mktemp "${repository_root}/.compose.rider.XXXXXX.tmp")" +output_file="${repository_root}/compose.dev.generated.yaml" +temp_file="$(mktemp "${repository_root}/.compose.dev.generated.XXXXXX.tmp")" cleanup() { rm -f "${temp_file}" From 98d8a731cad634185e4fcc3068a3b34e61d1575d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:47:16 -0400 Subject: [PATCH 57/98] Document generic generated development compose file --- DOCKER.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index ff7c476..2ea48f1 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -106,11 +106,12 @@ Rider can attach to the running development container or use the container's SDK. The source directory is mounted directly, so normal edits trigger `dotnet watch`. -### Rider single-file Compose configuration +### Generated single-file development configuration -Some Rider versions fail while discovering services from multiple Compose -files. `compose.rider.yaml` is the merged development configuration for that -case. Regenerate it from `compose.yaml` and `compose.dev.yaml` before launch. +Some tools and IDEs fail while discovering services from multiple Compose +files. `compose.dev.generated.yaml` is the fully merged development +configuration for that case. It is generated from `compose.yaml` and +`compose.dev.yaml` and should not be edited directly. PowerShell 7 on Windows, Linux, or macOS: @@ -124,10 +125,11 @@ Bash on Linux or macOS: bash ./docker/generate-rider-compose.sh ``` -Both scripts write to a temporary file and replace `compose.rider.yaml` only -after `docker compose config` succeeds. In Rider, use `compose.rider.yaml` as -the sole Compose file and add the appropriate command as an External Tool in -the run configuration's **Before launch** tasks. +Both scripts write to a temporary file and replace +`compose.dev.generated.yaml` only after `docker compose config` succeeds. In +Rider, use `compose.dev.generated.yaml` as the sole Compose file and add the +appropriate generator as an External Tool in the run configuration's +**Before launch** tasks. ## Configuration From 31d34812cbd947ff282248f3bf595cd6a7160375 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:23 -0400 Subject: [PATCH 58/98] Remove generated development compose workaround --- compose.dev.generated.yaml | 73 -------------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 compose.dev.generated.yaml diff --git a/compose.dev.generated.yaml b/compose.dev.generated.yaml deleted file mode 100644 index d8cbdd9..0000000 --- a/compose.dev.generated.yaml +++ /dev/null @@ -1,73 +0,0 @@ -# Generated single-file development configuration. -# Regenerate from compose.yaml + compose.dev.yaml; do not edit this file directly. -name: truthgate - -services: - truthgate: - image: truthgate-ipfs:dev - build: - context: . - dockerfile: Dockerfile - target: development - args: - DOTNET_VERSION: ${DOTNET_VERSION:-10.0} - KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} - TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} - TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} - container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} - command: ["development"] - restart: "no" - read_only: false - working_dir: /workspace - security_opt: - - no-new-privileges:true - sysctls: - net.ipv4.ip_unprivileged_port_start: "0" - environment: - ASPNETCORE_ENVIRONMENT: Development - DOTNET_ENVIRONMENT: Development - ASPNETCORE_URLS: http://0.0.0.0:80 - DOTNET_USE_POLLING_FILE_WATCHER: "1" - IPFS_PATH: /data/ipfs/repo - TMPDIR: /run/truthgate - TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} - TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} - TRUTHGATE_CERT_PATH: /data/truthgate/certificates - TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json - TRUTHGATE_DATABASE_PATH: /data/truthgate/database - TRUTHGATE_STATE_PATH: /data/truthgate/state - TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj - TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ - ports: - - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/tcp" - - "${IPFS_SWARM_PORT:-4001}:4001/udp" - volumes: - - type: bind - source: . - target: /workspace - - type: volume - source: truthgate_nuget - target: /home/truthgate/.nuget/packages - - type: bind - source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} - target: /data/truthgate - - type: bind - source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} - target: /data/ipfs/repo - - type: bind - source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} - target: /data/ipfs/repo/blocks - tmpfs: - - /tmp:rw,noexec,nosuid,size=256m - - /run/truthgate:rw,noexec,nosuid,size=64m - healthcheck: - test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] - interval: 30s - timeout: 10s - start_period: 90s - retries: 3 - stop_grace_period: 45s - -volumes: - truthgate_nuget: From 5ee2482606ee28cc4aedd64667298e611576d8c5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:28 -0400 Subject: [PATCH 59/98] Remove obsolete Rider compose generator --- docker/generate-rider-compose.ps1 | 32 ------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 docker/generate-rider-compose.ps1 diff --git a/docker/generate-rider-compose.ps1 b/docker/generate-rider-compose.ps1 deleted file mode 100644 index f984077..0000000 --- a/docker/generate-rider-compose.ps1 +++ /dev/null @@ -1,32 +0,0 @@ -$ErrorActionPreference = 'Stop' -Set-StrictMode -Version Latest - -$repositoryRoot = Split-Path -Parent $PSScriptRoot -$composeFile = Join-Path $repositoryRoot 'compose.yaml' -$devComposeFile = Join-Path $repositoryRoot 'compose.dev.yaml' -$outputFile = Join-Path $repositoryRoot 'compose.dev.generated.yaml' -$tempFile = Join-Path $repositoryRoot ('.compose.dev.generated.{0}.tmp' -f [Guid]::NewGuid().ToString('N')) - -try { - Push-Location $repositoryRoot - - & docker compose -f $composeFile -f $devComposeFile config | Set-Content -Path $tempFile -Encoding utf8 - - if ($LASTEXITCODE -ne 0) { - throw "docker compose config failed with exit code $LASTEXITCODE." - } - - if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { - throw 'Generated Compose file is empty.' - } - - Move-Item -Path $tempFile -Destination $outputFile -Force - Write-Host "Generated $outputFile" -} -finally { - if (Test-Path $tempFile) { - Remove-Item $tempFile -Force - } - - Pop-Location -} From b51a6f42521d89ce8a7a40a69201ce539e086117 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:35 -0400 Subject: [PATCH 60/98] Remove obsolete Rider compose generator --- docker/generate-rider-compose.sh | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 docker/generate-rider-compose.sh diff --git a/docker/generate-rider-compose.sh b/docker/generate-rider-compose.sh deleted file mode 100644 index 5120707..0000000 --- a/docker/generate-rider-compose.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -compose_file="${repository_root}/compose.yaml" -dev_compose_file="${repository_root}/compose.dev.yaml" -output_file="${repository_root}/compose.dev.generated.yaml" -temp_file="$(mktemp "${repository_root}/.compose.dev.generated.XXXXXX.tmp")" - -cleanup() { - rm -f "${temp_file}" -} -trap cleanup EXIT - -cd "${repository_root}" - -docker compose \ - -f "${compose_file}" \ - -f "${dev_compose_file}" \ - config > "${temp_file}" - -if [[ ! -s "${temp_file}" ]]; then - echo "Generated Compose file is empty." >&2 - exit 1 -fi - -mv -f "${temp_file}" "${output_file}" -trap - EXIT - -echo "Generated ${output_file}" From 461dbda00de73aeea86b4af3e7b728e89faef7b5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:58:50 -0400 Subject: [PATCH 61/98] Remove obsolete generated compose documentation --- DOCKER.md | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 2ea48f1..7a26849 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -102,34 +102,9 @@ Kubo initialization, repository migrations, persistent paths, startup order, and process supervision are shared with production. This prevents development from silently using a different node layout. -Rider can attach to the running development container or use the container's -SDK. The source directory is mounted directly, so normal edits trigger -`dotnet watch`. - -### Generated single-file development configuration - -Some tools and IDEs fail while discovering services from multiple Compose -files. `compose.dev.generated.yaml` is the fully merged development -configuration for that case. It is generated from `compose.yaml` and -`compose.dev.yaml` and should not be edited directly. - -PowerShell 7 on Windows, Linux, or macOS: - -```powershell -pwsh -NoProfile -File ./docker/generate-rider-compose.ps1 -``` - -Bash on Linux or macOS: - -```bash -bash ./docker/generate-rider-compose.sh -``` - -Both scripts write to a temporary file and replace -`compose.dev.generated.yaml` only after `docker compose config` succeeds. In -Rider, use `compose.dev.generated.yaml` as the sole Compose file and add the -appropriate generator as an External Tool in the run configuration's -**Before launch** tasks. +Rider can use `compose.yaml` followed by `compose.dev.yaml` directly in a Docker +Compose run configuration. The source directory is mounted directly, so normal +edits trigger `dotnet watch`. ## Configuration From 2ebef1a6453def1f15ffcc672dac6b1524fbd85d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:01:12 -0400 Subject: [PATCH 62/98] Fix Rider development container runtime paths --- compose.dev.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/compose.dev.yaml b/compose.dev.yaml index b1a9952..c540ebb 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -12,6 +12,11 @@ services: DOTNET_ENVIRONMENT: Development ASPNETCORE_URLS: http://0.0.0.0:80 DOTNET_USE_POLLING_FILE_WATCHER: "1" + DOTNET_CLI_HOME: /home/truthgate + HOME: /home/truthgate + NUGET_PACKAGES: /home/truthgate/.nuget/packages + NUGET_HTTP_CACHE_PATH: /home/truthgate/.nuget/http-cache + TINI_SUBREAPER: "1" TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ ports: !override @@ -25,6 +30,13 @@ services: - type: volume source: truthgate_nuget target: /home/truthgate/.nuget/packages + - type: volume + source: truthgate_nuget_http + target: /home/truthgate/.nuget/http-cache + tmpfs: !override + - /tmp:rw,noexec,nosuid,size=1g + - /run/truthgate:rw,noexec,nosuid,size=64m volumes: truthgate_nuget: + truthgate_nuget_http: From a93a8c40a84a3d12becfe48115c7b318629bc2e5 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:03:07 -0400 Subject: [PATCH 63/98] Fix development NuGet directory ownership --- docker/entrypoint.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 6e53846..e68ed61 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -55,6 +55,35 @@ install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ "${blocks_directory}" \ "${TMPDIR}" +# Named volumes are created as root-owned directories. Development tools such as +# NuGet run as the non-root truthgate user, so initialize the complete NuGet home +# (including its configuration directory) before dropping privileges. +if [[ "${mode}" == "development" ]]; then + : "${HOME:=/home/truthgate}" + : "${DOTNET_CLI_HOME:=${HOME}}" + : "${NUGET_PACKAGES:=${HOME}/.nuget/packages}" + : "${NUGET_HTTP_CACHE_PATH:=${HOME}/.nuget/http-cache}" + + export HOME DOTNET_CLI_HOME NUGET_PACKAGES NUGET_HTTP_CACHE_PATH + + nuget_root="${HOME}/.nuget" + nuget_config_directory="${nuget_root}/NuGet" + + install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ + "${HOME}" \ + "${nuget_root}" \ + "${nuget_config_directory}" \ + "${NUGET_PACKAGES}" \ + "${NUGET_HTTP_CACHE_PATH}" + + chown "${truthgate_user}:${truthgate_group}" \ + "${HOME}" \ + "${nuget_root}" \ + "${nuget_config_directory}" \ + "${NUGET_PACKAGES}" \ + "${NUGET_HTTP_CACHE_PATH}" +fi + # The default Compose layout mounts the repo and blockstore separately. Changing # ownership on the mount roots is cheap and avoids recursively walking a large # existing blockstore on every container start. From e9ac0a2b8ec5ef3fa69476dc8dc11b94a5e31703 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:37:59 -0400 Subject: [PATCH 64/98] Add Kubo diagnostics command --- docker/kubo-status.sh | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docker/kubo-status.sh diff --git a/docker/kubo-status.sh b/docker/kubo-status.sh new file mode 100644 index 0000000..ded9810 --- /dev/null +++ b/docker/kubo-status.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +: "${IPFS_PATH:=/data/ipfs/repo}" +api_address="/ip4/127.0.0.1/tcp/5001" + +config_value() { + ipfs config "$1" 2>/dev/null || printf '' +} + +config_json() { + ipfs config "$1" --json 2>/dev/null | jq -c . || printf '' +} + +printf 'TruthGate Kubo status\n' +printf '=====================\n' +printf 'Repository: %s\n' "${IPFS_PATH}" +printf 'Kubo version: %s\n' "$(ipfs version --number 2>/dev/null || printf '')" +printf 'Routing.Type: %s\n' "$(config_value Routing.Type)" +printf 'Addresses.API: %s\n' "$(config_value Addresses.API)" +printf 'Addresses.Gateway: %s\n' "$(config_value Addresses.Gateway)" +printf 'Addresses.Swarm: %s\n' "$(config_json Addresses.Swarm)" +printf 'Addresses.AppendAnnounce: %s\n' "$(config_json Addresses.AppendAnnounce)" +printf 'Swarm.EnableHolePunching: %s\n' "$(config_value Swarm.EnableHolePunching)" +printf 'Swarm.RelayClient.Enabled: %s\n' "$(config_value Swarm.RelayClient.Enabled)" +printf 'Provide.Enabled: %s\n' "$(config_value Provide.Enabled)" +printf 'Provide.Strategy: %s\n' "$(config_value Provide.Strategy)" +printf 'Provide.DHT.Interval: %s\n' "$(config_value Provide.DHT.Interval)" +printf 'Provide.DHT.SweepEnabled: %s\n' "$(config_value Provide.DHT.SweepEnabled)" +printf 'Provide.DHT.ResumeEnabled: %s\n' "$(config_value Provide.DHT.ResumeEnabled)" +printf 'Datastore.StorageMax: %s\n' "$(config_value Datastore.StorageMax)" +printf 'Datastore.StorageGCWatermark: %s%%\n' "$(config_value Datastore.StorageGCWatermark)" + +if ! ipfs --api="${api_address}" id >/dev/null 2>&1; then + printf '\nDaemon: unavailable at %s\n' "${api_address}" + exit 1 +fi + +peer_id="$(ipfs --api="${api_address}" id -f='')" +peer_count="$(ipfs --api="${api_address}" swarm peers 2>/dev/null | awk 'NF { count++ } END { print count + 0 }')" + +printf '\nDaemon: ready\n' +printf 'Peer ID: %s\n' "${peer_id}" +printf 'Connected swarm peers: %s\n' "${peer_count}" +printf 'Active listen addresses:\n' +ipfs --api="${api_address}" swarm addrs listen 2>/dev/null | sed 's/^/ /' || true + +printf '\nRepository statistics:\n' +ipfs --api="${api_address}" repo stat --human 2>/dev/null | sed 's/^/ /' || true + +printf '\nAutoNAT observations:\n' +ipfs --api="${api_address}" swarm addrs autonat 2>/dev/null | sed 's/^/ /' || \ + printf ' unavailable on this Kubo build\n' + +printf '\nProvider statistics:\n' +ipfs --api="${api_address}" provide stat 2>/dev/null | sed 's/^/ /' || \ + printf ' unavailable or provider is still initializing\n' From 1dc2eb1285445c3148c47ca01b5ea7ef491bd8d6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:38:16 -0400 Subject: [PATCH 65/98] Install Kubo configuration tooling --- Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dockerfile b/Dockerfile index b446b17..a0e54a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,6 +35,7 @@ RUN apt-get update \ ca-certificates \ curl \ gosu \ + jq \ tini \ && rm -rf /var/lib/apt/lists/* \ && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ @@ -49,6 +50,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Production \ DOTNET_ENVIRONMENT=Production \ @@ -60,6 +62,8 @@ ENV ASPNETCORE_ENVIRONMENT=Production \ TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_KUBO_OVERRIDES_PATH=/data/truthgate/config/kubo-overrides.json \ + TRUTHGATE_KUBO_SETTINGS_PATH=/data/truthgate/config/kubo-settings.json \ TRUTHGATE_STATE_PATH=/data/truthgate/state \ TRUTHGATE_HEALTH_URL=https://127.0.0.1:443/ @@ -84,6 +88,7 @@ RUN apt-get update \ curl \ git \ gosu \ + jq \ tini \ && rm -rf /var/lib/apt/lists/* \ && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ @@ -99,6 +104,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Development \ DOTNET_ENVIRONMENT=Development \ @@ -112,6 +118,8 @@ ENV ASPNETCORE_ENVIRONMENT=Development \ TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ TRUTHGATE_DEV_PROJECT=/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + TRUTHGATE_KUBO_OVERRIDES_PATH=/data/truthgate/config/kubo-overrides.json \ + TRUTHGATE_KUBO_SETTINGS_PATH=/data/truthgate/config/kubo-settings.json \ TRUTHGATE_STATE_PATH=/data/truthgate/state \ TRUTHGATE_HEALTH_URL=http://127.0.0.1:80/ From b55b0d96476ac44be02a0f3518c890e1185101e6 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:38:31 -0400 Subject: [PATCH 66/98] Expose managed Kubo overrides --- compose.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/compose.yaml b/compose.yaml index dfee427..d01c7f3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -23,6 +23,7 @@ services: ASPNETCORE_ENVIRONMENT: Production DOTNET_ENVIRONMENT: Production IPFS_PATH: /data/ipfs/repo + IPFS_SWARM_PORT: ${IPFS_SWARM_PORT:-4001} TMPDIR: /run/truthgate TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} @@ -31,6 +32,26 @@ services: TRUTHGATE_DATABASE_PATH: /data/truthgate/database TRUTHGATE_STATE_PATH: /data/truthgate/state TRUTHGATE_HEALTH_URL: https://127.0.0.1:443/ + TRUTHGATE_KUBO_SETTINGS_PATH: /data/truthgate/config/kubo-settings.json + TRUTHGATE_KUBO_OVERRIDES_PATH: /data/truthgate/config/kubo-overrides.json + TRUTHGATE_KUBO_APPLY_SERVER_PROFILE: "${TRUTHGATE_KUBO_APPLY_SERVER_PROFILE:-}" + TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS: "${TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS:-}" + TRUTHGATE_KUBO_ROUTING_TYPE: "${TRUTHGATE_KUBO_ROUTING_TYPE:-}" + TRUTHGATE_KUBO_HOLE_PUNCHING: "${TRUTHGATE_KUBO_HOLE_PUNCHING:-}" + TRUTHGATE_KUBO_RELAY_CLIENT: "${TRUTHGATE_KUBO_RELAY_CLIENT:-}" + TRUTHGATE_KUBO_PROVIDE_ENABLED: "${TRUTHGATE_KUBO_PROVIDE_ENABLED:-}" + TRUTHGATE_KUBO_PROVIDE_STRATEGY: "${TRUTHGATE_KUBO_PROVIDE_STRATEGY:-}" + TRUTHGATE_KUBO_PROVIDE_INTERVAL: "${TRUTHGATE_KUBO_PROVIDE_INTERVAL:-}" + TRUTHGATE_KUBO_PROVIDE_SWEEP: "${TRUTHGATE_KUBO_PROVIDE_SWEEP:-}" + TRUTHGATE_KUBO_PROVIDE_RESUME: "${TRUTHGATE_KUBO_PROVIDE_RESUME:-}" + TRUTHGATE_KUBO_STORAGE_MAX: "${TRUTHGATE_KUBO_STORAGE_MAX:-}" + TRUTHGATE_KUBO_STORAGE_PERCENT: "${TRUTHGATE_KUBO_STORAGE_PERCENT:-}" + TRUTHGATE_KUBO_STORAGE_FALLBACK: "${TRUTHGATE_KUBO_STORAGE_FALLBACK:-}" + TRUTHGATE_KUBO_STORAGE_GC_WATERMARK: "${TRUTHGATE_KUBO_STORAGE_GC_WATERMARK:-}" + TRUTHGATE_KUBO_ENABLE_GC: "${TRUTHGATE_KUBO_ENABLE_GC:-}" + TRUTHGATE_KUBO_PUBLIC_IPV4: "${TRUTHGATE_KUBO_PUBLIC_IPV4:-}" + TRUTHGATE_KUBO_PUBLIC_IPV6: "${TRUTHGATE_KUBO_PUBLIC_IPV6:-}" + TRUTHGATE_KUBO_ANNOUNCE_PORT: "${TRUTHGATE_KUBO_ANNOUNCE_PORT:-}" ports: - "${TRUTHGATE_HTTP_PORT:-80}:80/tcp" - "${TRUTHGATE_HTTPS_PORT:-443}:443/tcp" From 280002d3771fc46619d280342586a0845c543706 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:38:41 -0400 Subject: [PATCH 67/98] Document Kubo server overrides --- .env.example | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.env.example b/.env.example index 659b47c..64c411a 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,33 @@ IPFS_BLOCKS_HOST_PATH=./data/ipfs/blocks TRUTHGATE_ACME_STAGING=false # Optional comma-separated IP addresses for the self-signed fallback cert. TRUTHGATE_CERT_IPS= + +# TruthGate creates data/truthgate/config/kubo-settings.json on first boot. +# That persistent file contains the normal server defaults and is the future +# control-plane contract for the TruthGate UI. Uncomment any value below to +# override the corresponding persistent setting from Docker/Compose. +# +# TRUTHGATE_KUBO_APPLY_SERVER_PROFILE=true +# TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS=true +# TRUTHGATE_KUBO_ROUTING_TYPE=dhtserver +# TRUTHGATE_KUBO_HOLE_PUNCHING=true +# TRUTHGATE_KUBO_RELAY_CLIENT=true +# TRUTHGATE_KUBO_PROVIDE_ENABLED=true +# TRUTHGATE_KUBO_PROVIDE_STRATEGY=all +# TRUTHGATE_KUBO_PROVIDE_INTERVAL=22h +# TRUTHGATE_KUBO_PROVIDE_SWEEP=true +# TRUTHGATE_KUBO_PROVIDE_RESUME=true +# +# Storage can be "auto" or any Kubo size such as 750GB, 2TB, or 5TiB. +# Auto uses the configured percentage of the filesystem containing the blockstore. +# TRUTHGATE_KUBO_STORAGE_MAX=auto +# TRUTHGATE_KUBO_STORAGE_PERCENT=90 +# TRUTHGATE_KUBO_STORAGE_FALLBACK=200GB +# TRUTHGATE_KUBO_STORAGE_GC_WATERMARK=90 +# TRUTHGATE_KUBO_ENABLE_GC=true +# +# Public addresses can be "auto", "off", or a literal address. The announce +# port defaults to IPFS_SWARM_PORT so non-default Docker host mappings work. +# TRUTHGATE_KUBO_PUBLIC_IPV4=auto +# TRUTHGATE_KUBO_PUBLIC_IPV6=auto +# TRUTHGATE_KUBO_ANNOUNCE_PORT=4001 From 72f79023b5c2dfd2548a092286b883dcd95675b4 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:40:40 -0400 Subject: [PATCH 68/98] Configure Kubo as a public server --- docker/kubo-configure.sh | 323 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docker/kubo-configure.sh diff --git a/docker/kubo-configure.sh b/docker/kubo-configure.sh new file mode 100644 index 0000000..d245bf3 --- /dev/null +++ b/docker/kubo-configure.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +truthgate_user="truthgate" +truthgate_group="$(id -g "${truthgate_user}")" + +log() { printf '[truthgate] %s\n' "$*"; } +warn() { printf '[truthgate] WARNING: %s\n' "$*" >&2; } +fail() { printf '[truthgate] ERROR: %s\n' "$*" >&2; exit 1; } +as_truthgate() { gosu "${truthgate_user}:${truthgate_group}" "$@"; } + +normalize_bool() { + case "${1,,}" in + 1|true|yes|on) printf 'true' ;; + 0|false|no|off) printf 'false' ;; + *) return 1 ;; + esac +} + +json_setting() { + local filter="$1" fallback="$2" value + value="$(jq -er "${filter} // empty" "${TRUTHGATE_KUBO_SETTINGS_PATH}" 2>/dev/null || true)" + [[ -n "${value}" ]] && printf '%s' "${value}" || printf '%s' "${fallback}" +} + +setting() { + local environment_name="$1" filter="$2" fallback="$3" + local environment_value="${!environment_name:-}" + [[ -n "${environment_value}" ]] \ + && printf '%s' "${environment_value}" \ + || json_setting "${filter}" "${fallback}" +} + +set_string() { + as_truthgate ipfs config "$1" --json "$(jq -cn --arg value "$2" '$value')" +} + +set_json() { + as_truthgate ipfs config "$1" --json "$2" +} + +ensure_array_value() { + local key="$1" item="$2" current compact updated + current="$(as_truthgate ipfs config "${key}" --json 2>/dev/null || printf '[]')" + compact="$(jq -c . <<<"${current}")" + updated="$(jq -c --arg item "${item}" \ + 'if type != "array" then [$item] elif index($item) then . else . + [$item] end' \ + <<<"${current}")" + [[ "${updated}" == "${compact}" ]] || set_json "${key}" "${updated}" +} + +valid_ipv4() { + local a b c d extra octet + IFS=. read -r a b c d extra <<<"$1" + [[ -z "${extra:-}" && -n "${a:-}" && -n "${b:-}" && -n "${c:-}" && -n "${d:-}" ]] || return 1 + for octet in "${a}" "${b}" "${c}" "${d}"; do + [[ "${octet}" =~ ^[0-9]{1,3}$ ]] || return 1 + (( 10#${octet} <= 255 )) || return 1 + done +} + +valid_ipv6() { + [[ "$1" == *:* && "$1" =~ ^[0-9A-Fa-f:.]+$ ]] +} + +resolve_public_address() { + local family="$1" requested="$2" address="" + case "${requested,,}" in + ""|off|none|disabled|false) return 0 ;; + auto) + if [[ "${family}" == "4" ]]; then + address="$(curl -4fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)" + else + address="$(curl -6fsS --max-time 5 https://api6.ipify.org 2>/dev/null || true)" + fi + address="${address//$'\r'/}" + address="${address//$'\n'/}" + ;; + *) address="${requested}" ;; + esac + + if [[ -z "${address}" ]]; then + warn "Could not detect a public IPv${family} address; no managed IPv${family} announce address will be added." + return 0 + fi + + if [[ "${family}" == "4" ]]; then + valid_ipv4 "${address}" || fail "Invalid public IPv4 address: '${address}'." + else + valid_ipv6 "${address}" || fail "Invalid public IPv6 address: '${address}'." + fi + printf '%s' "${address}" +} + +human_bytes() { + command -v numfmt >/dev/null 2>&1 \ + && numfmt --to=iec-i --suffix=B "$1" \ + || printf '%sB' "$1" +} + +: "${IPFS_PATH:=/data/ipfs/repo}" +: "${TRUTHGATE_CONFIG_PATH:=/data/truthgate/config/config.json}" +: "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" +: "${TMPDIR:=/run/truthgate}" + +config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" +blocks_directory="${IPFS_PATH}/blocks" +: "${TRUTHGATE_KUBO_SETTINGS_PATH:=${config_directory}/kubo-settings.json}" +: "${TRUTHGATE_KUBO_OVERRIDES_PATH:=${config_directory}/kubo-overrides.json}" +export IPFS_PATH TRUTHGATE_KUBO_SETTINGS_PATH TRUTHGATE_KUBO_OVERRIDES_PATH + +if [[ ! -e "${TRUTHGATE_KUBO_SETTINGS_PATH}" ]]; then + cat >"${TRUTHGATE_KUBO_SETTINGS_PATH}" <<'JSON' +{ + "serverProfile": true, + "routingType": "dhtserver", + "ensureSwarmListeners": true, + "holePunching": true, + "relayClient": true, + "provide": { + "enabled": true, + "strategy": "all", + "interval": "22h", + "sweepEnabled": true, + "resumeEnabled": true + }, + "storage": { + "max": "auto", + "percent": 90, + "fallback": "200GB", + "gcWatermark": 90, + "enableGc": true + }, + "publicAnnounce": { + "ipv4": "auto", + "ipv6": "auto", + "port": null + } +} +JSON + chown "${truthgate_user}:${truthgate_group}" "${TRUTHGATE_KUBO_SETTINGS_PATH}" + chmod 0640 "${TRUTHGATE_KUBO_SETTINGS_PATH}" +fi +jq -e 'type == "object"' "${TRUTHGATE_KUBO_SETTINGS_PATH}" >/dev/null \ + || fail "Kubo settings file is not a valid JSON object: ${TRUTHGATE_KUBO_SETTINGS_PATH}" + +if [[ ! -e "${TRUTHGATE_KUBO_OVERRIDES_PATH}" ]]; then + printf '{}\n' >"${TRUTHGATE_KUBO_OVERRIDES_PATH}" + chown "${truthgate_user}:${truthgate_group}" "${TRUTHGATE_KUBO_OVERRIDES_PATH}" + chmod 0640 "${TRUTHGATE_KUBO_OVERRIDES_PATH}" +fi +jq -e 'type == "object"' "${TRUTHGATE_KUBO_OVERRIDES_PATH}" >/dev/null \ + || fail "Kubo overrides file is not a valid JSON object: ${TRUTHGATE_KUBO_OVERRIDES_PATH}" + +server_profile_raw="$(setting TRUTHGATE_KUBO_APPLY_SERVER_PROFILE '.serverProfile' true)" +server_profile="$(normalize_bool "${server_profile_raw}")" \ + || fail "TRUTHGATE_KUBO_APPLY_SERVER_PROFILE must be true or false." +server_profile_marker="${TRUTHGATE_STATE_PATH}/kubo-server-profile-v1" + +if [[ ! -s "${IPFS_PATH}/config" ]]; then + if [[ "${server_profile}" == "true" ]]; then + log "Initializing a new Kubo repository with the public-server profile." + as_truthgate ipfs init --profile server + printf 'applied during ipfs init\n' >"${server_profile_marker}" + chown "${truthgate_user}:${truthgate_group}" "${server_profile_marker}" + else + log "Initializing a new Kubo repository without the server profile." + as_truthgate ipfs init + fi +else + log "Using the existing Kubo repository at ${IPFS_PATH}." + if [[ "${server_profile}" == "true" && ! -e "${server_profile_marker}" ]]; then + log "Applying Kubo's public-server profile to the existing repository." + as_truthgate ipfs config profile apply server + printf 'applied to existing repository\n' >"${server_profile_marker}" + chown "${truthgate_user}:${truthgate_group}" "${server_profile_marker}" + fi +fi + +set_string Routing.Type "$(setting TRUTHGATE_KUBO_ROUTING_TYPE '.routingType' dhtserver)" + +ensure_swarm_raw="$(setting TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS '.ensureSwarmListeners' true)" +ensure_swarm="$(normalize_bool "${ensure_swarm_raw}")" \ + || fail "TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS must be true or false." +if [[ "${ensure_swarm}" == "true" ]]; then + for address in \ + /ip4/0.0.0.0/tcp/4001 \ + /ip6/::/tcp/4001 \ + /ip4/0.0.0.0/udp/4001/quic-v1 \ + /ip4/0.0.0.0/udp/4001/quic-v1/webtransport \ + /ip6/::/udp/4001/quic-v1 \ + /ip6/::/udp/4001/quic-v1/webtransport + do + ensure_array_value Addresses.Swarm "${address}" + done +fi + +for spec in \ + "TRUTHGATE_KUBO_HOLE_PUNCHING|.holePunching|true|Swarm.EnableHolePunching" \ + "TRUTHGATE_KUBO_RELAY_CLIENT|.relayClient|true|Swarm.RelayClient.Enabled" \ + "TRUTHGATE_KUBO_PROVIDE_ENABLED|.provide.enabled|true|Provide.Enabled" \ + "TRUTHGATE_KUBO_PROVIDE_SWEEP|.provide.sweepEnabled|true|Provide.DHT.SweepEnabled" \ + "TRUTHGATE_KUBO_PROVIDE_RESUME|.provide.resumeEnabled|true|Provide.DHT.ResumeEnabled" +do + IFS='|' read -r env_name filter fallback key <<<"${spec}" + raw="$(setting "${env_name}" "${filter}" "${fallback}")" + value="$(normalize_bool "${raw}")" || fail "${env_name} must be true or false." + set_json "${key}" "${value}" +done + +set_string Provide.Strategy "$(setting TRUTHGATE_KUBO_PROVIDE_STRATEGY '.provide.strategy' all)" +set_string Provide.DHT.Interval "$(setting TRUTHGATE_KUBO_PROVIDE_INTERVAL '.provide.interval' 22h)" + +storage_max_requested="$(setting TRUTHGATE_KUBO_STORAGE_MAX '.storage.max' auto)" +storage_percent="$(setting TRUTHGATE_KUBO_STORAGE_PERCENT '.storage.percent' 90)" +storage_fallback="$(setting TRUTHGATE_KUBO_STORAGE_FALLBACK '.storage.fallback' 200GB)" +storage_gc_watermark="$(setting TRUTHGATE_KUBO_STORAGE_GC_WATERMARK '.storage.gcWatermark' 90)" + +[[ "${storage_percent}" =~ ^[0-9]+$ ]] && (( storage_percent >= 1 && storage_percent <= 100 )) \ + || fail "TRUTHGATE_KUBO_STORAGE_PERCENT must be an integer from 1 through 100." +[[ "${storage_gc_watermark}" =~ ^[0-9]+$ ]] && (( storage_gc_watermark <= 100 )) \ + || fail "TRUTHGATE_KUBO_STORAGE_GC_WATERMARK must be an integer from 0 through 100." + +storage_source="fixed" +if [[ "${storage_max_requested,,}" == "auto" ]]; then + storage_total_bytes="$(df -PB1 -- "${blocks_directory}" 2>/dev/null | awk 'NR == 2 { print $2 }' || true)" + if [[ "${storage_total_bytes}" =~ ^[0-9]+$ && "${storage_total_bytes}" -gt 0 ]]; then + storage_max_effective="$(( storage_total_bytes * storage_percent / 100 ))B" + storage_source="auto (${storage_percent}% of $(human_bytes "${storage_total_bytes}"))" + else + storage_max_effective="${storage_fallback}" + storage_source="fallback because filesystem capacity detection failed" + fi +else + storage_max_effective="${storage_max_requested}" +fi +set_string Datastore.StorageMax "${storage_max_effective}" +set_json Datastore.StorageGCWatermark "${storage_gc_watermark}" + +enable_gc_raw="$(setting TRUTHGATE_KUBO_ENABLE_GC '.storage.enableGc' true)" +enable_gc="$(normalize_bool "${enable_gc_raw}")" \ + || fail "TRUTHGATE_KUBO_ENABLE_GC must be true or false." +printf '%s\n' "${enable_gc}" >"${TMPDIR}/kubo-enable-gc" + +announce_port="${TRUTHGATE_KUBO_ANNOUNCE_PORT:-}" +[[ -n "${announce_port}" ]] || announce_port="$(json_setting '.publicAnnounce.port' '')" +[[ -n "${announce_port}" && "${announce_port}" != "null" ]] || announce_port="${IPFS_SWARM_PORT:-4001}" +[[ "${announce_port}" =~ ^[0-9]+$ ]] && (( announce_port >= 1 && announce_port <= 65535 )) \ + || fail "TRUTHGATE_KUBO_ANNOUNCE_PORT must be an integer from 1 through 65535." + +public_ipv4="$(resolve_public_address 4 "$(setting TRUTHGATE_KUBO_PUBLIC_IPV4 '.publicAnnounce.ipv4' auto)")" +public_ipv6="$(resolve_public_address 6 "$(setting TRUTHGATE_KUBO_PUBLIC_IPV6 '.publicAnnounce.ipv6' auto)")" + +managed_addresses=() +if [[ -n "${public_ipv4}" ]]; then + managed_addresses+=( + "/ip4/${public_ipv4}/tcp/${announce_port}" + "/ip4/${public_ipv4}/udp/${announce_port}/quic-v1" + "/ip4/${public_ipv4}/udp/${announce_port}/quic-v1/webtransport" + ) +fi +if [[ -n "${public_ipv6}" ]]; then + managed_addresses+=( + "/ip6/${public_ipv6}/tcp/${announce_port}" + "/ip6/${public_ipv6}/udp/${announce_port}/quic-v1" + "/ip6/${public_ipv6}/udp/${announce_port}/quic-v1/webtransport" + ) +fi + +managed_state="${TRUTHGATE_STATE_PATH}/kubo-managed-append-announce.json" +current="$(as_truthgate ipfs config Addresses.AppendAnnounce --json 2>/dev/null || printf '[]')" +previous='[]' +if [[ -s "${managed_state}" ]]; then + previous="$(cat "${managed_state}")" + jq -e 'type == "array"' <<<"${previous}" >/dev/null || previous='[]' +fi +without_previous="$(jq -c --argjson previous "${previous}" \ + '. as $current | [$current[] | select(. as $item | ($previous | index($item)) == null)]' \ + <<<"${current}")" +if (( ${#managed_addresses[@]} > 0 )); then + desired="$(printf '%s\n' "${managed_addresses[@]}" | jq -R . | jq -sc .)" +else + desired='[]' +fi +updated="$(jq -c --argjson desired "${desired}" \ + 'reduce $desired[] as $item (. ; if index($item) then . else . + [$item] end)' \ + <<<"${without_previous}")" +set_json Addresses.AppendAnnounce "${updated}" +printf '%s\n' "${desired}" >"${managed_state}" +chown "${truthgate_user}:${truthgate_group}" "${managed_state}" +chmod 0640 "${managed_state}" + +while IFS= read -r entry; do + key="$(jq -r '.key' <<<"${entry}")" + value="$(jq -c '.value' <<<"${entry}")" + case "${key}" in + Addresses.API|Addresses.Gateway) + warn "Ignoring protected Kubo override '${key}'; TruthGate requires loopback-only listeners." + ;; + *) + log "Applying advanced Kubo override: ${key}" + set_json "${key}" "${value}" + ;; + esac +done < <(jq -c 'to_entries[]' "${TRUTHGATE_KUBO_OVERRIDES_PATH}") + +set_string Addresses.API /ip4/127.0.0.1/tcp/5001 +set_string Addresses.Gateway /ip4/127.0.0.1/tcp/9010 + +log "Kubo server configuration:" +log " Routing.Type: $(as_truthgate ipfs config Routing.Type)" +log " Swarm listeners: $(as_truthgate ipfs config Addresses.Swarm --json | jq -c .)" +log " Managed public announce IPv4: ${public_ipv4:-disabled or unavailable}" +log " Managed public announce IPv6: ${public_ipv6:-disabled or unavailable}" +log " Public announce port: ${announce_port}" +log " Provide.Enabled: $(as_truthgate ipfs config Provide.Enabled)" +log " Provide.Strategy: $(as_truthgate ipfs config Provide.Strategy)" +log " Provide.DHT.Interval: $(as_truthgate ipfs config Provide.DHT.Interval)" +log " Provide.DHT.SweepEnabled: $(as_truthgate ipfs config Provide.DHT.SweepEnabled)" +log " Provide.DHT.ResumeEnabled: $(as_truthgate ipfs config Provide.DHT.ResumeEnabled)" +log " Datastore.StorageMax: $(as_truthgate ipfs config Datastore.StorageMax) (${storage_source})" +log " Datastore.StorageGCWatermark: $(as_truthgate ipfs config Datastore.StorageGCWatermark)%" +log " Automatic repository GC: ${enable_gc}" From 0c042d027c0317c0a8fcb0c915a1b83c7301fe3c Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:41:03 -0400 Subject: [PATCH 69/98] Wire Kubo server configuration into images --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index a0e54a3..7278b89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,6 +50,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-configure.sh /usr/local/bin/truthgate-configure-kubo COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Production \ @@ -104,6 +105,7 @@ RUN apt-get update \ COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-configure.sh /usr/local/bin/truthgate-configure-kubo COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status ENV ASPNETCORE_ENVIRONMENT=Development \ From f51b4c8e4b9af014f95c5f74a69d483101c0cb58 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:41:22 -0400 Subject: [PATCH 70/98] Apply managed Kubo configuration at startup --- docker/entrypoint.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index e68ed61..c4ace3f 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -45,6 +45,12 @@ config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" blocks_directory="${IPFS_PATH}/blocks" data_protection_directory="/data/truthgate/secrets/data-protection-keys" +: "${TRUTHGATE_KUBO_SETTINGS_PATH:=${config_directory}/kubo-settings.json}" +: "${TRUTHGATE_KUBO_OVERRIDES_PATH:=${config_directory}/kubo-overrides.json}" +require_absolute_path TRUTHGATE_KUBO_SETTINGS_PATH "${TRUTHGATE_KUBO_SETTINGS_PATH}" +require_absolute_path TRUTHGATE_KUBO_OVERRIDES_PATH "${TRUTHGATE_KUBO_OVERRIDES_PATH}" +export TRUTHGATE_KUBO_SETTINGS_PATH TRUTHGATE_KUBO_OVERRIDES_PATH + install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ "${config_directory}" \ "${TRUTHGATE_DATABASE_PATH}" \ @@ -106,7 +112,7 @@ if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then else bootstrap_password="$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')" umask 077 - printf '%s' "${bootstrap_password}" > "${bootstrap_password_file}" + printf '%s' "${bootstrap_password}" >"${bootstrap_password_file}" chown "${truthgate_user}:${truthgate_group}" "${bootstrap_password_file}" fi @@ -119,20 +125,15 @@ else unset TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD || true fi -if [[ ! -s "${IPFS_PATH}/config" ]]; then - log "Initializing a new Kubo repository at ${IPFS_PATH}." - as_truthgate ipfs init -else - log "Using the existing Kubo repository at ${IPFS_PATH}." -fi +/usr/local/bin/truthgate-configure-kubo -# TruthGate talks to Kubo over loopback. The RPC API is intentionally not -# published by Compose; the gateway is likewise kept private behind TruthGate. -as_truthgate ipfs config Addresses.API /ip4/127.0.0.1/tcp/5001 -as_truthgate ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/9010 +daemon_args=(daemon --migrate=true) +if [[ "$(<"${TMPDIR}/kubo-enable-gc")" == "true" ]]; then + daemon_args+=(--enable-gc) +fi log "Starting Kubo with automatic repository migrations enabled." -as_truthgate ipfs daemon --migrate=true & +as_truthgate ipfs "${daemon_args[@]}" & ipfs_pid=$! stop_children() { From 468c7c0c2c7f7831f4f22db3908c28e53b03000a Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:42:08 -0400 Subject: [PATCH 71/98] Validate managed Kubo server defaults in CI --- .github/workflows/docker.yml | 57 +++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bffe3ea..464d29c 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -29,6 +29,12 @@ jobs: run: docker compose -f compose.yaml config - name: Validate development override run: docker compose -f compose.yaml -f compose.dev.yaml config + - name: Validate container scripts + run: | + bash -n docker/entrypoint.sh + bash -n docker/healthcheck.sh + bash -n docker/kubo-configure.sh + bash -n docker/kubo-status.sh smoke-test: strategy: @@ -91,6 +97,8 @@ jobs: export TRUTHGATE_HTTP_PORT=18080 export TRUTHGATE_HTTPS_PORT=18443 export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks @@ -132,6 +140,47 @@ jobs: | cut -d' ' -f1 } + read_settings_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/config/kubo-settings.json \ + | cut -d' ' -f1 + } + + validate_kubo_server_defaults() { + test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver" + test "$(docker exec truthgate ipfs config Swarm.EnableHolePunching)" = "true" + test "$(docker exec truthgate ipfs config Swarm.RelayClient.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all" + test "$(docker exec truthgate ipfs config Provide.DHT.Interval)" = "22h" + test "$(docker exec truthgate ipfs config Provide.DHT.SweepEnabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.DHT.ResumeEnabled)" = "true" + test "$(docker exec truthgate ipfs config Datastore.StorageGCWatermark)" = "90" + + storage_max="$(docker exec truthgate ipfs config Datastore.StorageMax)" + test "${storage_max}" != "10GB" + [[ "${storage_max}" =~ ^[0-9]+B$ ]] + + docker exec truthgate test -s /data/truthgate/state/kubo-server-profile-v1 + docker exec truthgate test -s /data/truthgate/config/kubo-settings.json + docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json + + docker exec truthgate \ + ipfs config Addresses.Swarm --json \ + | jq -e ' + index("/ip4/0.0.0.0/tcp/4001") and + index("/ip6/::/tcp/4001") and + index("/ip4/0.0.0.0/udp/4001/quic-v1") and + index("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport") and + index("/ip6/::/udp/4001/quic-v1") and + index("/ip6/::/udp/4001/quic-v1/webtransport") + ' + + test "$(docker exec truthgate ipfs config Addresses.API)" = "/ip4/127.0.0.1/tcp/5001" + test "$(docker exec truthgate ipfs config Addresses.Gateway)" = "/ip4/127.0.0.1/tcp/9010" + docker exec truthgate truthgate-kubo-status + } + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks docker compose up --detach --no-build @@ -141,22 +190,28 @@ jobs: # intentionally unable to inspect the bootstrap secret directly. docker exec truthgate test -s /data/ipfs/repo/config docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_kubo_server_defaults peer_id_before="$(read_peer_id)" bootstrap_hash_before="$(read_bootstrap_hash)" + settings_hash_before="$(read_settings_hash)" # Replace the container while retaining only the documented bind-mounted - # state. The Kubo identity and first-run credential must remain stable. + # state. Kubo identity, managed settings, and first-run credentials must + # remain stable. docker compose down --remove-orphans docker compose up --detach --no-build wait_for_health docker exec truthgate test -s /data/ipfs/repo/config docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_kubo_server_defaults peer_id_after="$(read_peer_id)" bootstrap_hash_after="$(read_bootstrap_hash)" + settings_hash_after="$(read_settings_hash)" test "${peer_id_before}" = "${peer_id_after}" test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" + test "${settings_hash_before}" = "${settings_hash_after}" docker compose logs --no-color From 969be3b5208b20dbf7b1a8c1b52f28c7dc6b469a Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:42:33 -0400 Subject: [PATCH 72/98] Document managed Kubo server behavior --- DOCKER.md | 224 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 206 insertions(+), 18 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 7a26849..9222885 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -4,12 +4,19 @@ TruthGate ships as one container containing the TruthGate ASP.NET application, Kubo, and the matching `ipfs` CLI. Docker owns the software lifecycle; mounted state survives image replacement. +The appliance is intentionally configured as a contributing IPFS server. It is +not a quiet desktop-node preset: DHT server mode, inbound TCP/UDP swarm +transports, content providing, and automatic repository GC are enabled unless +an operator explicitly overrides them. + ## Requirements - Docker Engine with the Compose v2 plugin - Docker Compose 2.24.4 or newer for the development override's `!override` merge directive - TCP ports 80, 443, and 4001 plus UDP port 4001 available by default +- Public TCP and UDP forwarding for the selected swarm port when deployed + behind NAT ## Production quick start @@ -66,24 +73,203 @@ They are mounted as: Kubo still sees a conventional `$IPFS_PATH/blocks` directory, while the host can later relocate only the blockstore by changing `IPFS_BLOCKS_HOST_PATH`. No network filesystem, FUSE, JuiceFS, mount management, or mount health logic -is included in this initial implementation. +is included in this implementation. The software inside the image is disposable. Backups should target the mounted state, not the .NET runtime, Kubo binary, application binaries, or build output. +## Opinionated Kubo server defaults + +A new repository is initialized with Kubo's `server` profile. An existing +repository receives that profile once, tracked by +`data/truthgate/state/kubo-server-profile-v1`. The profile disables mDNS and +automatic NAT port mapping and filters non-public address ranges, matching a +public server deployment. Set `TRUTHGATE_KUBO_APPLY_SERVER_PROFILE=false` +before the first managed boot when that behavior is not appropriate. + +TruthGate then manages these defaults: + +```text +Routing.Type dhtserver +Addresses.Swarm current Kubo defaults, with missing + TCP/QUIC/WebTransport listeners restored +Swarm.EnableHolePunching true +Swarm.RelayClient.Enabled true +Provide.Enabled true +Provide.Strategy all +Provide.DHT.Interval 22h +Provide.DHT.SweepEnabled true +Provide.DHT.ResumeEnabled true +Datastore.StorageMax auto: 90% of the blockstore filesystem +Datastore.StorageGCWatermark 90 +Automatic repository GC enabled +Addresses.API /ip4/127.0.0.1/tcp/5001 +Addresses.Gateway /ip4/127.0.0.1/tcp/9010 +``` + +The entrypoint preserves Kubo's current listener array and only appends missing +standard listeners. That avoids deleting newer transports such as +WebTransport when Kubo changes its defaults. + +The RPC API and HTTP gateway are always loopback-only. TruthGate already +authenticates and proxies public `/api/v0`, `/ipfs`, `/ipns`, and WebUI access. +Those two Kubo listener settings cannot be overridden. + +The public swarm mapping on port 4001 is the current implementation stage. +TruthGate is expected to own and proxy swarm transports in a future +architecture; until that transport layer exists, Docker publishes Kubo's TCP +and UDP swarm traffic directly. + +## Persistent Kubo settings + +On first boot TruthGate creates: + +```text +data/truthgate/config/kubo-settings.json +``` + +This file is persistent and contains the normal managed settings. It is also +the contract intended for future live management from the TruthGate UI. +Editing the file and restarting the container changes the effective Kubo +configuration without rebuilding the image. + +Environment variables take precedence over the persistent settings file. The +available variables are documented in `.env.example`, including: + +```text +TRUTHGATE_KUBO_ROUTING_TYPE +TRUTHGATE_KUBO_PROVIDE_ENABLED +TRUTHGATE_KUBO_PROVIDE_STRATEGY +TRUTHGATE_KUBO_STORAGE_MAX +TRUTHGATE_KUBO_STORAGE_PERCENT +TRUTHGATE_KUBO_ENABLE_GC +TRUTHGATE_KUBO_PUBLIC_IPV4 +TRUTHGATE_KUBO_PUBLIC_IPV6 +TRUTHGATE_KUBO_ANNOUNCE_PORT +``` + +For advanced settings, edit: + +```text +data/truthgate/config/kubo-overrides.json +``` + +It is a JSON object whose keys are Kubo config paths and whose values are raw +JSON values: + +```json +{ + "Swarm.ConnMgr.LowWater": 100, + "Swarm.ConnMgr.HighWater": 300, + "Routing.AcceleratedDHTClient": false +} +``` + +Advanced overrides run after TruthGate's normal defaults. `Addresses.API` and +`Addresses.Gateway` are ignored in this file because they are protected +loopback interfaces. + +## Storage policy + +The default storage policy is: + +```json +{ + "storage": { + "max": "auto", + "percent": 90, + "fallback": "200GB", + "gcWatermark": 90, + "enableGc": true + } +} +``` + +`auto` reads the total capacity of the filesystem containing +`/data/ipfs/repo/blocks` and sets Kubo's `Datastore.StorageMax` to 90 percent of +that total. It uses total capacity rather than currently free space so the +limit does not shrink as content is added. If capacity detection fails, the +fallback is `200GB`. + +A fixed value is also supported: + +```env +TRUTHGATE_KUBO_STORAGE_MAX=2TB +``` + +Auto mode recalculates on every container start, so expanding a VPS disk only +requires a container restart. Fixed mode remains exactly the configured value. +`StorageMax` is a soft Kubo blockstore/GC threshold rather than a hard quota, +so filesystem and metadata headroom still matter. + +## Public announce addresses + +By default TruthGate attempts to detect public IPv4 and IPv6 addresses and +manages matching TCP, QUIC-v1, and WebTransport entries in +`Addresses.AppendAnnounce`. Previously managed entries are removed before new +ones are added, so public-IP changes do not accumulate stale addresses. + +Each address can be automatic, disabled, or literal: + +```env +TRUTHGATE_KUBO_PUBLIC_IPV4=auto +TRUTHGATE_KUBO_PUBLIC_IPV6=off +TRUTHGATE_KUBO_ANNOUNCE_PORT=4001 +``` + +The announce port defaults to `IPFS_SWARM_PORT`. If Docker publishes +`14001:4001`, set `IPFS_SWARM_PORT=14001`; TruthGate will announce `14001` while +Kubo continues listening on container port 4001. + ## Kubo startup and migrations -The entrypoint initializes Kubo only when `/data/ipfs/repo/config` is absent. -Existing repositories are preserved. Every start uses: +Every start uses automatic repository migration. Automatic GC is included when +enabled: + +```bash +ipfs daemon --migrate=true --enable-gc +``` + +The entrypoint applies managed settings before starting the daemon, then starts +TruthGate only after Kubo's loopback RPC API is ready. + +## Diagnostics + +Inspect the effective configuration and live node state with: + +```bash +docker exec truthgate truthgate-kubo-status +``` + +The report includes: + +- peer ID and connected peer count +- effective routing, provide, swarm, and storage configuration +- configured and active listen addresses +- public append-announcement addresses +- repository statistics +- AutoNAT observations +- sweep-provider statistics + +Useful direct checks include: + +```bash +docker exec truthgate ipfs config Routing.Type +docker exec truthgate ipfs config --json Addresses.Swarm +docker exec truthgate ipfs config --json Addresses.AppendAnnounce +docker exec truthgate ipfs provide stat +docker exec truthgate ipfs stats dht +``` + +A real external validation should use another IPFS node: ```bash -ipfs daemon --migrate=true +ipfs routing findpeer ``` -This lets Kubo perform supported repository migrations when a newer image is -started against older persistent data. The RPC API listens only on container -loopback at port 5001, and the Kubo HTTP gateway listens only on loopback at -port 9010. Neither is published to the host. +Then add a unique file through TruthGate, obtain its CID, and retrieve it from +the separate node. That verifies routing and content providing, not merely +local daemon health. ## Development @@ -94,21 +280,21 @@ docker compose -f compose.yaml -f compose.dev.yaml up --build ``` The override changes the image target to the .NET SDK development stage, -bind-mounts the repository at `/workspace`, adds a persistent NuGet cache, and +bind-mounts the repository at `/workspace`, adds persistent NuGet caches, and runs the web project with `dotnet watch`. TruthGate is available at `http://localhost:8080` by default. -Kubo initialization, repository migrations, persistent paths, startup order, -and process supervision are shared with production. This prevents development -from silently using a different node layout. +Kubo initialization, server defaults, repository migrations, persistent paths, +startup order, and process supervision are shared with production. This +prevents development from silently using a different node layout. Rider can use `compose.yaml` followed by `compose.dev.yaml` directly in a Docker Compose run configuration. The source directory is mounted directly, so normal edits trigger `dotnet watch`. -## Configuration +## General configuration -All ordinary host-facing settings live in `.env`. Important values include: +Ordinary host-facing settings live in `.env`. Important values include: - `TRUTHGATE_IMAGE` - `KUBO_VERSION` @@ -124,7 +310,9 @@ at the directory containing `compose.yaml`. ## Process model -`tini` is PID 1. The entrypoint starts Kubo first, waits for its RPC API, then -starts TruthGate. If either process exits, the entrypoint terminates the other -and exits so Docker's restart policy can recover the appliance. SIGTERM and -SIGINT are forwarded to both processes for graceful shutdown. +`tini` is PID 1 in normal Docker execution. Rider's debugger may become PID 1, +in which case Tini registers as a child subreaper. The entrypoint starts Kubo +first, waits for its RPC API, then starts TruthGate. If either process exits, +the entrypoint terminates the other and exits so Docker's restart policy can +recover the appliance. SIGTERM and SIGINT are forwarded to both processes for +graceful shutdown. From fca02c65079ba6107f527e8dad3e831b235ae9a8 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:43:10 -0400 Subject: [PATCH 73/98] Use canonical Kubo JSON config syntax --- docker/kubo-status.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/kubo-status.sh b/docker/kubo-status.sh index ded9810..cd91767 100644 --- a/docker/kubo-status.sh +++ b/docker/kubo-status.sh @@ -9,7 +9,7 @@ config_value() { } config_json() { - ipfs config "$1" --json 2>/dev/null | jq -c . || printf '' + ipfs config --json "$1" 2>/dev/null | jq -c . || printf '' } printf 'TruthGate Kubo status\n' From f26d8441e60be789563082c72f7b74234f640192 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:43:44 -0400 Subject: [PATCH 74/98] Use canonical Kubo JSON config syntax --- docker/kubo-configure.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/kubo-configure.sh b/docker/kubo-configure.sh index d245bf3..074a137 100644 --- a/docker/kubo-configure.sh +++ b/docker/kubo-configure.sh @@ -32,16 +32,16 @@ setting() { } set_string() { - as_truthgate ipfs config "$1" --json "$(jq -cn --arg value "$2" '$value')" + as_truthgate ipfs config --json "$1" "$(jq -cn --arg value "$2" '$value')" } set_json() { - as_truthgate ipfs config "$1" --json "$2" + as_truthgate ipfs config --json "$1" "$2" } ensure_array_value() { local key="$1" item="$2" current compact updated - current="$(as_truthgate ipfs config "${key}" --json 2>/dev/null || printf '[]')" + current="$(as_truthgate ipfs config --json "${key}" 2>/dev/null || printf '[]')" compact="$(jq -c . <<<"${current}")" updated="$(jq -c --arg item "${item}" \ 'if type != "array" then [$item] elif index($item) then . else . + [$item] end' \ @@ -268,7 +268,7 @@ if [[ -n "${public_ipv6}" ]]; then fi managed_state="${TRUTHGATE_STATE_PATH}/kubo-managed-append-announce.json" -current="$(as_truthgate ipfs config Addresses.AppendAnnounce --json 2>/dev/null || printf '[]')" +current="$(as_truthgate ipfs config --json Addresses.AppendAnnounce 2>/dev/null || printf '[]')" previous='[]' if [[ -s "${managed_state}" ]]; then previous="$(cat "${managed_state}")" @@ -309,7 +309,7 @@ set_string Addresses.Gateway /ip4/127.0.0.1/tcp/9010 log "Kubo server configuration:" log " Routing.Type: $(as_truthgate ipfs config Routing.Type)" -log " Swarm listeners: $(as_truthgate ipfs config Addresses.Swarm --json | jq -c .)" +log " Swarm listeners: $(as_truthgate ipfs config --json Addresses.Swarm | jq -c .)" log " Managed public announce IPv4: ${public_ipv4:-disabled or unavailable}" log " Managed public announce IPv6: ${public_ipv6:-disabled or unavailable}" log " Public announce port: ${announce_port}" From f6f3f98a607aa3a6cff0439d606ce2fddca70fe9 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:44:05 -0400 Subject: [PATCH 75/98] Use canonical Kubo JSON config syntax in CI --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 464d29c..7ec5080 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -166,7 +166,7 @@ jobs: docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json docker exec truthgate \ - ipfs config Addresses.Swarm --json \ + ipfs config --json Addresses.Swarm \ | jq -e ' index("/ip4/0.0.0.0/tcp/4001") and index("/ip6/::/tcp/4001") and From 03497f1fcec7120b25bbb1ae3c3efc97ad6a2632 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:44:41 -0400 Subject: [PATCH 76/98] Describe Kubo server defaults in README --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef04aca..437c784 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,18 @@ The first-start logs contain a generated password for the `admin` account. Open `https://localhost`, accept the temporary self-signed fallback certificate, and change the password. -For the full persistence contract, image update flow, ARM64/AMD64 publishing, -and Docker-based development setup, see **[DOCKER.md](DOCKER.md)**. +The appliance configures Kubo as a contributing server by default: DHT server +mode, TCP/QUIC/WebTransport swarm listeners, content providing, automatic +storage sizing, and repository GC are enabled with persistent per-setting +overrides. Inspect the live node with: + +```bash +docker exec truthgate truthgate-kubo-status +``` + +For the full persistence contract, Kubo settings, image update flow, +ARM64/AMD64 publishing, and Docker-based development setup, see +**[DOCKER.md](DOCKER.md)**. Development with hot reload uses the production definition plus a small override: From c426e5e9741f6fc5111b16e32c77eaddf3f52878 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:31:07 -0400 Subject: [PATCH 77/98] Migrate existing Kubo repos before configuration --- docker/entrypoint.sh | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c4ace3f..a9de22d 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -30,6 +30,7 @@ require_absolute_path() { : "${TRUTHGATE_CERT_PATH:=/data/truthgate/certificates}" : "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" : "${TMPDIR:=/run/truthgate}" +: "${TRUTHGATE_KUBO_REPO_VERSION:=18}" export IPFS_PATH TRUTHGATE_CONFIG_PATH TRUTHGATE_DATABASE_PATH export TRUTHGATE_CERT_PATH TRUTHGATE_STATE_PATH TMPDIR @@ -40,6 +41,8 @@ require_absolute_path TRUTHGATE_DATABASE_PATH "${TRUTHGATE_DATABASE_PATH}" require_absolute_path TRUTHGATE_CERT_PATH "${TRUTHGATE_CERT_PATH}" require_absolute_path TRUTHGATE_STATE_PATH "${TRUTHGATE_STATE_PATH}" require_absolute_path TMPDIR "${TMPDIR}" +[[ "${TRUTHGATE_KUBO_REPO_VERSION}" =~ ^[0-9]+$ ]] \ + || fail "TRUTHGATE_KUBO_REPO_VERSION must be a positive integer." config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" blocks_directory="${IPFS_PATH}/blocks" @@ -103,6 +106,43 @@ chown "${truthgate_user}:${truthgate_group}" \ "${data_protection_directory}" \ "${TMPDIR}" +migrate_existing_kubo_repository() { + local repo_config="${IPFS_PATH}/config" + local repo_version_file="${IPFS_PATH}/version" + local current_version migrated_version + + # Fresh repositories are initialized later by truthgate-configure-kubo and + # already use the format expected by the bundled Kubo binary. + [[ -s "${repo_config}" ]] || return 0 + + [[ -s "${repo_version_file}" ]] \ + || fail "Existing Kubo repository is missing its version file: ${repo_version_file}" + + current_version="$(tr -d '[:space:]' <"${repo_version_file}")" + [[ "${current_version}" =~ ^[0-9]+$ ]] \ + || fail "Kubo repository version is invalid: '${current_version}'." + + if (( current_version > TRUTHGATE_KUBO_REPO_VERSION )); then + fail "Kubo repository version ${current_version} is newer than the bundled Kubo supports (${TRUTHGATE_KUBO_REPO_VERSION}). Upgrade the TruthGate image before starting this repository." + fi + + if (( current_version == TRUTHGATE_KUBO_REPO_VERSION )); then + log "Kubo repository is already at version ${current_version}." + return 0 + fi + + log "Migrating Kubo repository from version ${current_version} to ${TRUTHGATE_KUBO_REPO_VERSION} before applying managed configuration." + as_truthgate ipfs repo migrate --to="${TRUTHGATE_KUBO_REPO_VERSION}" + + migrated_version="$(tr -d '[:space:]' <"${repo_version_file}")" + [[ "${migrated_version}" == "${TRUTHGATE_KUBO_REPO_VERSION}" ]] \ + || fail "Kubo repository migration completed without producing expected version ${TRUTHGATE_KUBO_REPO_VERSION}; found '${migrated_version}'." + + log "Kubo repository migration completed at version ${migrated_version}." +} + +migrate_existing_kubo_repository + bootstrap_password_file="${TRUTHGATE_STATE_PATH}/bootstrap-admin-password" if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then if [[ -n "${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD:-}" ]]; then From 3d0b0f3176f6da25a13ccf1ddd60173453af4f1b Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:31:27 -0400 Subject: [PATCH 78/98] Test automatic Kubo repository migration --- .github/workflows/docker-repo-migration.yml | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/docker-repo-migration.yml diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml new file mode 100644 index 0000000..f95beb5 --- /dev/null +++ b/.github/workflows/docker-repo-migration.yml @@ -0,0 +1,120 @@ +name: Docker repository migration + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-repo-migration-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + migrate-kubo-repository: + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v4 + + - name: Build production image (${{ matrix.architecture }}) + run: | + docker build \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:migration-test-${{ matrix.architecture }} \ + . + + - name: Create a Kubo 0.36 repository and verify automatic migration + shell: bash + run: | + exec > >(tee repo-migration-${{ matrix.architecture }}.log) 2>&1 + set -Eeuo pipefail + + export TRUTHGATE_IMAGE=truthgate-ipfs:migration-test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off + export TRUTHGATE_DATA_HOST_PATH=./.ci-migration/truthgate + export IPFS_REPO_HOST_PATH=./.ci-migration/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-migration/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + rm -rf .ci-migration + mkdir -p \ + .ci-migration/truthgate \ + .ci-migration/ipfs/repo \ + .ci-migration/ipfs/blocks + + docker run --rm \ + --user 0:0 \ + --entrypoint /usr/local/bin/ipfs \ + --env IPFS_PATH=/data/ipfs/repo \ + --volume "$PWD/.ci-migration/ipfs/repo:/data/ipfs/repo" \ + --volume "$PWD/.ci-migration/ipfs/blocks:/data/ipfs/repo/blocks" \ + ipfs/kubo:v0.36.0 \ + init --profile server + + test "$(tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" + peer_id_before="$(jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" + test -n "${peer_id_before}" + + sudo chown -R 1000:1000 .ci-migration + + docker compose up --detach --no-build + + for attempt in $(seq 1 60); do + state="$(docker inspect --format '{{.State.Status}}' truthgate)" + health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + echo "Attempt ${attempt}: state=${state} health=${health}" + + if [[ "${health}" == "healthy" ]]; then + break + fi + + if [[ "${state}" == "exited" || "${health}" == "unhealthy" ]]; then + docker compose logs --no-color + exit 1 + fi + + sleep 5 + done + + test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" + test "$(docker exec truthgate tr -d '[:space:]' Date: Fri, 24 Jul 2026 18:31:57 -0400 Subject: [PATCH 79/98] Fix migration version assertion --- .github/workflows/docker-repo-migration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index f95beb5..87f8c46 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -97,7 +97,7 @@ jobs: done test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" - test "$(docker exec truthgate tr -d '[:space:]' Date: Fri, 24 Jul 2026 18:32:17 -0400 Subject: [PATCH 80/98] Simplify migrated repo version check --- .github/workflows/docker-repo-migration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index 87f8c46..d08f626 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -97,7 +97,7 @@ jobs: done test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" - test "$(docker exec truthgate sh -lc "tr -d '[:space:]' Date: Fri, 24 Jul 2026 18:34:16 -0400 Subject: [PATCH 81/98] Fix legacy repo test ownership --- .github/workflows/docker-repo-migration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index d08f626..393544e 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -71,12 +71,12 @@ jobs: ipfs/kubo:v0.36.0 \ init --profile server + sudo chown -R 1000:1000 .ci-migration + test "$(tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" peer_id_before="$(jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" test -n "${peer_id_before}" - sudo chown -R 1000:1000 .ci-migration - docker compose up --detach --no-build for attempt in $(seq 1 60); do From dbae6006a7ad988055e4a05a8b59256ca8bf9d03 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:36:19 -0400 Subject: [PATCH 82/98] Read legacy repo metadata before ownership handoff --- .github/workflows/docker-repo-migration.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index 393544e..69dd808 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -71,12 +71,12 @@ jobs: ipfs/kubo:v0.36.0 \ init --profile server - sudo chown -R 1000:1000 .ci-migration - - test "$(tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" - peer_id_before="$(jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" + test "$(sudo tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" + peer_id_before="$(sudo jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" test -n "${peer_id_before}" + sudo chown -R 1000:1000 .ci-migration + docker compose up --detach --no-build for attempt in $(seq 1 60); do From 28147d662b1a850f87ac19bb02bd09063443419d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:56:39 -0400 Subject: [PATCH 83/98] Let authenticated users access the TruthGate portal --- .../Middleware/DomainGatewayMiddleware.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs index 45260a2..d056ea5 100644 --- a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs +++ b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using System.IO; using System.Net; @@ -19,6 +19,16 @@ private sealed record RunOnceResult( private static async Task RunOnce(HttpContext ctx, Func next) { + // A mapped hostname can serve both the public IPFS site and TruthGate's + // administrator portal. Anonymous visitors receive the mapped site; + // authenticated users stay inside the real application, including its + // Blazor framework, SignalR circuit, static assets, and dashboard routes. + if (ctx.User?.Identity?.IsAuthenticated == true) + { + await next(); + return new RunOnceResult(Handled: true, RetryCandidate: false, Cid: null, MfsPath: null); + } + var mfsPath = DomainHelpers.GetMappedDomain(ctx); if (string.IsNullOrWhiteSpace(mfsPath)) { @@ -162,4 +172,4 @@ public static IApplicationBuilder UseDomainGateway(this IApplicationBuilder app) return app; } } -} \ No newline at end of file +} From 67c127007c27dec9b7ff3980715a8fd8c40f410e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:56:49 -0400 Subject: [PATCH 84/98] Test authenticated portal bypass for mapped domains --- .../DomainGatewayMiddlewareTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Test.TruthGate/DomainGatewayMiddlewareTests.cs diff --git a/Test.TruthGate/DomainGatewayMiddlewareTests.cs b/Test.TruthGate/DomainGatewayMiddlewareTests.cs new file mode 100644 index 0000000..790746a --- /dev/null +++ b/Test.TruthGate/DomainGatewayMiddlewareTests.cs @@ -0,0 +1,41 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using TruthGate_Web.Middleware; + +namespace Test.TruthGate; + +public sealed class DomainGatewayMiddlewareTests +{ + [Fact] + public async Task AuthenticatedRequest_BypassesGatewayBeforeMappedDomainLookup() + { + await using var services = new ServiceCollection().BuildServiceProvider(); + var app = new ApplicationBuilder(services); + var nextCalled = false; + + app.UseDomainGateway(); + app.Run(context => + { + nextCalled = true; + context.Response.StatusCode = StatusCodes.Status204NoContent; + return Task.CompletedTask; + }); + + var context = new DefaultHttpContext + { + RequestServices = services, + User = new ClaimsPrincipal( + new ClaimsIdentity( + new[] { new Claim(ClaimTypes.NameIdentifier, "admin") }, + authenticationType: "Test")) + }; + context.Request.Host = new HostString("truthgate.io"); + + await app.Build()(context); + + Assert.True(nextCalled); + Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + } +} From 7cf8bb195ec581e8a929711f358be4498641b041 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:21:23 -0400 Subject: [PATCH 85/98] Revert authenticated domain gateway bypass --- .../Middleware/DomainGatewayMiddleware.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs index d056ea5..45260a2 100644 --- a/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs +++ b/TruthGate-Web/TruthGate-Web/Middleware/DomainGatewayMiddleware.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using System.IO; using System.Net; @@ -19,16 +19,6 @@ private sealed record RunOnceResult( private static async Task RunOnce(HttpContext ctx, Func next) { - // A mapped hostname can serve both the public IPFS site and TruthGate's - // administrator portal. Anonymous visitors receive the mapped site; - // authenticated users stay inside the real application, including its - // Blazor framework, SignalR circuit, static assets, and dashboard routes. - if (ctx.User?.Identity?.IsAuthenticated == true) - { - await next(); - return new RunOnceResult(Handled: true, RetryCandidate: false, Cid: null, MfsPath: null); - } - var mfsPath = DomainHelpers.GetMappedDomain(ctx); if (string.IsNullOrWhiteSpace(mfsPath)) { @@ -172,4 +162,4 @@ public static IApplicationBuilder UseDomainGateway(this IApplicationBuilder app) return app; } } -} +} \ No newline at end of file From 0f55ef8382eee127e15ef85b1ad62b4addfeabaa Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:21:34 -0400 Subject: [PATCH 86/98] Remove invalid domain gateway regression test --- .../DomainGatewayMiddlewareTests.cs | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 Test.TruthGate/DomainGatewayMiddlewareTests.cs diff --git a/Test.TruthGate/DomainGatewayMiddlewareTests.cs b/Test.TruthGate/DomainGatewayMiddlewareTests.cs deleted file mode 100644 index 790746a..0000000 --- a/Test.TruthGate/DomainGatewayMiddlewareTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Security.Claims; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using TruthGate_Web.Middleware; - -namespace Test.TruthGate; - -public sealed class DomainGatewayMiddlewareTests -{ - [Fact] - public async Task AuthenticatedRequest_BypassesGatewayBeforeMappedDomainLookup() - { - await using var services = new ServiceCollection().BuildServiceProvider(); - var app = new ApplicationBuilder(services); - var nextCalled = false; - - app.UseDomainGateway(); - app.Run(context => - { - nextCalled = true; - context.Response.StatusCode = StatusCodes.Status204NoContent; - return Task.CompletedTask; - }); - - var context = new DefaultHttpContext - { - RequestServices = services, - User = new ClaimsPrincipal( - new ClaimsIdentity( - new[] { new Claim(ClaimTypes.NameIdentifier, "admin") }, - authenticationType: "Test")) - }; - context.Request.Host = new HostString("truthgate.io"); - - await app.Build()(context); - - Assert.True(nextCalled); - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); - } -} From 1287e7833824b7ab4d31afe8c915f68c12645c61 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:32:03 -0400 Subject: [PATCH 87/98] Include Blazor web assets in production publish --- TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index d18a0fb..0a9f586 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -6,6 +6,7 @@ enable TruthGate_Web $(AssemblyName.Replace(' ', '_')) + true @@ -32,4 +33,4 @@ - + \ No newline at end of file From ed2986e494457c091473a5528e8ce2b8aac56a7d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:32:54 -0400 Subject: [PATCH 88/98] Fail Docker builds without Blazor bootstrap assets --- Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7278b89..11fae41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,21 @@ RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ --output /out \ /p:UseAppHost=false +# .NET 10 publishes the Blazor bootstrap as a static web asset instead of an +# embedded framework resource. Fail the image build if restore/publish ever +# omits it, because the portal would otherwise render static HTML while every +# interactive server component silently remains inactive. +RUN set -eu; \ + manifest=/out/TruthGate-Web.staticwebassets.endpoints.json; \ + test -s "${manifest}"; \ + grep -q '"Route":"_framework/blazor\.web\.js"' "${manifest}"; \ + asset="$(find /out/wwwroot/_framework -maxdepth 1 -type f \ + \( -name 'blazor.web.js' -o -name 'blazor.web.*.js' \) \ + -size +10000c -print -quit)"; \ + test -n "${asset}"; \ + printf 'Verified Blazor bootstrap asset: %s (%s bytes)\n' \ + "${asset}" "$(wc -c <"${asset}")" + FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-resolute AS runtime-base ARG TRUTHGATE_UID ARG TRUTHGATE_GID From b0f40ba8a7080dd5e04c8e3b9a4325d18fc6fd8d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:21:13 -0400 Subject: [PATCH 89/98] Extract reusable Docker appliance CI --- .github/workflows/docker-ci.yml | 235 ++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 .github/workflows/docker-ci.yml diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..fe07ba8 --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,235 @@ +name: Reusable Docker appliance CI + +on: + workflow_call: + +permissions: + contents: read + +jobs: + validate-compose: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Validate production Compose + run: docker compose -f compose.yaml config + + - name: Validate development override + run: docker compose -f compose.yaml -f compose.dev.yaml config + + - name: Validate container scripts + run: | + bash -n docker/entrypoint.sh + bash -n docker/healthcheck.sh + bash -n docker/kubo-configure.sh + bash -n docker/kubo-status.sh + + smoke-test: + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v4 + + - name: Build production image (${{ matrix.architecture }}) + shell: bash + run: | + set -o pipefail + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:test-${{ matrix.architecture }} \ + . 2>&1 | tee production-image-build-${{ matrix.architecture }}.log + + - name: Upload production build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: production-image-build-${{ matrix.architecture }} + path: production-image-build-${{ matrix.architecture }}.log + if-no-files-found: error + + - name: Build development image (${{ matrix.architecture }}) + shell: bash + run: | + set -o pipefail + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target development \ + --tag truthgate-ipfs:dev-test-${{ matrix.architecture }} \ + . 2>&1 | tee development-image-build-${{ matrix.architecture }}.log + + - name: Upload development build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: development-image-build-${{ matrix.architecture }} + path: development-image-build-${{ matrix.architecture }}.log + if-no-files-found: ignore + + - name: Start, replace, and revalidate the appliance + shell: bash + run: | + exec > >(tee runtime-smoke-${{ matrix.architecture }}.log) 2>&1 + set -Eeuo pipefail + + export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off + export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate + export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + wait_for_health() { + for attempt in $(seq 1 60); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + if [[ "${status}" == "healthy" ]]; then + return 0 + fi + + if [[ "${status}" == "unhealthy" ]]; then + echo "TruthGate became unhealthy." + return 1 + fi + + sleep 5 + done + + echo "TruthGate did not become healthy in time." + return 1 + } + + read_peer_id() { + docker exec truthgate \ + ipfs --api=/ip4/127.0.0.1/tcp/5001 id \ + | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' + } + + read_bootstrap_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/state/bootstrap-admin-password \ + | cut -d' ' -f1 + } + + read_settings_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/config/kubo-settings.json \ + | cut -d' ' -f1 + } + + validate_truthgate_portal() { + local asset=/tmp/truthgate-blazor.web.js + + curl \ + --silent \ + --show-error \ + --fail \ + --insecure \ + --header 'Accept-Encoding: identity' \ + --output "${asset}" \ + https://127.0.0.1:18443/_framework/blazor.web.js + + local size + size="$(wc -c <"${asset}")" + echo "Blazor bootstrap size: ${size} bytes" + test "${size}" -gt 10000 + } + + validate_kubo_server_defaults() { + test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver" + test "$(docker exec truthgate ipfs config Swarm.EnableHolePunching)" = "true" + test "$(docker exec truthgate ipfs config Swarm.RelayClient.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all" + test "$(docker exec truthgate ipfs config Provide.DHT.Interval)" = "22h" + test "$(docker exec truthgate ipfs config Provide.DHT.SweepEnabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.DHT.ResumeEnabled)" = "true" + test "$(docker exec truthgate ipfs config Datastore.StorageGCWatermark)" = "90" + + storage_max="$(docker exec truthgate ipfs config Datastore.StorageMax)" + test "${storage_max}" != "10GB" + [[ "${storage_max}" =~ ^[0-9]+B$ ]] + + docker exec truthgate test -s /data/truthgate/state/kubo-server-profile-v1 + docker exec truthgate test -s /data/truthgate/config/kubo-settings.json + docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json + + docker exec truthgate \ + ipfs config --json Addresses.Swarm \ + | jq -e ' + index("/ip4/0.0.0.0/tcp/4001") and + index("/ip6/::/tcp/4001") and + index("/ip4/0.0.0.0/udp/4001/quic-v1") and + index("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport") and + index("/ip6/::/udp/4001/quic-v1") and + index("/ip6/::/udp/4001/quic-v1/webtransport") + ' + + test "$(docker exec truthgate ipfs config Addresses.API)" = "/ip4/127.0.0.1/tcp/5001" + test "$(docker exec truthgate ipfs config Addresses.Gateway)" = "/ip4/127.0.0.1/tcp/9010" + docker exec truthgate truthgate-kubo-status + } + + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks + + docker compose up --detach --no-build + wait_for_health + + # Read protected state from inside the container. The host runner is + # intentionally unable to inspect the bootstrap secret directly. + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_truthgate_portal + validate_kubo_server_defaults + peer_id_before="$(read_peer_id)" + bootstrap_hash_before="$(read_bootstrap_hash)" + settings_hash_before="$(read_settings_hash)" + + # Replace the container while retaining only the documented bind-mounted + # state. Kubo identity, managed settings, and first-run credentials must + # remain stable. + docker compose down --remove-orphans + docker compose up --detach --no-build + wait_for_health + + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_truthgate_portal + validate_kubo_server_defaults + peer_id_after="$(read_peer_id)" + bootstrap_hash_after="$(read_bootstrap_hash)" + settings_hash_after="$(read_settings_hash)" + + test "${peer_id_before}" = "${peer_id_after}" + test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" + test "${settings_hash_before}" = "${settings_hash_after}" + + docker compose logs --no-color + + - name: Upload runtime diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: runtime-smoke-${{ matrix.architecture }} + path: runtime-smoke-${{ matrix.architecture }}.log + if-no-files-found: ignore From b4ff52323df70524b7b215ff73ed646ad31fdc0e Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:21:31 -0400 Subject: [PATCH 90/98] Reuse Docker appliance CI before GHCR publishing --- .github/workflows/docker.yml | 209 +---------------------------------- 1 file changed, 6 insertions(+), 203 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7ec5080..94e0940 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,214 +21,17 @@ env: IMAGE_NAME: ghcr.io/magiccodingman/truthgate-ipfs jobs: - validate-compose: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - name: Validate production Compose - run: docker compose -f compose.yaml config - - name: Validate development override - run: docker compose -f compose.yaml -f compose.dev.yaml config - - name: Validate container scripts - run: | - bash -n docker/entrypoint.sh - bash -n docker/healthcheck.sh - bash -n docker/kubo-configure.sh - bash -n docker/kubo-status.sh - - smoke-test: - strategy: - fail-fast: false - matrix: - include: - - architecture: amd64 - runner: ubuntu-24.04 - - architecture: arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} - steps: - - uses: actions/checkout@v4 - - - name: Build production image (${{ matrix.architecture }}) - shell: bash - run: | - set -o pipefail - docker build \ - --progress=plain \ - --platform linux/${{ matrix.architecture }} \ - --target production \ - --tag truthgate-ipfs:test-${{ matrix.architecture }} \ - . 2>&1 | tee production-image-build-${{ matrix.architecture }}.log - - - name: Upload production build diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: production-image-build-${{ matrix.architecture }} - path: production-image-build-${{ matrix.architecture }}.log - if-no-files-found: error - - - name: Build development image (${{ matrix.architecture }}) - shell: bash - run: | - set -o pipefail - docker build \ - --progress=plain \ - --platform linux/${{ matrix.architecture }} \ - --target development \ - --tag truthgate-ipfs:dev-test-${{ matrix.architecture }} \ - . 2>&1 | tee development-image-build-${{ matrix.architecture }}.log - - - name: Upload development build diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: development-image-build-${{ matrix.architecture }} - path: development-image-build-${{ matrix.architecture }}.log - if-no-files-found: ignore - - - name: Start, replace, and revalidate the appliance - shell: bash - run: | - exec > >(tee runtime-smoke-${{ matrix.architecture }}.log) 2>&1 - set -Eeuo pipefail - - export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} - export TRUTHGATE_HTTP_PORT=18080 - export TRUTHGATE_HTTPS_PORT=18443 - export IPFS_SWARM_PORT=14001 - export TRUTHGATE_KUBO_PUBLIC_IPV4=off - export TRUTHGATE_KUBO_PUBLIC_IPV6=off - export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate - export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo - export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks - - cleanup() { - docker compose logs --no-color 2>/dev/null || true - docker compose down --remove-orphans 2>/dev/null || true - } - trap cleanup EXIT - - wait_for_health() { - for attempt in $(seq 1 60); do - status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" - if [[ "${status}" == "healthy" ]]; then - return 0 - fi - - if [[ "${status}" == "unhealthy" ]]; then - echo "TruthGate became unhealthy." - return 1 - fi - - sleep 5 - done - - echo "TruthGate did not become healthy in time." - return 1 - } - - read_peer_id() { - docker exec truthgate \ - ipfs --api=/ip4/127.0.0.1/tcp/5001 id \ - | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' - } - - read_bootstrap_hash() { - docker exec truthgate \ - sha256sum /data/truthgate/state/bootstrap-admin-password \ - | cut -d' ' -f1 - } - - read_settings_hash() { - docker exec truthgate \ - sha256sum /data/truthgate/config/kubo-settings.json \ - | cut -d' ' -f1 - } - - validate_kubo_server_defaults() { - test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver" - test "$(docker exec truthgate ipfs config Swarm.EnableHolePunching)" = "true" - test "$(docker exec truthgate ipfs config Swarm.RelayClient.Enabled)" = "true" - test "$(docker exec truthgate ipfs config Provide.Enabled)" = "true" - test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all" - test "$(docker exec truthgate ipfs config Provide.DHT.Interval)" = "22h" - test "$(docker exec truthgate ipfs config Provide.DHT.SweepEnabled)" = "true" - test "$(docker exec truthgate ipfs config Provide.DHT.ResumeEnabled)" = "true" - test "$(docker exec truthgate ipfs config Datastore.StorageGCWatermark)" = "90" - - storage_max="$(docker exec truthgate ipfs config Datastore.StorageMax)" - test "${storage_max}" != "10GB" - [[ "${storage_max}" =~ ^[0-9]+B$ ]] - - docker exec truthgate test -s /data/truthgate/state/kubo-server-profile-v1 - docker exec truthgate test -s /data/truthgate/config/kubo-settings.json - docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json - - docker exec truthgate \ - ipfs config --json Addresses.Swarm \ - | jq -e ' - index("/ip4/0.0.0.0/tcp/4001") and - index("/ip6/::/tcp/4001") and - index("/ip4/0.0.0.0/udp/4001/quic-v1") and - index("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport") and - index("/ip6/::/udp/4001/quic-v1") and - index("/ip6/::/udp/4001/quic-v1/webtransport") - ' - - test "$(docker exec truthgate ipfs config Addresses.API)" = "/ip4/127.0.0.1/tcp/5001" - test "$(docker exec truthgate ipfs config Addresses.Gateway)" = "/ip4/127.0.0.1/tcp/9010" - docker exec truthgate truthgate-kubo-status - } - - mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks - - docker compose up --detach --no-build - wait_for_health - - # Read protected state from inside the container. The host runner is - # intentionally unable to inspect the bootstrap secret directly. - docker exec truthgate test -s /data/ipfs/repo/config - docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password - validate_kubo_server_defaults - peer_id_before="$(read_peer_id)" - bootstrap_hash_before="$(read_bootstrap_hash)" - settings_hash_before="$(read_settings_hash)" - - # Replace the container while retaining only the documented bind-mounted - # state. Kubo identity, managed settings, and first-run credentials must - # remain stable. - docker compose down --remove-orphans - docker compose up --detach --no-build - wait_for_health - - docker exec truthgate test -s /data/ipfs/repo/config - docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password - validate_kubo_server_defaults - peer_id_after="$(read_peer_id)" - bootstrap_hash_after="$(read_bootstrap_hash)" - settings_hash_after="$(read_settings_hash)" - - test "${peer_id_before}" = "${peer_id_after}" - test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" - test "${settings_hash_before}" = "${settings_hash_after}" - - docker compose logs --no-color - - - name: Upload runtime diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: runtime-smoke-${{ matrix.architecture }} - path: runtime-smoke-${{ matrix.architecture }}.log - if-no-files-found: ignore + appliance-ci: + name: Validate and smoke test appliance + uses: ./.github/workflows/docker-ci.yml build-and-publish: + name: Publish rolling GHCR image if: github.event_name != 'pull_request' needs: - - validate-compose - - smoke-test + - appliance-ci runs-on: ubuntu-24.04 + steps: - uses: actions/checkout@v4 From 4c4be064dc1f6225f6bd3b66518d26e96f622ded Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:03 -0400 Subject: [PATCH 91/98] Add gated Docker Hub stable release workflow --- .github/workflows/dockerhub-stable.yml | 205 +++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 .github/workflows/dockerhub-stable.yml diff --git a/.github/workflows/dockerhub-stable.yml b/.github/workflows/dockerhub-stable.yml new file mode 100644 index 0000000..fe37b0c --- /dev/null +++ b/.github/workflows/dockerhub-stable.yml @@ -0,0 +1,205 @@ +name: Docker Hub stable release + +on: + push: + branches: + - stable + +permissions: + contents: write + +concurrency: + group: dockerhub-stable-release + cancel-in-progress: false + +jobs: + appliance-ci: + name: Validate and smoke test appliance + uses: ./.github/workflows/docker-ci.yml + + repository-migration: + name: Validate legacy Kubo migration + uses: ./.github/workflows/docker-repo-migration.yml + + tls-lifecycle: + name: Validate TLS lifecycle + uses: ./.github/workflows/tls-lifecycle.yml + + prepare-release: + name: Allocate stable version + needs: + - appliance-ci + - repository-migration + - tls-lifecycle + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.version.outputs.version }} + series: ${{ steps.version.outputs.series }} + tag: ${{ steps.version.outputs.tag }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Calculate idempotent patch version + id: version + shell: bash + run: | + set -Eeuo pipefail + + series="$(tr -d '[:space:]' < VERSION)" + if [[ ! "${series}" =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "VERSION must contain only a major.minor series such as 0.1." >&2 + exit 1 + fi + + git fetch --force --tags origin + + existing_tag="" + while IFS= read -r candidate; do + [[ -n "${candidate}" ]] || continue + if [[ "${candidate}" =~ ^v${series//./\.}\.([0-9]+)$ ]]; then + existing_tag="${candidate}" + fi + done < <(git tag --points-at "${GITHUB_SHA}" --list "v${series}.*" | sort -V) + + if [[ -n "${existing_tag}" ]]; then + tag="${existing_tag}" + version="${tag#v}" + echo "Reusing ${tag}; this commit was already assigned a stable version." + else + max_patch=-1 + + while IFS= read -r candidate; do + [[ -n "${candidate}" ]] || continue + + if [[ "${candidate}" =~ ^v${series//./\.}\.([0-9]+)$ ]]; then + patch="${BASH_REMATCH[1]}" + if (( patch > max_patch )); then + max_patch="${patch}" + fi + fi + done < <(git tag --list "v${series}.*") + + version="${series}.$((max_patch + 1))" + tag="v${version}" + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag --annotate "${tag}" --message "TruthGate ${tag}" "${GITHUB_SHA}" + git push origin "${tag}" + + echo "Created stable release tag ${tag}." + fi + + echo "series=${series}" >>"${GITHUB_OUTPUT}" + echo "version=${version}" >>"${GITHUB_OUTPUT}" + echo "tag=${tag}" >>"${GITHUB_OUTPUT}" + + publish-dockerhub: + name: Publish Docker Hub image + needs: + - prepare-release + runs-on: ubuntu-24.04 + env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/truthgate-ipfs + VERSION: ${{ needs.prepare-release.outputs.version }} + SERIES: ${{ needs.prepare-release.outputs.series }} + + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate stable image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ env.VERSION }},priority=900 + type=raw,value=${{ env.SERIES }},priority=800 + type=raw,value=stable,priority=700 + type=raw,value=latest,priority=600 + type=sha,prefix=sha-,priority=100 + labels: | + org.opencontainers.image.title=TruthGate + org.opencontainers.image.description=Secure, self-hosted IPFS edge gateway with HTTPS, authentication, GUI, and Kubo. + org.opencontainers.image.version=${{ env.VERSION }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + + - name: Publish AMD64 + ARM64 to Docker Hub + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + sbom: true + + - name: Verify published multi-platform manifest + shell: bash + run: | + set -Eeuo pipefail + + docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}" \ + | tee dockerhub-manifest.txt + + grep -F 'linux/amd64' dockerhub-manifest.txt + grep -F 'linux/arm64' dockerhub-manifest.txt + + - name: Upload Docker Hub manifest diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: dockerhub-manifest-${{ env.VERSION }} + path: dockerhub-manifest.txt + if-no-files-found: ignore + + update-dockerhub-description: + name: Sync Docker Hub README + needs: + - prepare-release + - publish-dockerhub + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + + - name: Update Docker Hub overview from README + uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ secrets.DOCKERHUB_USERNAME }}/truthgate-ipfs + short-description: Secure, self-hosted IPFS edge gateway with HTTPS, authentication, GUI, and Kubo. + readme-filepath: ./README.md + enable-url-completion: true + + - name: Stable release summary + shell: bash + run: | + { + echo "## TruthGate stable release" + echo + echo "- Git tag: \`${{ needs.prepare-release.outputs.tag }}\`" + echo "- Docker image: \`${{ secrets.DOCKERHUB_USERNAME }}/truthgate-ipfs:${{ needs.prepare-release.outputs.version }}\`" + echo "- Moving tags: \`stable\`, \`latest\`, and \`${{ needs.prepare-release.outputs.series }}\`" + echo "- Architectures: \`linux/amd64\`, \`linux/arm64\`" + echo "- Docker Hub overview synchronized from \`README.md\`" + } >>"${GITHUB_STEP_SUMMARY}" From d61676ed6493361ab4626547596edcebf76e84b3 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:28 -0400 Subject: [PATCH 92/98] Make repository migration tests reusable --- .github/workflows/docker-repo-migration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml index 69dd808..28e9c40 100644 --- a/.github/workflows/docker-repo-migration.yml +++ b/.github/workflows/docker-repo-migration.yml @@ -3,6 +3,7 @@ name: Docker repository migration on: pull_request: workflow_dispatch: + workflow_call: permissions: contents: read From 06cea5f512d5f0a0814dea4948095117a5854238 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:43 -0400 Subject: [PATCH 93/98] Make TLS lifecycle tests reusable --- .github/workflows/tls-lifecycle.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tls-lifecycle.yml b/.github/workflows/tls-lifecycle.yml index 3f1191b..839e3e5 100644 --- a/.github/workflows/tls-lifecycle.yml +++ b/.github/workflows/tls-lifecycle.yml @@ -11,6 +11,10 @@ on: paths: - "TruthGate-Web/**" - ".github/workflows/tls-lifecycle.yml" + workflow_call: + +permissions: + contents: read jobs: test: From 5d99129d234729433d8c37cbbb58eacc2d325926 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:22:54 -0400 Subject: [PATCH 94/98] Set initial stable release series --- VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 VERSION diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1 From 56cdd47a414c53f308ca2f945f9e5475c831d49d Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:23:16 -0400 Subject: [PATCH 95/98] Default Compose to Docker Hub stable image --- compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yaml b/compose.yaml index d01c7f3..114ce8f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ name: truthgate services: truthgate: - image: ${TRUTHGATE_IMAGE:-ghcr.io/magiccodingman/truthgate-ipfs:master} + image: ${TRUTHGATE_IMAGE:-magiccodingman/truthgate-ipfs:stable} build: context: . dockerfile: Dockerfile From 271454fbdfaac2e4bc4fad6a38168cca113e9ade Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:23:35 -0400 Subject: [PATCH 96/98] Point example configuration at stable image --- .env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 64c411a..2cdb4ca 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ -# Published image. Local builds use the same Compose file and can override this. -TRUTHGATE_IMAGE=ghcr.io/magiccodingman/truthgate-ipfs:master +# Tested multi-platform release. Override this to pin a version or use a local image. +TRUTHGATE_IMAGE=magiccodingman/truthgate-ipfs:stable # Build inputs. Both official base images are multi-platform. DOTNET_VERSION=10.0 From 3fab15b23dcc6c5d9e8131962f18f81a7ecb09bc Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:24:13 -0400 Subject: [PATCH 97/98] Document the stable Docker Hub image flow --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 437c784..ece6c62 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,20 @@ container. Application state, Kubo repository metadata, and Kubo blocks are persisted separately by default. ```bash +git clone https://github.com/magiccodingman/TruthGate-IPFS.git +cd TruthGate-IPFS cp .env.example .env -docker compose up --build -d +docker compose pull +docker compose up -d docker compose logs truthgate ``` +The default Compose image is the tested multi-platform stable release: + +```text +magiccodingman/truthgate-ipfs:stable +``` + The first-start logs contain a generated password for the `admin` account. Open `https://localhost`, accept the temporary self-signed fallback certificate, and change the password. @@ -43,10 +52,16 @@ overrides. Inspect the live node with: docker exec truthgate truthgate-kubo-status ``` -For the full persistence contract, Kubo settings, image update flow, -ARM64/AMD64 publishing, and Docker-based development setup, see +For the full persistence contract, Kubo settings, release tags, image update +flow, ARM64/AMD64 publishing, and Docker-based development setup, see **[DOCKER.md](DOCKER.md)**. +To build the production image locally instead of pulling the stable release: + +```bash +docker compose up --build -d +``` + Development with hot reload uses the production definition plus a small override: From 8f0a7d20342e4ad92cbbee9e4e2c05ac06a38ae9 Mon Sep 17 00:00:00 2001 From: Magic <131926685+magiccodingman@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:25:11 -0400 Subject: [PATCH 98/98] Document automatic stable release tags --- DOCKER.md | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 9222885..8c3c118 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -21,12 +21,17 @@ an operator explicitly overrides them. ## Production quick start ```bash +git clone https://github.com/magiccodingman/TruthGate-IPFS.git +cd TruthGate-IPFS cp .env.example .env -docker compose up --build -d +docker compose pull +docker compose up -d ``` -Open `https://localhost` (the first connection uses TruthGate's self-signed -fallback certificate unless a configured domain has an issued certificate). +The default Compose image is `magiccodingman/truthgate-ipfs:stable`. Open +`https://localhost` after the container becomes healthy. The first connection +uses TruthGate's self-signed fallback certificate unless a configured domain +has an issued certificate. On the first boot, retrieve the generated administrator password with: @@ -38,17 +43,47 @@ The username is `admin`. Change its password in the TruthGate UI. Until the configuration is persisted, the bootstrap password is also retained at `data/truthgate/state/bootstrap-admin-password` with restrictive permissions. -## Pulling the published image +To build the production image locally instead of pulling the stable release: + +```bash +docker compose up --build -d +``` + +## Published image tags + +A successful promotion to the protected `stable` branch runs the complete +AMD64/ARM64 appliance, legacy-repository migration, and TLS lifecycle gates +before Docker Hub publishing starts. + +The release workflow publishes: + +```text +magiccodingman/truthgate-ipfs:stable +magiccodingman/truthgate-ipfs:latest +magiccodingman/truthgate-ipfs:0.1 +magiccodingman/truthgate-ipfs:0.1.0 +magiccodingman/truthgate-ipfs:sha- +``` + +`stable` and `latest` move to the newest successful stable release. The +major/minor series tag, such as `0.1`, also moves forward. Full semantic +versions are immutable release identifiers. `VERSION` contains the intentional +major/minor series; every successful stable promotion automatically allocates +the next patch version. + +The same multi-platform tag resolves to the correct `linux/amd64` or +`linux/arm64` image automatically. Replacing the container does not replace +mounted state. -After the multi-platform image has been published by GitHub Actions: +To update an existing deployment: ```bash docker compose pull docker compose up -d ``` -The same `master` tag resolves to the correct `linux/amd64` or `linux/arm64` -image automatically. Replacing the container does not replace mounted state. +Pin `TRUTHGATE_IMAGE` in `.env` to a full version when an installation should +not automatically follow the moving `stable` tag. ## Persistent storage contract