From 4e9a82f4259cf0f64a7332d84d2dda7dbe155578 Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 15:57:16 +0100 Subject: [PATCH 01/38] [WEB.40] Add the .NET 10 solution skeleton and NuGet admission records Step 1 of the Blazor migration. Adds the central .NET 10 solution layout with one exact SDK pin (10.0.401), central package management and the AGPL licence metadata on every project: - src/ArcForges.Web.Site (C# static Site generator skeleton) - src/ArcForges.Web.Ui (Razor class library skeleton) - src/ArcForges.Web.App (Blazor WebAssembly standalone Account/Chat profile skeleton, RunAOTCompilation false, transitive framework references off) - src/ArcForges.Web.Operations (separate-origin Blazor WebAssembly profile skeleton; no operator features) - tools/ArcForges.Web.Tooling (refuses every command until the port lands) - tests/ArcForges.Web.{Site,App,Ui,Operations,Policy}.Tests (xUnit v3 on Microsoft.Testing.Platform, bUnit for Ui; the Policy suite checks the restored NuGet closure against the admitted records, with negative cases) NuGet admission: eng/policy/dependency-policy.json gains nugetClosure (55 exact packages from the project lock files, with content hash, licence, nuspec SHA-256 and source) and the successor receipt eng/policy/dependency-reviews/web-40-admission-r1.json. Every new csproj, props, targets, global.json, NuGet.config and packages.lock.json input is hashed in the npm input map. The npm closure is unchanged. Architecture policy: the GOV.11 refusal of every Blazor target is reversed to the P2-021 rule: Blazor WebAssembly standalone is allowed, and Blazor Server render modes and circuits are refused. The fixture follows. win.slnx keeps the ArcForges.Web.esproj entry until the React parity step. Co-Authored-By: Claude Haiku 5.5 --- Directory.Build.props | 28 + Directory.Packages.props | 13 + NuGet.config | 12 + eng/policy/architecture.ts | 10 +- eng/policy/dependency-policy.json | 457 +++++++++- .../web-40-admission-r1.json | 791 ++++++++++++++++++ eng/policy/licence-boundary.json | 48 +- global.json | 10 + .../ArcForges.Web.App.csproj | 17 + src/ArcForges.Web.App/ProfileIdentity.cs | 9 + src/ArcForges.Web.App/Program.cs | 5 + src/ArcForges.Web.App/packages.lock.json | 263 ++++++ .../ArcForges.Web.Operations.csproj | 17 + .../ProfileIdentity.cs | 9 + src/ArcForges.Web.Operations/Program.cs | 5 + .../packages.lock.json | 263 ++++++ .../ArcForges.Web.Site.csproj | 10 + src/ArcForges.Web.Site/ProfileIdentity.cs | 9 + src/ArcForges.Web.Site/packages.lock.json | 13 + src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj | 10 + src/ArcForges.Web.Ui/ProfileIdentity.cs | 9 + src/ArcForges.Web.Ui/_Imports.razor | 3 + src/ArcForges.Web.Ui/packages.lock.json | 13 + .../ArcForges.Web.App.Tests.csproj | 15 + .../ProfileIdentityTests.cs | 13 + .../packages.lock.json | 376 +++++++++ .../ArcForges.Web.Operations.Tests.csproj | 15 + .../ProfileIdentityTests.cs | 13 + .../packages.lock.json | 376 +++++++++ .../ArcForges.Web.Policy.Tests.csproj | 14 + .../NuGetAdmission.cs | 31 + .../NuGetClosureAdmissionTests.cs | 92 ++ .../RepositoryRoot.cs | 20 + .../packages.lock.json | 132 +++ .../ArcForges.Web.Site.Tests.csproj | 15 + .../ProfileIdentityTests.cs | 13 + .../packages.lock.json | 135 +++ .../ArcForges.Web.Ui.Tests.csproj | 16 + .../ProfileIdentityTests.cs | 13 + .../ArcForges.Web.Ui.Tests/packages.lock.json | 454 ++++++++++ tests/unit/architecture-policy.test.ts | 3 +- .../ArcForges.Web.Tooling.csproj | 10 + tools/ArcForges.Web.Tooling/Program.cs | 13 + .../ArcForges.Web.Tooling/packages.lock.json | 13 + win.slnx | 16 + 45 files changed, 3805 insertions(+), 17 deletions(-) create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 NuGet.config create mode 100644 eng/policy/dependency-reviews/web-40-admission-r1.json create mode 100644 global.json create mode 100644 src/ArcForges.Web.App/ArcForges.Web.App.csproj create mode 100644 src/ArcForges.Web.App/ProfileIdentity.cs create mode 100644 src/ArcForges.Web.App/Program.cs create mode 100644 src/ArcForges.Web.App/packages.lock.json create mode 100644 src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj create mode 100644 src/ArcForges.Web.Operations/ProfileIdentity.cs create mode 100644 src/ArcForges.Web.Operations/Program.cs create mode 100644 src/ArcForges.Web.Operations/packages.lock.json create mode 100644 src/ArcForges.Web.Site/ArcForges.Web.Site.csproj create mode 100644 src/ArcForges.Web.Site/ProfileIdentity.cs create mode 100644 src/ArcForges.Web.Site/packages.lock.json create mode 100644 src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj create mode 100644 src/ArcForges.Web.Ui/ProfileIdentity.cs create mode 100644 src/ArcForges.Web.Ui/_Imports.razor create mode 100644 src/ArcForges.Web.Ui/packages.lock.json create mode 100644 tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj create mode 100644 tests/ArcForges.Web.App.Tests/ProfileIdentityTests.cs create mode 100644 tests/ArcForges.Web.App.Tests/packages.lock.json create mode 100644 tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj create mode 100644 tests/ArcForges.Web.Operations.Tests/ProfileIdentityTests.cs create mode 100644 tests/ArcForges.Web.Operations.Tests/packages.lock.json create mode 100644 tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj create mode 100644 tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/RepositoryRoot.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/packages.lock.json create mode 100644 tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj create mode 100644 tests/ArcForges.Web.Site.Tests/ProfileIdentityTests.cs create mode 100644 tests/ArcForges.Web.Site.Tests/packages.lock.json create mode 100644 tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj create mode 100644 tests/ArcForges.Web.Ui.Tests/ProfileIdentityTests.cs create mode 100644 tests/ArcForges.Web.Ui.Tests/packages.lock.json create mode 100644 tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj create mode 100644 tools/ArcForges.Web.Tooling/Program.cs create mode 100644 tools/ArcForges.Web.Tooling/packages.lock.json diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..cab9efd --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,28 @@ + + + + net10.0 + 14 + enable + enable + true + true + true + true + latest + true + true + true + true + true + all + false + ArcForges + https://github.com/ArcForges/Web + AGPL + AGPL-3.0-only + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..163a7be --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,13 @@ + + + + true + true + + + + + + + + diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..4e800bf --- /dev/null +++ b/NuGet.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/eng/policy/architecture.ts b/eng/policy/architecture.ts index 86bff41..41d305b 100644 --- a/eng/policy/architecture.ts +++ b/eng/policy/architecture.ts @@ -182,9 +182,15 @@ export function auditArchitecture(sources: Sources): Finding[] { report("portable-reference", file, "Managed graph references esproj"); if ( /\.(?:csproj|esproj|props|targets|slnx)$/u.test(file) && - /Blazor|Microsoft\.AspNetCore\.Components\.WebAssembly/iu.test(source) + /AddInteractiveServer|InteractiveServer|Microsoft\.AspNetCore\.Components\.Server|AddRazorComponents|Circuit/iu.test( + source, + ) ) - report("obsolete-target", file, "Obsolete target"); + report( + "obsolete-target", + file, + "Blazor Server render modes and circuits are forbidden; Blazor WebAssembly standalone is the only Blazor target", + ); if (!code.test(file)) continue; const ast = parse(source, { sourceType: "unambiguous", diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 382ecc3..6e2e78c 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "repository": "Web", - "sourceCommit": "337a3375102d4347c9663eaea1f091e2627ad838", + "sourceCommit": "a469064ffe0c497d4dd611d58bfaa1ac158656ff", "manifests": [ "apps/app/package.json", "apps/site/package.json", @@ -21,13 +21,37 @@ ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", - "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b" + "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Packages.props": "a6605a0f309646263b123de07b1e1845eb3982eecb55ab49b5b9d01e814c8135", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.App/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "fc1359d918faf269627654b2fbb8054fb84698ef87448addc9140ded90e14ed0", + "src/ArcForges.Web.Site/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "e03ffb73ea082dcb30c89511d3dcbd4b81758ab1236bc135ff388be2024c5292", + "src/ArcForges.Web.Ui/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "0fc82162fcd624282101edb68b3b6faa170b4a2d7eec677370fb7eea86436790", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "592c15bdc6e7138d910bbc60a5ebaf6383522765daffdfc27e60a78ef2509894", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "e8863d7846f106617b23115117e07746e056cb5ace697ba7841df38e8f1aaa0c", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" }, "review": { "owner": "Web dependency maintainer", - "reviewer": "Claude Opus 5.5 implementation worker w-c20261008-web-sharp; independent exact-head review pending on the pull request", + "reviewer": "w-deku-20261008-web-40 (Haiku 5.5 execution agent); independent exact-head review pending on the pull request", "date": "2026-10-08", - "rationale": "Security prerequisite for every Web pull request: npm audit --audit-level=high fails on the locked transitive sharp 0.35.4 (GHSA-wq5f-xc86-pv6w, high: librsvg CVE-2026-96889 in the bundled libvips). sharp is a development-only transitive of wrangler 4.143.1 through miniflare 5.20260926.1-alpha, which pins sharp exactly at 0.35.4 (as does the latest miniflare release), so no parent bump reaches the patched release. A root package.json override selects sharp 0.35.5, the first patched version; the lock moves only node_modules/sharp and its 26 @img platform packages (0.35.4 to 0.35.5, libvips 1.3.3 to 1.3.4) with unchanged licences, registry origin and dependency ranges. No direct dependency, other lock entry, owner or class changes. Browser profile and record r10 are immutable successors that rebind only the lock, the active-profile selector and the lock-derived build SBOM.", + "rationale": "WEB.40 step 1 admits the first .NET 10 solution skeleton for the Web repository. The C# Site generator, the shared Razor class library, the Blazor WebAssembly standalone Account/Chat and Operations profiles, the build tooling and the test projects are added with central package management and one exact .NET SDK pin (10.0.401). The NuGet closure of every project lock is recorded in nugetClosure with the exact version, the lock contentHash, the nuspec SHA-256 and the licence expression of the exact package. The only non-stable coordinate is the existing first-party ArcForges.Build.Policy 1.0.0-ci.94.1 publication (AGPL, build-only, PrivateAssets all). The npm closure, package.json, package-lock.json and every npm manifest are unchanged, and every previously admitted npm coordinate keeps its sha512 integrity. This successor records only the new csproj, props, targets, global.json, NuGet.config and packages.lock.json inputs of the .NET skeleton.", "inputs": { ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", @@ -41,7 +65,31 @@ ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", - "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b" + "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Packages.props": "a6605a0f309646263b123de07b1e1845eb3982eecb55ab49b5b9d01e814c8135", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.App/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "fc1359d918faf269627654b2fbb8054fb84698ef87448addc9140ded90e14ed0", + "src/ArcForges.Web.Site/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "e03ffb73ea082dcb30c89511d3dcbd4b81758ab1236bc135ff388be2024c5292", + "src/ArcForges.Web.Ui/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "0fc82162fcd624282101edb68b3b6faa170b4a2d7eec677370fb7eea86436790", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "592c15bdc6e7138d910bbc60a5ebaf6383522765daffdfc27e60a78ef2509894", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "e8863d7846f106617b23115117e07746e056cb5ace697ba7841df38e8f1aaa0c", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" }, "checks": [ "locked-restore", @@ -52,9 +100,9 @@ "sbom", "framework-posture" ], - "runtimeAssessment": "No framework, runtime or browser-emitted package changed: sharp is image tooling loaded only by the local Miniflare/Wrangler development server and is not emitted into browser assets or the canonical-host Worker. Offline gates only, once on the pinned Node 24.21.0 and npm 11.19.0: npm ci from the lock, npm run check and a local candidate build verified against profile r10; npm audit --audit-level=high reports no vulnerabilities on the new lock. Wrangler/Miniflare local image handling with sharp 0.35.5 was not exercised. No live, browser or installed-consumer result is claimed.", - "maintenance": "Exact producer pins and every other locked package remain unchanged. The sharp override must be removed once Miniflare's own exact pin reaches a patched sharp; until then each Wrangler update must keep the override at a patched release. Dependency review and the existing high-severity audit remain required. Lockfile licence labels are metadata, not replacement file-level redistribution grants. Updates must refresh this reviewed input-bound record and pass applicable existing class checks.", - "previous": "eng/policy/dependency-reviews/source-map-js-admission-r1.json", + "runtimeAssessment": "Step 1 produces compiled skeletons only. The Blazor WebAssembly projects compile for browser-wasm with DisableTransitiveFrameworkReferences set on the profiles, so the Microsoft.AspNetCore.App browser runtime pack, which is absent from the local SDK and NuGet cache, is not required by the skeleton build. No publish, browser run, AOT compilation or deployment is performed in this step. Native AOT, CSP, budget and browser proofs belong to the later WEB.40 steps and to PRF.11.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. Any package or version change requires a new reviewed successor receipt that names this file as its predecessor; historical receipts remain immutable. The Blazor WebAssembly and bUnit pins are on the .NET 10 line and must move together with the global.json SDK pin. Any use of the browser-wasm ASP.NET Core runtime pack, or any publish of a Blazor profile, requires its own reviewed admission first.", + "previous": "eng/policy/dependency-reviews/sharp-admission-r1.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -2189,7 +2237,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/sharp-admission-r1.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r1.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", @@ -2203,5 +2251,394 @@ "trustedRef": "refs/heads/main", "promotion": "sealed candidate; existing provider identity verification; no registry tooling publication or credential in PR" }, - "additionalFeeds": {} + "additionalFeeds": {}, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetFeed": "https://api.nuget.org/v3/index.json", + "nugetSdk": "10.0.401" } diff --git a/eng/policy/dependency-reviews/web-40-admission-r1.json b/eng/policy/dependency-reviews/web-40-admission-r1.json new file mode 100644 index 0000000..046494f --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r1.json @@ -0,0 +1,791 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-web-40 (Haiku 5.5 execution agent); independent exact-head review pending on the pull request", + "date": "2026-10-08", + "rationale": "WEB.40 step 1 admits the first .NET 10 solution skeleton for the Web repository. The C# Site generator, the shared Razor class library, the Blazor WebAssembly standalone Account/Chat and Operations profiles, the build tooling and the test projects are added with central package management and one exact .NET SDK pin (10.0.401). The NuGet closure of every project lock is recorded in nugetClosure with the exact version, the lock contentHash, the nuspec SHA-256 and the licence expression of the exact package. The only non-stable coordinate is the existing first-party ArcForges.Build.Policy 1.0.0-ci.94.1 publication (AGPL, build-only, PrivateAssets all). The npm closure, package.json, package-lock.json and every npm manifest are unchanged, and every previously admitted npm coordinate keeps its sha512 integrity. This successor records only the new csproj, props, targets, global.json, NuGet.config and packages.lock.json inputs of the .NET skeleton.", + "inputs": { + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", + "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", + "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Packages.props": "a6605a0f309646263b123de07b1e1845eb3982eecb55ab49b5b9d01e814c8135", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.App/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "fc1359d918faf269627654b2fbb8054fb84698ef87448addc9140ded90e14ed0", + "src/ArcForges.Web.Site/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "e03ffb73ea082dcb30c89511d3dcbd4b81758ab1236bc135ff388be2024c5292", + "src/ArcForges.Web.Ui/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "0fc82162fcd624282101edb68b3b6faa170b4a2d7eec677370fb7eea86436790", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "592c15bdc6e7138d910bbc60a5ebaf6383522765daffdfc27e60a78ef2509894", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "e8863d7846f106617b23115117e07746e056cb5ace697ba7841df38e8f1aaa0c", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "Step 1 produces compiled skeletons only. The Blazor WebAssembly projects compile for browser-wasm with DisableTransitiveFrameworkReferences set on the profiles, so the Microsoft.AspNetCore.App browser runtime pack, which is absent from the local SDK and NuGet cache, is not required by the skeleton build. No publish, browser run, AOT compilation or deployment is performed in this step. Native AOT, CSP, budget and browser proofs belong to the later WEB.40 steps and to PRF.11.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. Any package or version change requires a new reviewed successor receipt that names this file as its predecessor; historical receipts remain immutable. The Blazor WebAssembly and bUnit pins are on the .NET 10 line and must move together with the global.json SDK pin. Any use of the browser-wasm ASP.NET Core runtime pack, or any publish of a Blazor profile, requires its own reviewed admission first.", + "previous": "eng/policy/dependency-reviews/sharp-admission-r1.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@adobe/css-tools@4.5.0": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "@arcforges/api-client@1.0.0-ci.287.1": "sha512-Nzr25VLIi8nSRQcO46gs2QobNp8yiw51Tf+5th4ZpB19Q6DoweWVs0CYSDhbtAAH8uAIC1IzLiKywEsoD48Geg==", + "@arcforges/proto@1.0.0-ci.287.1": "sha512-lJNdv4ReiG1KF54tziiJpkHgDHS96XsO504no07Ly7cnXCqqK9O0d5o6AaufIn08K7J7C5IR+Hpbq2ZFCrL8+w==", + "@asamuzakjp/css-color@7.0.0": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", + "lru-cache@11.5.2": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "@asamuzakjp/dom-selector@9.2.0": "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw==", + "@axe-core/playwright@4.13.0": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "@babel/code-frame@7.29.7": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "@babel/compat-data@7.29.7": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "@babel/core@7.29.7": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "semver@6.3.1": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "@babel/generator@7.29.8": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "@babel/helper-compilation-targets@7.29.7": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "@babel/helper-globals@7.29.7": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "@babel/helper-module-imports@7.29.7": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "@babel/helper-module-transforms@7.29.7": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "@babel/helper-string-parser@7.29.7": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "@babel/helper-validator-identifier@7.29.7": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "@babel/helper-validator-option@7.29.7": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "@babel/helpers@7.29.7": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "@babel/parser@7.29.8": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "@babel/runtime@7.29.7": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "@babel/template@7.29.7": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "@babel/traverse@7.29.8": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "@babel/types@7.29.8": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "@biomejs/biome@2.5.14": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", + "@biomejs/cli-darwin-arm64@2.5.14": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", + "@biomejs/cli-darwin-x64@2.5.14": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", + "@biomejs/cli-linux-arm64@2.5.14": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", + "@biomejs/cli-linux-arm64-musl@2.5.14": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", + "@biomejs/cli-linux-x64@2.5.14": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", + "@biomejs/cli-linux-x64-musl@2.5.14": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", + "@biomejs/cli-win32-arm64@2.5.14": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", + "@biomejs/cli-win32-x64@2.5.14": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", + "@bramus/specificity@2.4.2": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "@bufbuild/protobuf@2.15.0": "sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==", + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@connectrpc/connect@2.2.0": "sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==", + "@connectrpc/connect-web@2.2.0": "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@csstools/color-helpers@6.1.1": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "@csstools/css-calc@3.4.0": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "@csstools/css-color-parser@4.2.3": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "@csstools/css-parser-algorithms@4.0.0": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "@csstools/css-syntax-patches-for-csstree@1.1.14": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "@csstools/css-tokenizer@4.0.1": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@exodus/bytes@1.15.1": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/gen-mapping@0.3.13": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "@jridgewell/remapping@2.3.5": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@jridgewell/trace-mapping@0.3.31": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "@oxc-project/types@0.149.0": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "@playwright/test@1.63.0": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@react-router/dev@8.4.0": "sha512-GKznpn6h8c52xGXwtsypD5ZwNiWFSeqoKPdyGq7SfG+T9rFjGO8F0i/R/yECfZqHXaNz1NVmpODuBamBy9FfMg==", + "@react-router/node@8.4.0": "sha512-1teZHtk7XDm1pteQ2Plgtvtjjup5C5sIwFlOMYVIyoOH7X3bId3Qb//kUdbydX6NRIxcGK28UnfZlz4SrD/mkg==", + "@remix-run/node-fetch-server@0.14.1": "sha512-pKenF5ysIN5lDCnCyvoUxDhKP0tfnGagvGbZh01xxHG6DJXSIXWVC9NpLSDGjxcxd1CKdCOKuZFleXBF3pChIg==", + "@remix-run/route-pattern@0.22.1": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "@rolldown/binding-android-arm-eabi@1.2.8": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "@rolldown/binding-android-arm64@1.2.8": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "@rolldown/binding-darwin-arm64@1.2.8": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "@rolldown/binding-darwin-x64@1.2.8": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "@rolldown/binding-freebsd-x64@1.2.8": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "@rolldown/binding-linux-arm-gnueabihf@1.2.8": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "@rolldown/binding-linux-arm64-gnu@1.2.8": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "@rolldown/binding-linux-arm64-musl@1.2.8": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "@rolldown/binding-linux-ppc64-gnu@1.2.8": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "@rolldown/binding-linux-s390x-gnu@1.2.8": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "@rolldown/binding-linux-x64-gnu@1.2.8": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "@rolldown/binding-linux-x64-musl@1.2.8": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "@rolldown/binding-openharmony-arm64@1.2.8": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "@rolldown/binding-win32-arm64-msvc@1.2.8": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "@rolldown/binding-win32-x64-msvc@1.2.8": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "@rolldown/pluginutils@1.0.1": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@tailwindcss/node@4.3.3": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "@tailwindcss/oxide@4.3.3": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "@tailwindcss/oxide-android-arm64@4.3.3": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "@tailwindcss/oxide-darwin-arm64@4.3.3": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "@tailwindcss/oxide-darwin-x64@4.3.3": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "@tailwindcss/oxide-freebsd-x64@4.3.3": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "@tailwindcss/oxide-linux-x64-musl@4.3.3": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "@tailwindcss/oxide-wasm32-wasi@4.3.3": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "@tailwindcss/vite@4.3.3": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "@testing-library/dom@10.4.2": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "@testing-library/jest-dom@7.0.1": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dom-accessibility-api@0.6.3": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "@testing-library/react@16.3.3": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "@types/aria-query@5.0.4": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "@types/chai@5.2.3": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "@types/deep-eql@4.0.2": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "@types/estree@1.0.9": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@types/react@19.3.0": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "@types/react-dom@19.3.0": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "@vitest/mocker@5.0.1": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "magic-string@1.4.1": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "@vitest/spy@5.0.1": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "ansi-regex@5.0.1": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "ansi-styles@5.2.0": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "aria-query@5.3.0": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "assertion-error@2.0.1": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "axe-core@4.13.0": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "babel-dead-code-elimination@1.0.12": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "baseline-browser-mapping@2.11.24": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "bidi-js@1.1.0": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "browserslist@4.29.0": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "caniuse-lite@1.0.30001810": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "chai@6.2.2": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "chokidar@5.0.0": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "confbox@0.3.1": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "convert-source-map@2.0.0": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "cookie-es@3.1.1": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "css-tree@3.2.1": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "css.escape@1.5.1": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "csstype@3.2.3": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "data-urls@7.0.0": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "whatwg-url@16.0.1": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "debug@4.4.3": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "decimal.js@10.6.0": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dedent@1.7.2": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dequal@2.0.3": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dom-accessibility-api@0.5.16": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "electron-to-chromium@1.5.429": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==", + "enhanced-resolve@5.25.1": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "entities@8.1.0": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "es-module-lexer@2.3.2": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "escalade@3.2.0": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "estree-walker@3.0.3": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "exit-hook@5.1.0": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "expect-type@1.4.0": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "exsolve@1.1.1": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "fdir@6.5.0": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "gensync@1.0.0-beta.2": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "graceful-fs@4.2.11": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "html-encoding-sniffer@6.0.0": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "indent-string@4.0.0": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "is-potential-custom-element-name@1.0.1": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "isbot@5.2.2": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "jiti@2.7.0": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "js-tokens@4.0.0": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "jsdom@30.1.0": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "jsesc@3.1.0": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "json5@2.2.3": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "lightningcss@1.32.0": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "lightningcss-android-arm64@1.32.0": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "lightningcss-darwin-arm64@1.32.0": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "lightningcss-darwin-x64@1.32.0": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "lightningcss-freebsd-x64@1.32.0": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "lightningcss-linux-arm-gnueabihf@1.32.0": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "lightningcss-linux-arm64-gnu@1.32.0": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "lightningcss-linux-arm64-musl@1.32.0": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "lightningcss-linux-x64-gnu@1.32.0": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "lightningcss-linux-x64-musl@1.32.0": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "lightningcss-win32-arm64-msvc@1.32.0": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "lightningcss-win32-x64-msvc@1.32.0": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "lodash@4.18.1": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "lru-cache@5.1.1": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "lz-string@1.5.0": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "magic-string@0.30.21": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "mdn-data@2.27.1": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "min-indent@1.0.1": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "ms@2.1.3": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "nanoid@3.3.19": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node-releases@2.0.55": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "obug@2.2.1": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "p-map@7.0.8": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "parse5@8.0.1": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "picocolors@1.1.1": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "picomatch@4.0.7": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "pkg-types@2.3.3": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "playwright@1.63.0": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "playwright-core@1.63.0": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "postcss@8.5.28": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "prettier@3.9.8": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "pretty-format@27.5.1": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "punycode@2.3.1": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "react@19.3.0": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "react-dom@19.3.0": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "react-is@17.0.2": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "react-refresh@0.18.0": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "react-router@8.4.0": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "readdirp@5.1.1": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "redent@3.0.0": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "require-from-string@2.0.2": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "rolldown@1.2.8": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "saxes@6.0.0": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "scheduler@0.28.0": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "siginfo@2.0.0": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "source-map-js@1.2.2": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "stackback@0.0.2": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "std-env@4.2.0": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "strip-indent@3.0.0": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tailwindcss@4.3.3": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "tapable@2.3.3": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "tinybench@6.1.4": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "tinyexec@1.3.0": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "tinyglobby@0.2.17": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "tldts@7.4.13": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", + "tldts-core@7.4.13": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", + "tough-cookie@6.0.2": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "tr46@6.0.0": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici@8.10.2": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "update-browserslist-db@1.3.3": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "valibot@1.5.0": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "vite@8.3.0": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "lightningcss@1.33.0": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "lightningcss-android-arm64@1.33.0": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "lightningcss-darwin-arm64@1.33.0": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "lightningcss-darwin-x64@1.33.0": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "lightningcss-freebsd-x64@1.33.0": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "lightningcss-linux-arm-gnueabihf@1.33.0": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "lightningcss-linux-arm64-gnu@1.33.0": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "lightningcss-linux-arm64-musl@1.33.0": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "lightningcss-linux-x64-gnu@1.33.0": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "lightningcss-linux-x64-musl@1.33.0": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "lightningcss-win32-arm64-msvc@1.33.0": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "lightningcss-win32-x64-msvc@1.33.0": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "vitest@5.0.1": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "w3c-xmlserializer@5.0.0": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "webidl-conversions@8.0.1": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "whatwg-mimetype@5.0.0": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "whatwg-url@17.1.1": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "why-is-node-running@2.3.0": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "xml-name-validator@5.0.0": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "xmlchars@2.2.0": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "yallist@3.1.1": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/sharp-admission-r1.json", + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + } +} diff --git a/eng/policy/licence-boundary.json b/eng/policy/licence-boundary.json index c9b8cf0..96518c7 100644 --- a/eng/policy/licence-boundary.json +++ b/eng/policy/licence-boundary.json @@ -4,10 +4,6 @@ "spdxLicense": "AGPL-3.0-only", "licenceBoundary": "AGPL", "projects": [ - { - "path": "ArcForges.Web.esproj", - "kind": "msbuild" - }, { "path": "apps/app/package.json", "kind": "npm" @@ -23,6 +19,50 @@ { "path": "packages/ui/package.json", "kind": "npm" + }, + { + "path": "ArcForges.Web.esproj", + "kind": "msbuild" + }, + { + "path": "src/ArcForges.Web.App/ArcForges.Web.App.csproj", + "kind": "msbuild" + }, + { + "path": "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj", + "kind": "msbuild" + }, + { + "path": "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj", + "kind": "msbuild" + }, + { + "path": "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj", + "kind": "msbuild" + }, + { + "path": "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj", + "kind": "msbuild" + }, + { + "path": "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj", + "kind": "msbuild" + }, + { + "path": "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj", + "kind": "msbuild" + }, + { + "path": "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj", + "kind": "msbuild" + }, + { + "path": "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj", + "kind": "msbuild" + }, + { + "path": "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj", + "kind": "msbuild" } ] } diff --git a/global.json b/global.json new file mode 100644 index 0000000..cf3f086 --- /dev/null +++ b/global.json @@ -0,0 +1,10 @@ +{ + "sdk": { + "version": "10.0.401", + "rollForward": "disable", + "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/src/ArcForges.Web.App/ArcForges.Web.App.csproj b/src/ArcForges.Web.App/ArcForges.Web.App.csproj new file mode 100644 index 0000000..03a0678 --- /dev/null +++ b/src/ArcForges.Web.App/ArcForges.Web.App.csproj @@ -0,0 +1,17 @@ + + + + false + true + + + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/src/ArcForges.Web.App/ProfileIdentity.cs b/src/ArcForges.Web.App/ProfileIdentity.cs new file mode 100644 index 0000000..788bf1e --- /dev/null +++ b/src/ArcForges.Web.App/ProfileIdentity.cs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.App; + +/// Stable identity of the Blazor WebAssembly standalone profile (WEB.40 skeleton). +public static class ProfileIdentity +{ + /// The published profile name. + public const string Name = "app"; +} diff --git a/src/ArcForges.Web.App/Program.cs b/src/ArcForges.Web.App/Program.cs new file mode 100644 index 0000000..1ac0a18 --- /dev/null +++ b/src/ArcForges.Web.App/Program.cs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +await builder.Build().RunAsync(); diff --git a/src/ArcForges.Web.App/packages.lock.json b/src/ArcForges.Web.App/packages.lock.json new file mode 100644 index 0000000..f971269 --- /dev/null +++ b/src/ArcForges.Web.App/packages.lock.json @@ -0,0 +1,263 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "Microsoft.AspNetCore.Components.WebAssembly": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.JSInterop.WebAssembly": "10.0.10" + } + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==" + }, + "Microsoft.NET.Sdk.WebAssembly.Pack": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Analyzers": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" + }, + "Microsoft.AspNetCore.Components.Forms": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.Extensions.Validation": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Web": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Forms": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10", + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.Extensions.Validation": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.JSInterop": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==" + }, + "Microsoft.JSInterop.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "dependencies": { + "Microsoft.JSInterop": "10.0.10" + } + }, + "arcforges.web.ui": { + "type": "Project" + } + }, + "net10.0/browser-wasm": {} + } +} \ No newline at end of file diff --git a/src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj b/src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj new file mode 100644 index 0000000..03a0678 --- /dev/null +++ b/src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj @@ -0,0 +1,17 @@ + + + + false + true + + + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/src/ArcForges.Web.Operations/ProfileIdentity.cs b/src/ArcForges.Web.Operations/ProfileIdentity.cs new file mode 100644 index 0000000..1f597c2 --- /dev/null +++ b/src/ArcForges.Web.Operations/ProfileIdentity.cs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Operations; + +/// Stable identity of the Blazor WebAssembly standalone profile (WEB.40 skeleton). +public static class ProfileIdentity +{ + /// The published profile name. + public const string Name = "operations"; +} diff --git a/src/ArcForges.Web.Operations/Program.cs b/src/ArcForges.Web.Operations/Program.cs new file mode 100644 index 0000000..1ac0a18 --- /dev/null +++ b/src/ArcForges.Web.Operations/Program.cs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +await builder.Build().RunAsync(); diff --git a/src/ArcForges.Web.Operations/packages.lock.json b/src/ArcForges.Web.Operations/packages.lock.json new file mode 100644 index 0000000..f971269 --- /dev/null +++ b/src/ArcForges.Web.Operations/packages.lock.json @@ -0,0 +1,263 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "Microsoft.AspNetCore.Components.WebAssembly": { + "type": "Direct", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.JSInterop.WebAssembly": "10.0.10" + } + }, + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==" + }, + "Microsoft.NET.Sdk.WebAssembly.Pack": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Analyzers": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" + }, + "Microsoft.AspNetCore.Components.Forms": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.Extensions.Validation": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Web": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Forms": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10", + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.Extensions.Validation": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.JSInterop": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==" + }, + "Microsoft.JSInterop.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "dependencies": { + "Microsoft.JSInterop": "10.0.10" + } + }, + "arcforges.web.ui": { + "type": "Project" + } + }, + "net10.0/browser-wasm": {} + } +} \ No newline at end of file diff --git a/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj b/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj new file mode 100644 index 0000000..40b3732 --- /dev/null +++ b/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj @@ -0,0 +1,10 @@ + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/src/ArcForges.Web.Site/ProfileIdentity.cs b/src/ArcForges.Web.Site/ProfileIdentity.cs new file mode 100644 index 0000000..cedab5f --- /dev/null +++ b/src/ArcForges.Web.Site/ProfileIdentity.cs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Site; + +/// Stable identity of the C# static Site generator project (WEB.40 skeleton). +public static class ProfileIdentity +{ + /// The published profile name for the static public Site. + public const string Name = "site"; +} diff --git a/src/ArcForges.Web.Site/packages.lock.json b/src/ArcForges.Web.Site/packages.lock.json new file mode 100644 index 0000000..a8b0c23 --- /dev/null +++ b/src/ArcForges.Web.Site/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + } + } + } +} \ No newline at end of file diff --git a/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj b/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj new file mode 100644 index 0000000..878ddcd --- /dev/null +++ b/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj @@ -0,0 +1,10 @@ + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/src/ArcForges.Web.Ui/ProfileIdentity.cs b/src/ArcForges.Web.Ui/ProfileIdentity.cs new file mode 100644 index 0000000..a9037a2 --- /dev/null +++ b/src/ArcForges.Web.Ui/ProfileIdentity.cs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Ui; + +/// Stable identity of the shared Razor class library (WEB.40 skeleton). +public static class ProfileIdentity +{ + /// The published library name that replaces packages/ui. + public const string Name = "ui"; +} diff --git a/src/ArcForges.Web.Ui/_Imports.razor b/src/ArcForges.Web.Ui/_Imports.razor new file mode 100644 index 0000000..b9332a2 --- /dev/null +++ b/src/ArcForges.Web.Ui/_Imports.razor @@ -0,0 +1,3 @@ +@* SPDX-License-Identifier: AGPL-3.0-only *@ +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web diff --git a/src/ArcForges.Web.Ui/packages.lock.json b/src/ArcForges.Web.Ui/packages.lock.json new file mode 100644 index 0000000..a8b0c23 --- /dev/null +++ b/src/ArcForges.Web.Ui/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + } + } + } +} \ No newline at end of file diff --git a/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj b/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj new file mode 100644 index 0000000..cda1cc9 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj @@ -0,0 +1,15 @@ + + + + Exe + true + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/tests/ArcForges.Web.App.Tests/ProfileIdentityTests.cs b/tests/ArcForges.Web.App.Tests/ProfileIdentityTests.cs new file mode 100644 index 0000000..2835b07 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/ProfileIdentityTests.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class ProfileIdentityTests +{ + [Fact] + public void ProfileNameIsStable() + { + Assert.Equal("app", ArcForges.Web.App.ProfileIdentity.Name); + } +} diff --git a/tests/ArcForges.Web.App.Tests/packages.lock.json b/tests/ArcForges.Web.App.Tests/packages.lock.json new file mode 100644 index 0000000..33e424a --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/packages.lock.json @@ -0,0 +1,376 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "xunit.v3.mtp-v2": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Analyzers": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" + }, + "Microsoft.AspNetCore.Components.Forms": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.Extensions.Validation": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Web": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Forms": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10", + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.Extensions.Validation": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.JSInterop": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==" + }, + "Microsoft.JSInterop.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "dependencies": { + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + }, + "arcforges.web.app": { + "type": "Project", + "dependencies": { + "ArcForges.Web.Ui": "[1.0.0, )", + "Microsoft.AspNetCore.Components.WebAssembly": "[10.0.10, )" + } + }, + "arcforges.web.ui": { + "type": "Project" + }, + "Microsoft.AspNetCore.Components.WebAssembly": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.JSInterop.WebAssembly": "10.0.10" + } + } + } + } +} \ No newline at end of file diff --git a/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj b/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj new file mode 100644 index 0000000..a23e80c --- /dev/null +++ b/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj @@ -0,0 +1,15 @@ + + + + Exe + true + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/tests/ArcForges.Web.Operations.Tests/ProfileIdentityTests.cs b/tests/ArcForges.Web.Operations.Tests/ProfileIdentityTests.cs new file mode 100644 index 0000000..2470d88 --- /dev/null +++ b/tests/ArcForges.Web.Operations.Tests/ProfileIdentityTests.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Xunit; + +namespace ArcForges.Web.Operations.Tests; + +public sealed class ProfileIdentityTests +{ + [Fact] + public void ProfileNameIsStable() + { + Assert.Equal("operations", ArcForges.Web.Operations.ProfileIdentity.Name); + } +} diff --git a/tests/ArcForges.Web.Operations.Tests/packages.lock.json b/tests/ArcForges.Web.Operations.Tests/packages.lock.json new file mode 100644 index 0000000..0223f59 --- /dev/null +++ b/tests/ArcForges.Web.Operations.Tests/packages.lock.json @@ -0,0 +1,376 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "xunit.v3.mtp-v2": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Analyzers": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" + }, + "Microsoft.AspNetCore.Components.Forms": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.Extensions.Validation": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Web": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Forms": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10", + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.Extensions.Validation": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.JSInterop": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==" + }, + "Microsoft.JSInterop.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "dependencies": { + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + }, + "arcforges.web.operations": { + "type": "Project", + "dependencies": { + "ArcForges.Web.Ui": "[1.0.0, )", + "Microsoft.AspNetCore.Components.WebAssembly": "[10.0.10, )" + } + }, + "arcforges.web.ui": { + "type": "Project" + }, + "Microsoft.AspNetCore.Components.WebAssembly": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.JSInterop.WebAssembly": "10.0.10" + } + } + } + } +} \ No newline at end of file diff --git a/tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj b/tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj new file mode 100644 index 0000000..7f4bea1 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj @@ -0,0 +1,14 @@ + + + + Exe + true + + + + + + AGPL + AGPL-3.0-only + + diff --git a/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs b/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs new file mode 100644 index 0000000..568197e --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; + +namespace ArcForges.Web.Policy.Tests; + +/// Compares the restored NuGet closure with the admitted closure in eng/policy (WEB.40 successor check). +public static class NuGetAdmission +{ + private static readonly string[] PermissiveLicences = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]; + + /// Returns every refusal reason; an empty list means the closure is admitted. + public static IReadOnlyList Violations(JsonObject admitted, IReadOnlyDictionary restored) + { + var violations = new List(); + foreach (var key in restored.Keys.Where(key => !admitted.ContainsKey(key))) + violations.Add("Unadmitted NuGet package: " + key); + foreach (var key in admitted.Select(entry => entry.Key).Where(key => !restored.ContainsKey(key))) + violations.Add("Admitted package missing from the restored closure: " + key); + foreach (var (key, row) in admitted.Select(entry => (entry.Key, entry.Value!.AsObject()))) + { + if (restored.TryGetValue(key, out var content) && row["contentHash"]?.GetValue() != content) + violations.Add("Content hash differs from the admitted record: " + key); + var licence = row["licence"]?.GetValue() ?? string.Empty; + var firstParty = key.StartsWith("arcforges.", StringComparison.Ordinal); + if (!PermissiveLicences.Contains(licence, StringComparer.Ordinal) + && !(firstParty && licence == "AGPL-3.0-only")) + violations.Add("Forbidden or unreviewed licence " + licence + ": " + key); + } + return violations; + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs b/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs new file mode 100644 index 0000000..8309b7a --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; +using Xunit; + +namespace ArcForges.Web.Policy.Tests; + +public sealed class NuGetClosureAdmissionTests +{ + [Fact] + public void RestoredClosureMatchesTheAdmittedRecords() + { + var root = RepositoryRoot.Find(); + var admitted = AdmittedClosure(root); + var restored = RestoredClosure(root); + Assert.Empty(NuGetAdmission.Violations(admitted, restored)); + } + + [Fact] + public void EveryAdmittedRowHasSourceAndDigestEvidence() + { + var admitted = AdmittedClosure(RepositoryRoot.Find()); + Assert.NotEmpty(admitted); + foreach (var entry in admitted) + { + var row = entry.Value!.AsObject(); + var digest = row["nuspecSha256"]?.GetValue() ?? string.Empty; + Assert.Matches("^[0-9a-f]{64}$", digest); + var source = row["source"]?.GetValue() ?? string.Empty; + Assert.StartsWith("https://api.nuget.org/v3-flatcontainer/", source, StringComparison.Ordinal); + } + } + + [Fact] + public void ChangedContentHashIsRefused() + { + var admitted = AdmittedClosure(RepositoryRoot.Find()); + var restored = admitted.ToDictionary(entry => entry.Key, entry => entry.Value!["contentHash"]!.GetValue()); + var first = restored.Keys.First(); + restored[first] = "changed-hash"; + Assert.Contains(NuGetAdmission.Violations(admitted, restored), message => message.StartsWith("Content hash differs", StringComparison.Ordinal)); + } + + [Fact] + public void UnadmittedPackageIsRefused() + { + var admitted = AdmittedClosure(RepositoryRoot.Find()); + var restored = admitted.ToDictionary(entry => entry.Key, entry => entry.Value!["contentHash"]!.GetValue()); + restored["unadmitted.example/1.0.0"] = "hash"; + Assert.Contains(NuGetAdmission.Violations(admitted, restored), message => message.StartsWith("Unadmitted NuGet package", StringComparison.Ordinal)); + } + + [Fact] + public void CopyleftThirdPartyLicenceIsRefused() + { + var admitted = new JsonObject + { + ["third.party/1.0.0"] = new JsonObject { ["contentHash"] = "h", ["licence"] = "GPL-3.0-only" }, + }; + var restored = new Dictionary { ["third.party/1.0.0"] = "h" }; + Assert.Contains(NuGetAdmission.Violations(admitted, restored), message => message.StartsWith("Forbidden or unreviewed licence", StringComparison.Ordinal)); + } + + private static JsonObject AdmittedClosure(string root) + { + var policy = JsonNode.Parse(File.ReadAllText(Path.Combine(root, "eng", "policy", "dependency-policy.json")))!.AsObject(); + return policy["nugetClosure"]!.AsObject(); + } + + private static IReadOnlyDictionary RestoredClosure(string root) + { + var closure = new Dictionary(StringComparer.Ordinal); + foreach (var lockPath in Directory.EnumerateFiles(root, "packages.lock.json", SearchOption.AllDirectories) + .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + && !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))) + { + var document = JsonNode.Parse(File.ReadAllText(lockPath))!.AsObject(); + foreach (var framework in document["dependencies"]!.AsObject()) + foreach (var package in framework.Value!.AsObject()) + { + var entry = package.Value!.AsObject(); + if (entry["type"]!.GetValue() == "Project") + continue; + var key = package.Key.ToLowerInvariant() + "/" + entry["resolved"]!.GetValue(); + var hash = entry["contentHash"]!.GetValue(); + if (closure.TryGetValue(key, out var seen)) + Assert.Equal(seen, hash); + closure[key] = hash; + } + } + return closure; + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/RepositoryRoot.cs b/tests/ArcForges.Web.Policy.Tests/RepositoryRoot.cs new file mode 100644 index 0000000..a58fa98 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/RepositoryRoot.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Policy.Tests; + +/// Locates the repository root from the test output directory. +public static class RepositoryRoot +{ + /// Walks up from the test assembly until the repository marker files are found. + public static string Find() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "win.slnx")) + && File.Exists(Path.Combine(directory.FullName, "global.json"))) + return directory.FullName; + directory = directory.Parent; + } + throw new DirectoryNotFoundException("The repository root with win.slnx and global.json was not found."); + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/packages.lock.json b/tests/ArcForges.Web.Policy.Tests/packages.lock.json new file mode 100644 index 0000000..4aa11ee --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/packages.lock.json @@ -0,0 +1,132 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "xunit.v3.mtp-v2": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + } + } + } +} \ No newline at end of file diff --git a/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj b/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj new file mode 100644 index 0000000..cb04d26 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj @@ -0,0 +1,15 @@ + + + + Exe + true + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/tests/ArcForges.Web.Site.Tests/ProfileIdentityTests.cs b/tests/ArcForges.Web.Site.Tests/ProfileIdentityTests.cs new file mode 100644 index 0000000..edd3d35 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/ProfileIdentityTests.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class ProfileIdentityTests +{ + [Fact] + public void ProfileNameIsStable() + { + Assert.Equal("site", ArcForges.Web.Site.ProfileIdentity.Name); + } +} diff --git a/tests/ArcForges.Web.Site.Tests/packages.lock.json b/tests/ArcForges.Web.Site.Tests/packages.lock.json new file mode 100644 index 0000000..239d750 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/packages.lock.json @@ -0,0 +1,135 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "xunit.v3.mtp-v2": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + }, + "arcforges.web.site": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj b/tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj new file mode 100644 index 0000000..a08be19 --- /dev/null +++ b/tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj @@ -0,0 +1,16 @@ + + + + Exe + true + + + + + + + + AGPL + AGPL-3.0-only + + diff --git a/tests/ArcForges.Web.Ui.Tests/ProfileIdentityTests.cs b/tests/ArcForges.Web.Ui.Tests/ProfileIdentityTests.cs new file mode 100644 index 0000000..0226af7 --- /dev/null +++ b/tests/ArcForges.Web.Ui.Tests/ProfileIdentityTests.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Xunit; + +namespace ArcForges.Web.Ui.Tests; + +public sealed class ProfileIdentityTests +{ + [Fact] + public void ProfileNameIsStable() + { + Assert.Equal("ui", ArcForges.Web.Ui.ProfileIdentity.Name); + } +} diff --git a/tests/ArcForges.Web.Ui.Tests/packages.lock.json b/tests/ArcForges.Web.Ui.Tests/packages.lock.json new file mode 100644 index 0000000..8fd23c3 --- /dev/null +++ b/tests/ArcForges.Web.Ui.Tests/packages.lock.json @@ -0,0 +1,454 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "bunit": { + "type": "Direct", + "requested": "[2.9.0, )", + "resolved": "2.9.0", + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "dependencies": { + "AngleSharp": "1.7.0", + "AngleSharp.Css": "1.0.1", + "AngleSharp.Diffing": "1.1.1", + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.AspNetCore.Components.WebAssembly": "10.0.10", + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Localization.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, + "xunit.v3.mtp-v2": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "dependencies": { + "xunit.analyzers": "2.1.0", + "xunit.v3.assert": "[4.0.1]", + "xunit.v3.core.mtp-v2": "[4.0.1]" + } + }, + "AngleSharp": { + "type": "Transitive", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "dependencies": { + "AngleSharp": "[1.5.0, 2.0.0)" + } + }, + "AngleSharp.Diffing": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "dependencies": { + "AngleSharp": "1.1.2", + "AngleSharp.Css": "1.0.0-beta.144" + } + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "10.0.10", + "Microsoft.Extensions.Diagnostics": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Analyzers": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" + }, + "Microsoft.AspNetCore.Components.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Forms": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.Extensions.Validation": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.Web": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "dependencies": { + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Forms": "10.0.10", + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10", + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "dependencies": { + "Microsoft.AspNetCore.Components.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Web": "10.0.10" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileProviders.Physical": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.10", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.10", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + }, + "Microsoft.Extensions.Validation": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10" + } + }, + "Microsoft.JSInterop": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==" + }, + "Microsoft.JSInterop.WebAssembly": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "dependencies": { + "Microsoft.JSInterop": "10.0.10" + } + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "2.4.0", + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "dependencies": { + "Microsoft.Testing.Platform": "[2.4.0, 3.0.0)" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v2": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "2.4.0", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.4.0", + "Microsoft.Testing.Platform": "2.4.0", + "Microsoft.Testing.Platform.MSBuild": "2.4.0", + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.inproc.console": "[4.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "dependencies": { + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "dependencies": { + "xunit.v3.extensibility.core": "[4.0.1]", + "xunit.v3.runner.common": "[4.0.1]" + } + }, + "arcforges.web.ui": { + "type": "Project" + }, + "Microsoft.AspNetCore.Components.WebAssembly": { + "type": "CentralTransitive", + "requested": "[10.0.10, )", + "resolved": "10.0.10", + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "dependencies": { + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.Extensions.Configuration.Binder": "10.0.10", + "Microsoft.Extensions.Configuration.Json": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.JSInterop.WebAssembly": "10.0.10" + } + } + } + } +} \ No newline at end of file diff --git a/tests/unit/architecture-policy.test.ts b/tests/unit/architecture-policy.test.ts index 976c0e2..e8d4ea6 100644 --- a/tests/unit/architecture-policy.test.ts +++ b/tests/unit/architecture-policy.test.ts @@ -172,7 +172,8 @@ describe("GOV.11 architecture refusal fixtures", () => { [ "obsolete-target", (s) => { - s["obsolete.csproj"] = ''; + s["obsolete.csproj"] = + ''; }, ], [ diff --git a/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj b/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj new file mode 100644 index 0000000..d208bac --- /dev/null +++ b/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj @@ -0,0 +1,10 @@ + + + + Exe + + + AGPL + AGPL-3.0-only + + diff --git a/tools/ArcForges.Web.Tooling/Program.cs b/tools/ArcForges.Web.Tooling/Program.cs new file mode 100644 index 0000000..fb97249 --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Program.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Tooling; + +/// Build tooling entry point. The TypeScript tooling ports here in WEB.40 step 2. +public static class Program +{ + /// Refuses every command until the tooling port lands; no build behaviour exists yet. + public static int Main(string[] args) + { + Console.Error.WriteLine("ArcForges.Web.Tooling: no commands are ported yet (WEB.40 step 2)."); + return 2; + } +} diff --git a/tools/ArcForges.Web.Tooling/packages.lock.json b/tools/ArcForges.Web.Tooling/packages.lock.json new file mode 100644 index 0000000..a8b0c23 --- /dev/null +++ b/tools/ArcForges.Web.Tooling/packages.lock.json @@ -0,0 +1,13 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "ArcForges.Build.Policy": { + "type": "Direct", + "requested": "[1.0.0-ci.94.1, )", + "resolved": "1.0.0-ci.94.1", + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + } + } + } +} \ No newline at end of file diff --git a/win.slnx b/win.slnx index 87eda03..084104b 100644 --- a/win.slnx +++ b/win.slnx @@ -3,4 +3,20 @@ + + + + + + + + + + + + + + + + From 61e03c3f5aabc7ac7a0a625ea686554f1cf6b521 Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 16:59:14 +0100 Subject: [PATCH 02/38] [WEB.40] Static C# Site on the first-party HtmlRenderer, with CON.07 and browser-wasm admission Step 2 of the Blazor migration. Section 10 items first: - CON.07 identity: verified from Plan ledger con-07.md (Contracts PR 74, merge ca45f36, candidate 1.0.0-ci.287.1, publication run 36966686225) and from the nuget.org flat-container index and nuspec of all twelve packable Contracts projects (each lists 1.0.0-ci.287.1, carries repository commit ca45f36 and is Apache-2.0). ArcForges.Contracts.PublicApi 1.0.0-ci.287.1 is pinned centrally and referenced by ArcForges.Web.Ui; its transitive Foundation, Google.Protobuf 3.36.1 and Grpc.Core.Api 2.84.0 are admitted. - browser-wasm admission: Microsoft.NETCore.App.Runtime.Mono.browser-wasm 10.0.12 (implicit for the BlazorWebAssembly profile) and Microsoft.NET.Runtime.WebAssembly.Sdk 10.0.12 (PackageDownload in ArcForges.Web.App.csproj), recorded in nugetPackDownloads because NuGet does not record PackageDownload items in packages.lock.json. Microsoft.AspNetCore.App.Runtime.browser-wasm is not admitted: no such package exists on nuget.org. - Receipt web-40-admission-r2.json supersedes r1 (r1 is unchanged), with the con07Identity table, browserWasmPacks and the input digests of every changed csproj, props and packages.lock.json. Static Site (src/ArcForges.Web.Site, src/ArcForges.Web.Ui): - Public pages (/, /hello, /cloud-hello) are Razor components rendered with Microsoft.AspNetCore.Components.Web.HtmlRenderer. The markup reproduces the React prerender content: titles, descriptions, shell, copy, disabled initial controls, the default greeting and the noscript notices. No script element, no event-handler attribute and no framework reference is emitted (TB-01). Blazor internal event markers are removed from the static markup. - The stylesheet is one content-hashed asset (Tailwind preflight equivalent plus the packages/ui styles). The 404 page, robots.txt, favicon and 404.css are the React public files, byte for byte. - The Content-Security-Policy is derived from the emitted pages with the React rule (script-src 'self' plus the SHA-256 of every inline script body; no unsafe-inline, unsafe-eval or wasm-unsafe-eval; an external script is refused). _headers carries the exact React securityHeaders text. - Greeting and connection logic are ports of apps/site/app/hello.ts and cloud-hello.ts: the protobuf SayHello round trip, the JavaScript trim set, the 80-code-point limit, the control-character refusal, the exact failure text, the same-origin gRPC-Web unary call with no credentials, no redirect, a ten-second deadline and a reply size limit. - The build is deterministic: two builds of the same inputs produce the same bytes (tested). Tests: xUnit ports of the site unit cases (GreetingTests, ServerConnectionTests, bUnit HelloExampleTests and CloudHelloExampleTests), plus determinism, no-script, header, CSP, inventory and framing failure tests. The Policy suite gains the pack-download admission check. Results: Site 65 passed, Ui 7 passed, Policy 9 passed (local dotnet 10.0.401 build and run). Not in this step (recorded as remaining): apps/site and the React tests are kept until byte and content parity is shown; the interactive hello and connection checks need the Blazor App profile (no runtime JavaScript on public pages); Node/vitest and the Playwright suites were not run locally (no node_modules, hosted CI authoritative). Co-Authored-By: Claude Haiku 5.5 --- Directory.Packages.props | 1 + eng/policy/dependency-policy.json | 98 +- .../web-40-admission-r2.json | 951 ++++++++++++++++++ .../ArcForges.Web.App.csproj | 4 + src/ArcForges.Web.App/packages.lock.json | 34 +- .../ArcForges.Web.Site.csproj | 8 +- src/ArcForges.Web.Site/Assets/404.css | 29 + src/ArcForges.Web.Site/Assets/404.html | 19 + src/ArcForges.Web.Site/Assets/favicon.svg | 1 + src/ArcForges.Web.Site/Assets/robots.txt | 2 + src/ArcForges.Web.Site/Assets/site.css | 396 ++++++++ src/ArcForges.Web.Site/SiteBuilder.cs | 91 ++ .../SiteContentSecurityPolicy.cs | 58 ++ src/ArcForges.Web.Site/SiteDocument.razor | 23 + src/ArcForges.Web.Site/SiteOutput.cs | 28 + src/ArcForges.Web.Site/SitePages.cs | 19 + src/ArcForges.Web.Site/SiteSecurityHeaders.cs | 33 + src/ArcForges.Web.Site/StaticAssets.cs | 19 + src/ArcForges.Web.Site/StaticMarkup.cs | 18 + src/ArcForges.Web.Site/_Imports.razor | 4 + src/ArcForges.Web.Site/packages.lock.json | 35 + src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj | 3 + src/ArcForges.Web.Ui/Arrow.razor | 1 + src/ArcForges.Web.Ui/Button.razor | 11 + src/ArcForges.Web.Ui/CloudHelloExample.razor | 83 ++ src/ArcForges.Web.Ui/GrpcWebFrames.cs | 79 ++ src/ArcForges.Web.Ui/HelloExample.razor | 35 + src/ArcForges.Web.Ui/HelloGreeting.cs | 87 ++ src/ArcForges.Web.Ui/HomeContent.razor | 1 + src/ArcForges.Web.Ui/ServerConnection.cs | 120 +++ src/ArcForges.Web.Ui/Shell.razor | 11 + src/ArcForges.Web.Ui/packages.lock.json | 29 + .../packages.lock.json | 34 +- .../NuGetPackDownloadAdmissionTests.cs | 90 ++ .../ContentSecurityPolicyTests.cs | 88 ++ .../ArcForges.Web.Site.Tests/GreetingTests.cs | 50 + .../GrpcWebFixture.cs | 48 + .../GrpcWebFramesTests.cs | 89 ++ .../ServerConnectionTests.cs | 154 +++ .../SiteOutputTests.cs | 245 +++++ .../packages.lock.json | 40 +- .../CloudHelloExampleTests.cs | 46 + .../HelloExampleTests.cs | 53 + .../ArcForges.Web.Ui.Tests/packages.lock.json | 34 +- 44 files changed, 3271 insertions(+), 31 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r2.json create mode 100644 src/ArcForges.Web.Site/Assets/404.css create mode 100644 src/ArcForges.Web.Site/Assets/404.html create mode 100644 src/ArcForges.Web.Site/Assets/favicon.svg create mode 100644 src/ArcForges.Web.Site/Assets/robots.txt create mode 100644 src/ArcForges.Web.Site/Assets/site.css create mode 100644 src/ArcForges.Web.Site/SiteBuilder.cs create mode 100644 src/ArcForges.Web.Site/SiteContentSecurityPolicy.cs create mode 100644 src/ArcForges.Web.Site/SiteDocument.razor create mode 100644 src/ArcForges.Web.Site/SiteOutput.cs create mode 100644 src/ArcForges.Web.Site/SitePages.cs create mode 100644 src/ArcForges.Web.Site/SiteSecurityHeaders.cs create mode 100644 src/ArcForges.Web.Site/StaticAssets.cs create mode 100644 src/ArcForges.Web.Site/StaticMarkup.cs create mode 100644 src/ArcForges.Web.Site/_Imports.razor create mode 100644 src/ArcForges.Web.Ui/Arrow.razor create mode 100644 src/ArcForges.Web.Ui/Button.razor create mode 100644 src/ArcForges.Web.Ui/CloudHelloExample.razor create mode 100644 src/ArcForges.Web.Ui/GrpcWebFrames.cs create mode 100644 src/ArcForges.Web.Ui/HelloExample.razor create mode 100644 src/ArcForges.Web.Ui/HelloGreeting.cs create mode 100644 src/ArcForges.Web.Ui/HomeContent.razor create mode 100644 src/ArcForges.Web.Ui/ServerConnection.cs create mode 100644 src/ArcForges.Web.Ui/Shell.razor create mode 100644 tests/ArcForges.Web.Policy.Tests/NuGetPackDownloadAdmissionTests.cs create mode 100644 tests/ArcForges.Web.Site.Tests/ContentSecurityPolicyTests.cs create mode 100644 tests/ArcForges.Web.Site.Tests/GreetingTests.cs create mode 100644 tests/ArcForges.Web.Site.Tests/GrpcWebFixture.cs create mode 100644 tests/ArcForges.Web.Site.Tests/GrpcWebFramesTests.cs create mode 100644 tests/ArcForges.Web.Site.Tests/ServerConnectionTests.cs create mode 100644 tests/ArcForges.Web.Site.Tests/SiteOutputTests.cs create mode 100644 tests/ArcForges.Web.Ui.Tests/CloudHelloExampleTests.cs create mode 100644 tests/ArcForges.Web.Ui.Tests/HelloExampleTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 163a7be..8aa135b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 6e2e78c..4a3b685 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -23,27 +23,27 @@ "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", - "Directory.Packages.props": "a6605a0f309646263b123de07b1e1845eb3982eecb55ab49b5b9d01e814c8135", + "Directory.Packages.props": "1f12df7c9a4f0be605147f19028a2909a4e5175e4b7d6536f053cfa7c27ecca6", "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", - "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", - "src/ArcForges.Web.App/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "72cd82a690be6cfa33c5435a43222deec18ba5d00bc34355092f5ed4d1178707", + "src/ArcForges.Web.App/packages.lock.json": "1b43bb0452f1e2afab5139ad6ea8b30a872097f0293ba7fae3d2ec1c4291329d", "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", - "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "fc1359d918faf269627654b2fbb8054fb84698ef87448addc9140ded90e14ed0", - "src/ArcForges.Web.Site/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", - "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "e03ffb73ea082dcb30c89511d3dcbd4b81758ab1236bc135ff388be2024c5292", - "src/ArcForges.Web.Ui/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", - "tests/ArcForges.Web.App.Tests/packages.lock.json": "0fc82162fcd624282101edb68b3b6faa170b4a2d7eec677370fb7eea86436790", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "c8b392108a5fdb80ef8ed7abeb81e7636f97b5945b41a52f46fea2e20bb8b49e", "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", - "tests/ArcForges.Web.Site.Tests/packages.lock.json": "592c15bdc6e7138d910bbc60a5ebaf6383522765daffdfc27e60a78ef2509894", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", - "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "e8863d7846f106617b23115117e07746e056cb5ace697ba7841df38e8f1aaa0c", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" }, @@ -51,7 +51,7 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-web-40 (Haiku 5.5 execution agent); independent exact-head review pending on the pull request", "date": "2026-10-08", - "rationale": "WEB.40 step 1 admits the first .NET 10 solution skeleton for the Web repository. The C# Site generator, the shared Razor class library, the Blazor WebAssembly standalone Account/Chat and Operations profiles, the build tooling and the test projects are added with central package management and one exact .NET SDK pin (10.0.401). The NuGet closure of every project lock is recorded in nugetClosure with the exact version, the lock contentHash, the nuspec SHA-256 and the licence expression of the exact package. The only non-stable coordinate is the existing first-party ArcForges.Build.Policy 1.0.0-ci.94.1 publication (AGPL, build-only, PrivateAssets all). The npm closure, package.json, package-lock.json and every npm manifest are unchanged, and every previously admitted npm coordinate keeps its sha512 integrity. This successor records only the new csproj, props, targets, global.json, NuGet.config and packages.lock.json inputs of the .NET skeleton.", + "rationale": "WEB.40 step 2 (static Site) completes two section 10 items before the Site code is admitted. (a) CON.07 identity: the NuGet identity is verified from the Plan ledger record con-07.md (Contracts PR 74, merge ca45f36cccbdd31380f76b8f6cdecc958fdcb430, candidate 1.0.0-ci.287.1, publication run 36966686225) and from the nuget.org flat-container indexes and nuspecs of the twelve packable Contracts projects; every one lists 1.0.0-ci.287.1, carries the repository commit ca45f36 and is Apache-2.0 (the table is in the con07Identity field of this receipt). Only the four packages that the Site and the Ui class library restore are admitted: ArcForges.Contracts.PublicApi (direct, Ui), ArcForges.Contracts.Foundation, Google.Protobuf 3.36.1 and Grpc.Core.Api 2.84.0 (transitive). The Contracts are published packages consumed by exact version, never imported from the Contracts source tree. (b) Browser-wasm packs: the Mono browser-wasm runtime pack and the WebAssembly SDK pack are admitted at 10.0.12, the SDK 10.0.401 bundled runtime version, in the nugetPackDownloads section because NuGet records PackageDownload items outside packages.lock.json. The first is added implicitly for the BlazorWebAssembly profile, the second is declared in ArcForges.Web.App.csproj. Microsoft.AspNetCore.App.Runtime.browser-wasm is not admitted because no such package exists on nuget.org (flat-container BlobNotFound on 2026-10-08); the ASP.NET Core browser pieces ship in the framework reference. The npm closure, package.json, package-lock.json and every npm manifest are unchanged. Every changed csproj, props and packages.lock.json input is re-hashed in inputs.", "inputs": { ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", @@ -67,27 +67,27 @@ "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", - "Directory.Packages.props": "a6605a0f309646263b123de07b1e1845eb3982eecb55ab49b5b9d01e814c8135", + "Directory.Packages.props": "1f12df7c9a4f0be605147f19028a2909a4e5175e4b7d6536f053cfa7c27ecca6", "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", - "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", - "src/ArcForges.Web.App/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "72cd82a690be6cfa33c5435a43222deec18ba5d00bc34355092f5ed4d1178707", + "src/ArcForges.Web.App/packages.lock.json": "1b43bb0452f1e2afab5139ad6ea8b30a872097f0293ba7fae3d2ec1c4291329d", "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", - "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "fc1359d918faf269627654b2fbb8054fb84698ef87448addc9140ded90e14ed0", - "src/ArcForges.Web.Site/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", - "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "e03ffb73ea082dcb30c89511d3dcbd4b81758ab1236bc135ff388be2024c5292", - "src/ArcForges.Web.Ui/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", - "tests/ArcForges.Web.App.Tests/packages.lock.json": "0fc82162fcd624282101edb68b3b6faa170b4a2d7eec677370fb7eea86436790", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "c8b392108a5fdb80ef8ed7abeb81e7636f97b5945b41a52f46fea2e20bb8b49e", "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", - "tests/ArcForges.Web.Site.Tests/packages.lock.json": "592c15bdc6e7138d910bbc60a5ebaf6383522765daffdfc27e60a78ef2509894", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", - "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "e8863d7846f106617b23115117e07746e056cb5ace697ba7841df38e8f1aaa0c", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" }, @@ -100,9 +100,9 @@ "sbom", "framework-posture" ], - "runtimeAssessment": "Step 1 produces compiled skeletons only. The Blazor WebAssembly projects compile for browser-wasm with DisableTransitiveFrameworkReferences set on the profiles, so the Microsoft.AspNetCore.App browser runtime pack, which is absent from the local SDK and NuGet cache, is not required by the skeleton build. No publish, browser run, AOT compilation or deployment is performed in this step. Native AOT, CSP, budget and browser proofs belong to the later WEB.40 steps and to PRF.11.", - "maintenance": "Each NuGet coordinate is exact and content-hashed. Any package or version change requires a new reviewed successor receipt that names this file as its predecessor; historical receipts remain immutable. The Blazor WebAssembly and bUnit pins are on the .NET 10 line and must move together with the global.json SDK pin. Any use of the browser-wasm ASP.NET Core runtime pack, or any publish of a Blazor profile, requires its own reviewed admission first.", - "previous": "eng/policy/dependency-reviews/sharp-admission-r1.json", + "runtimeAssessment": "Step 2 restores and compiles the Razor class library, the static Site generator and their xUnit and bUnit test projects against the admitted closure. No browser-wasm build, publish, AOT compilation, browser run or deployment is performed by this admission. The browser-wasm packs are admitted for the later Blazor profile steps and are not executed here.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r1.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -2237,7 +2237,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r1.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r2.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", @@ -2281,6 +2281,20 @@ "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, "bunit/2.9.0": { "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", "licence": "MIT", @@ -2288,6 +2302,20 @@ "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, "microsoft.applicationinsights/2.23.0": { "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", "licence": "MIT", @@ -2640,5 +2668,23 @@ } }, "nugetFeed": "https://api.nuget.org/v3/index.json", - "nugetSdk": "10.0.401" + "nugetSdk": "10.0.401", + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } } diff --git a/eng/policy/dependency-reviews/web-40-admission-r2.json b/eng/policy/dependency-reviews/web-40-admission-r2.json new file mode 100644 index 0000000..1deaaa9 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r2.json @@ -0,0 +1,951 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-web-40 (Haiku 5.5 execution agent); independent exact-head review pending on the pull request", + "date": "2026-10-08", + "rationale": "WEB.40 step 2 (static Site) completes two section 10 items before the Site code is admitted. (a) CON.07 identity: the NuGet identity is verified from the Plan ledger record con-07.md (Contracts PR 74, merge ca45f36cccbdd31380f76b8f6cdecc958fdcb430, candidate 1.0.0-ci.287.1, publication run 36966686225) and from the nuget.org flat-container indexes and nuspecs of the twelve packable Contracts projects; every one lists 1.0.0-ci.287.1, carries the repository commit ca45f36 and is Apache-2.0 (the table is in the con07Identity field of this receipt). Only the four packages that the Site and the Ui class library restore are admitted: ArcForges.Contracts.PublicApi (direct, Ui), ArcForges.Contracts.Foundation, Google.Protobuf 3.36.1 and Grpc.Core.Api 2.84.0 (transitive). The Contracts are published packages consumed by exact version, never imported from the Contracts source tree. (b) Browser-wasm packs: the Mono browser-wasm runtime pack and the WebAssembly SDK pack are admitted at 10.0.12, the SDK 10.0.401 bundled runtime version, in the nugetPackDownloads section because NuGet records PackageDownload items outside packages.lock.json. The first is added implicitly for the BlazorWebAssembly profile, the second is declared in ArcForges.Web.App.csproj. Microsoft.AspNetCore.App.Runtime.browser-wasm is not admitted because no such package exists on nuget.org (flat-container BlobNotFound on 2026-10-08); the ASP.NET Core browser pieces ship in the framework reference. The npm closure, package.json, package-lock.json and every npm manifest are unchanged. Every changed csproj, props and packages.lock.json input is re-hashed in inputs.", + "inputs": { + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", + "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", + "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Packages.props": "1f12df7c9a4f0be605147f19028a2909a4e5175e4b7d6536f053cfa7c27ecca6", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "72cd82a690be6cfa33c5435a43222deec18ba5d00bc34355092f5ed4d1178707", + "src/ArcForges.Web.App/packages.lock.json": "1b43bb0452f1e2afab5139ad6ea8b30a872097f0293ba7fae3d2ec1c4291329d", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "c8b392108a5fdb80ef8ed7abeb81e7636f97b5945b41a52f46fea2e20bb8b49e", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "Step 2 restores and compiles the Razor class library, the static Site generator and their xUnit and bUnit test projects against the admitted closure. No browser-wasm build, publish, AOT compilation, browser run or deployment is performed by this admission. The browser-wasm packs are admitted for the later Blazor profile steps and are not executed here.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r1.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@adobe/css-tools@4.5.0": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "@arcforges/api-client@1.0.0-ci.287.1": "sha512-Nzr25VLIi8nSRQcO46gs2QobNp8yiw51Tf+5th4ZpB19Q6DoweWVs0CYSDhbtAAH8uAIC1IzLiKywEsoD48Geg==", + "@arcforges/proto@1.0.0-ci.287.1": "sha512-lJNdv4ReiG1KF54tziiJpkHgDHS96XsO504no07Ly7cnXCqqK9O0d5o6AaufIn08K7J7C5IR+Hpbq2ZFCrL8+w==", + "@asamuzakjp/css-color@7.0.0": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", + "lru-cache@11.5.2": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "@asamuzakjp/dom-selector@9.2.0": "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw==", + "@axe-core/playwright@4.13.0": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "@babel/code-frame@7.29.7": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "@babel/compat-data@7.29.7": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "@babel/core@7.29.7": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "semver@6.3.1": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "@babel/generator@7.29.8": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "@babel/helper-compilation-targets@7.29.7": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "@babel/helper-globals@7.29.7": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "@babel/helper-module-imports@7.29.7": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "@babel/helper-module-transforms@7.29.7": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "@babel/helper-string-parser@7.29.7": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "@babel/helper-validator-identifier@7.29.7": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "@babel/helper-validator-option@7.29.7": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "@babel/helpers@7.29.7": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "@babel/parser@7.29.8": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "@babel/runtime@7.29.7": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "@babel/template@7.29.7": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "@babel/traverse@7.29.8": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "@babel/types@7.29.8": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "@biomejs/biome@2.5.14": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", + "@biomejs/cli-darwin-arm64@2.5.14": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", + "@biomejs/cli-darwin-x64@2.5.14": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", + "@biomejs/cli-linux-arm64@2.5.14": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", + "@biomejs/cli-linux-arm64-musl@2.5.14": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", + "@biomejs/cli-linux-x64@2.5.14": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", + "@biomejs/cli-linux-x64-musl@2.5.14": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", + "@biomejs/cli-win32-arm64@2.5.14": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", + "@biomejs/cli-win32-x64@2.5.14": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", + "@bramus/specificity@2.4.2": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "@bufbuild/protobuf@2.15.0": "sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==", + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@connectrpc/connect@2.2.0": "sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==", + "@connectrpc/connect-web@2.2.0": "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@csstools/color-helpers@6.1.1": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "@csstools/css-calc@3.4.0": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "@csstools/css-color-parser@4.2.3": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "@csstools/css-parser-algorithms@4.0.0": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "@csstools/css-syntax-patches-for-csstree@1.1.14": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "@csstools/css-tokenizer@4.0.1": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@exodus/bytes@1.15.1": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/gen-mapping@0.3.13": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "@jridgewell/remapping@2.3.5": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@jridgewell/trace-mapping@0.3.31": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "@oxc-project/types@0.149.0": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "@playwright/test@1.63.0": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@react-router/dev@8.4.0": "sha512-GKznpn6h8c52xGXwtsypD5ZwNiWFSeqoKPdyGq7SfG+T9rFjGO8F0i/R/yECfZqHXaNz1NVmpODuBamBy9FfMg==", + "@react-router/node@8.4.0": "sha512-1teZHtk7XDm1pteQ2Plgtvtjjup5C5sIwFlOMYVIyoOH7X3bId3Qb//kUdbydX6NRIxcGK28UnfZlz4SrD/mkg==", + "@remix-run/node-fetch-server@0.14.1": "sha512-pKenF5ysIN5lDCnCyvoUxDhKP0tfnGagvGbZh01xxHG6DJXSIXWVC9NpLSDGjxcxd1CKdCOKuZFleXBF3pChIg==", + "@remix-run/route-pattern@0.22.1": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "@rolldown/binding-android-arm-eabi@1.2.8": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "@rolldown/binding-android-arm64@1.2.8": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "@rolldown/binding-darwin-arm64@1.2.8": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "@rolldown/binding-darwin-x64@1.2.8": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "@rolldown/binding-freebsd-x64@1.2.8": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "@rolldown/binding-linux-arm-gnueabihf@1.2.8": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "@rolldown/binding-linux-arm64-gnu@1.2.8": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "@rolldown/binding-linux-arm64-musl@1.2.8": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "@rolldown/binding-linux-ppc64-gnu@1.2.8": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "@rolldown/binding-linux-s390x-gnu@1.2.8": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "@rolldown/binding-linux-x64-gnu@1.2.8": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "@rolldown/binding-linux-x64-musl@1.2.8": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "@rolldown/binding-openharmony-arm64@1.2.8": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "@rolldown/binding-win32-arm64-msvc@1.2.8": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "@rolldown/binding-win32-x64-msvc@1.2.8": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "@rolldown/pluginutils@1.0.1": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@tailwindcss/node@4.3.3": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "@tailwindcss/oxide@4.3.3": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "@tailwindcss/oxide-android-arm64@4.3.3": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "@tailwindcss/oxide-darwin-arm64@4.3.3": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "@tailwindcss/oxide-darwin-x64@4.3.3": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "@tailwindcss/oxide-freebsd-x64@4.3.3": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "@tailwindcss/oxide-linux-x64-musl@4.3.3": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "@tailwindcss/oxide-wasm32-wasi@4.3.3": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "@tailwindcss/vite@4.3.3": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "@testing-library/dom@10.4.2": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "@testing-library/jest-dom@7.0.1": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dom-accessibility-api@0.6.3": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "@testing-library/react@16.3.3": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "@types/aria-query@5.0.4": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "@types/chai@5.2.3": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "@types/deep-eql@4.0.2": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "@types/estree@1.0.9": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@types/react@19.3.0": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "@types/react-dom@19.3.0": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "@vitest/mocker@5.0.1": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "magic-string@1.4.1": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "@vitest/spy@5.0.1": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "ansi-regex@5.0.1": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "ansi-styles@5.2.0": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "aria-query@5.3.0": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "assertion-error@2.0.1": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "axe-core@4.13.0": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "babel-dead-code-elimination@1.0.12": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "baseline-browser-mapping@2.11.24": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "bidi-js@1.1.0": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "browserslist@4.29.0": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "caniuse-lite@1.0.30001810": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "chai@6.2.2": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "chokidar@5.0.0": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "confbox@0.3.1": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "convert-source-map@2.0.0": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "cookie-es@3.1.1": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "css-tree@3.2.1": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "css.escape@1.5.1": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "csstype@3.2.3": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "data-urls@7.0.0": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "whatwg-url@16.0.1": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "debug@4.4.3": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "decimal.js@10.6.0": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dedent@1.7.2": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dequal@2.0.3": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dom-accessibility-api@0.5.16": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "electron-to-chromium@1.5.429": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==", + "enhanced-resolve@5.25.1": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "entities@8.1.0": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "es-module-lexer@2.3.2": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "escalade@3.2.0": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "estree-walker@3.0.3": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "exit-hook@5.1.0": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "expect-type@1.4.0": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "exsolve@1.1.1": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "fdir@6.5.0": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "gensync@1.0.0-beta.2": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "graceful-fs@4.2.11": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "html-encoding-sniffer@6.0.0": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "indent-string@4.0.0": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "is-potential-custom-element-name@1.0.1": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "isbot@5.2.2": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "jiti@2.7.0": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "js-tokens@4.0.0": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "jsdom@30.1.0": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "jsesc@3.1.0": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "json5@2.2.3": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "lightningcss@1.32.0": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "lightningcss-android-arm64@1.32.0": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "lightningcss-darwin-arm64@1.32.0": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "lightningcss-darwin-x64@1.32.0": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "lightningcss-freebsd-x64@1.32.0": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "lightningcss-linux-arm-gnueabihf@1.32.0": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "lightningcss-linux-arm64-gnu@1.32.0": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "lightningcss-linux-arm64-musl@1.32.0": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "lightningcss-linux-x64-gnu@1.32.0": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "lightningcss-linux-x64-musl@1.32.0": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "lightningcss-win32-arm64-msvc@1.32.0": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "lightningcss-win32-x64-msvc@1.32.0": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "lodash@4.18.1": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "lru-cache@5.1.1": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "lz-string@1.5.0": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "magic-string@0.30.21": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "mdn-data@2.27.1": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "min-indent@1.0.1": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "ms@2.1.3": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "nanoid@3.3.19": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node-releases@2.0.55": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "obug@2.2.1": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "p-map@7.0.8": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "parse5@8.0.1": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "picocolors@1.1.1": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "picomatch@4.0.7": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "pkg-types@2.3.3": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "playwright@1.63.0": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "playwright-core@1.63.0": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "postcss@8.5.28": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "prettier@3.9.8": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "pretty-format@27.5.1": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "punycode@2.3.1": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "react@19.3.0": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "react-dom@19.3.0": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "react-is@17.0.2": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "react-refresh@0.18.0": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "react-router@8.4.0": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "readdirp@5.1.1": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "redent@3.0.0": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "require-from-string@2.0.2": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "rolldown@1.2.8": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "saxes@6.0.0": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "scheduler@0.28.0": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "siginfo@2.0.0": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "source-map-js@1.2.2": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "stackback@0.0.2": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "std-env@4.2.0": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "strip-indent@3.0.0": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tailwindcss@4.3.3": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "tapable@2.3.3": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "tinybench@6.1.4": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "tinyexec@1.3.0": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "tinyglobby@0.2.17": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "tldts@7.4.13": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", + "tldts-core@7.4.13": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", + "tough-cookie@6.0.2": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "tr46@6.0.0": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici@8.10.2": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "update-browserslist-db@1.3.3": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "valibot@1.5.0": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "vite@8.3.0": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "lightningcss@1.33.0": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "lightningcss-android-arm64@1.33.0": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "lightningcss-darwin-arm64@1.33.0": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "lightningcss-darwin-x64@1.33.0": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "lightningcss-freebsd-x64@1.33.0": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "lightningcss-linux-arm-gnueabihf@1.33.0": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "lightningcss-linux-arm64-gnu@1.33.0": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "lightningcss-linux-arm64-musl@1.33.0": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "lightningcss-linux-x64-gnu@1.33.0": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "lightningcss-linux-x64-musl@1.33.0": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "lightningcss-win32-arm64-msvc@1.33.0": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "lightningcss-win32-x64-msvc@1.33.0": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "vitest@5.0.1": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "w3c-xmlserializer@5.0.0": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "webidl-conversions@8.0.1": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "whatwg-mimetype@5.0.0": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "whatwg-url@17.1.1": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "why-is-node-running@2.3.0": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "xml-name-validator@5.0.0": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "xmlchars@2.2.0": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "yallist@3.1.1": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r1.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/src/ArcForges.Web.App/ArcForges.Web.App.csproj b/src/ArcForges.Web.App/ArcForges.Web.App.csproj index 03a0678..68c8fc9 100644 --- a/src/ArcForges.Web.App/ArcForges.Web.App.csproj +++ b/src/ArcForges.Web.App/ArcForges.Web.App.csproj @@ -7,6 +7,10 @@ + + + + diff --git a/src/ArcForges.Web.App/packages.lock.json b/src/ArcForges.Web.App/packages.lock.json index f971269..b030892 100644 --- a/src/ArcForges.Web.App/packages.lock.json +++ b/src/ArcForges.Web.App/packages.lock.json @@ -33,6 +33,24 @@ "resolved": "10.0.12", "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==" }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "10.0.10", @@ -255,7 +273,21 @@ } }, "arcforges.web.ui": { - "type": "Project" + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } } }, "net10.0/browser-wasm": {} diff --git a/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj b/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj index 40b3732..6ee309e 100644 --- a/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj +++ b/src/ArcForges.Web.Site/ArcForges.Web.Site.csproj @@ -1,8 +1,14 @@ - + + + + + + + AGPL AGPL-3.0-only diff --git a/src/ArcForges.Web.Site/Assets/404.css b/src/ArcForges.Web.Site/Assets/404.css new file mode 100644 index 0000000..f1f9891 --- /dev/null +++ b/src/ArcForges.Web.Site/Assets/404.css @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: AGPL-3.0-only */ +body { + margin: 0; + background: #f4f3ed; + color: #1e2824; + font-family: "Segoe UI", sans-serif; +} +main { + max-width: 700px; + margin: 15vh auto; + padding: 32px; +} +h1 { + font-size: clamp(40px, 8vw, 64px); + letter-spacing: -2px; + font-weight: 450; +} +p { + line-height: 1.7; +} +a { + color: #235844; + display: inline-block; + margin-top: 24px; +} +a:focus-visible { + outline: 3px solid #235844; + outline-offset: 5px; +} diff --git a/src/ArcForges.Web.Site/Assets/404.html b/src/ArcForges.Web.Site/Assets/404.html new file mode 100644 index 0000000..ecf56d2 --- /dev/null +++ b/src/ArcForges.Web.Site/Assets/404.html @@ -0,0 +1,19 @@ + + + + + + + Page not found — ArcForges + + + + +
+

ARCFORGES

+

Page not found.

+

This page is not part of the preview.

+ ← Back home +
+ + diff --git a/src/ArcForges.Web.Site/Assets/favicon.svg b/src/ArcForges.Web.Site/Assets/favicon.svg new file mode 100644 index 0000000..e2325c1 --- /dev/null +++ b/src/ArcForges.Web.Site/Assets/favicon.svg @@ -0,0 +1 @@ + diff --git a/src/ArcForges.Web.Site/Assets/robots.txt b/src/ArcForges.Web.Site/Assets/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/src/ArcForges.Web.Site/Assets/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/src/ArcForges.Web.Site/Assets/site.css b/src/ArcForges.Web.Site/Assets/site.css new file mode 100644 index 0000000..915481d --- /dev/null +++ b/src/ArcForges.Web.Site/Assets/site.css @@ -0,0 +1,396 @@ +/* SPDX-License-Identifier: AGPL-3.0-only */ +/* Reset equivalent to the Tailwind CSS v4 preflight used by the React Site build (packages/ui/src/styles.css). */ +*,::after,::before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid} +html,:host{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent} +hr{height:0;color:inherit;border-top-width:1px} +abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted} +h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit} +a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit} +b,strong{font-weight:bolder} +code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em} +small{font-size:80%} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} +sub{bottom:-.25em} +sup{top:-.5em} +table{text-indent:0;border-color:inherit;border-collapse:collapse} +:-moz-focusring{outline:auto} +progress{vertical-align:baseline} +summary{display:list-item} +ol,ul,menu{list-style:none} +img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle} +img,video{max-width:100%;height:auto} +button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:transparent;opacity:1} +::file-selector-button{margin-inline-end:4px} +::placeholder{opacity:1} +textarea{resize:vertical} +::-webkit-search-decoration{-webkit-appearance:none} +::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto} +button,input:where([type=button],[type=reset],[type=submit]),::file-selector-button{appearance:button} +button,[role="button"]{cursor:pointer} +:disabled{cursor:default} +[hidden]:where(:not([hidden=until-found])){display:none!important} +:root{--font-sans:"Segoe UI","Helvetica Neue",Arial,sans-serif} +/* SPDX-License-Identifier: AGPL-3.0-only */ + + +:root { + font-family: var(--font-sans); + color: #1e2824; + background: #f4f3ed; + color-scheme: light; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-width: 320px; +} +button, +input { + font: inherit; +} +a { + color: inherit; + text-decoration: none; +} +a:hover { + text-decoration: underline; + text-underline-offset: 5px; +} +button, +a, +input { + -webkit-tap-highlight-color: transparent; +} +:focus-visible { + outline: 3px solid #235844; + outline-offset: 5px; +} +.site-shell { + max-width: 1440px; + margin: 0 auto; + padding: 0 64px; +} +.site-header { + height: 112px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + border-bottom: 1px solid #d5d9cf; +} +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 23px; + font-weight: 650; + letter-spacing: -0.8px; +} +.brand:hover { + text-decoration: none; +} +.site-header nav { + display: flex; + align-items: center; + gap: 32px; + font-size: 14px; + font-weight: 550; +} +.source-link { + color: #4c5d53; +} +.skip-link { + position: absolute; + top: -100px; + left: 24px; + padding: 12px 16px; + background: white; + z-index: 10; +} +.skip-link:focus { + top: 12px; +} +.hero { + display: grid; + grid-template-columns: 1.15fr 0.85fr; + gap: 40px; + align-items: center; + min-height: 600px; + padding: 76px 0; +} +.eyebrow { + display: flex; + align-items: center; + gap: 9px; + color: #496052; + font-family: ui-monospace, monospace; + font-size: 12px; + letter-spacing: 1.6px; + text-transform: uppercase; + margin: 0 0 32px; +} +.status-dot { + display: inline-block; + height: 7px; + width: 7px; + background: #36734a; + border-radius: 50%; +} +h1 { + font-size: clamp(64px, 7.8vw, 112px); + font-weight: 450; + line-height: 1.02; + letter-spacing: -7px; + margin: 0; +} +h1 em { + font-family: Georgia, "Times New Roman", serif; + font-weight: 400; + color: #235844; +} +.hero-copy { + max-width: 370px; + font-size: 18px; + line-height: 1.7; + color: #5a675e; + margin: 28px 0 32px; +} +.button { + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 40px; + min-height: 52px; + padding: 14px 22px; + border: 1px solid #235844; + border-radius: 6px; + background: #235844; + color: #fff; + font-size: 15px; + font-weight: 550; + cursor: pointer; + transition: background 140ms ease; +} +.button:hover { + background: #143d2e; + text-decoration: none; +} +.button:focus-visible { + outline-offset: 4px; +} +.hello-art { + position: relative; + display: grid; + place-items: center; + aspect-ratio: 1; + width: 100%; + max-width: 430px; + justify-self: end; + background: #e8ece2; + border: 1px solid #d5ddcc; + border-radius: 50%; + overflow: hidden; +} +.hello-art::before, +.hello-art::after { + content: ""; + position: absolute; + width: 72%; + height: 72%; + border: 1px solid #bbcdb7; + border-radius: 50%; + transform: rotate(-22deg) scaleX(0.5); +} +.hello-art::after { + transform: rotate(22deg) scaleX(0.5); +} +.hello-art span { + z-index: 1; + font-family: Georgia, "Times New Roman", serif; + font-size: clamp(76px, 10vw, 136px); + font-style: italic; + color: #235844; + letter-spacing: -8px; + transform: rotate(-8deg); +} +.intro-line { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + padding: 32px 0 44px; + border-top: 1px solid #d5d9cf; +} +.intro-line h2 { + font-size: 21px; + font-weight: 500; + margin: 0; +} +.intro-line p { + color: #5a675e; + line-height: 1.7; + margin: 0; + max-width: 510px; +} +.site-footer { + display: flex; + flex-wrap: wrap; + gap: 12px 28px; + padding: 28px 0 32px; + border-top: 1px solid #d5d9cf; + font-size: 12px; + color: #58665d; +} +.site-footer span { + margin-right: auto; +} +.example { + max-width: 720px; + margin: 0 auto; + padding: 72px 0 80px; + min-height: 640px; +} +.back-link { + display: inline-block; + margin-bottom: 32px; + color: #496052; + font-size: 14px; +} +.example h1 { + font-size: clamp(42px, 5vw, 64px); + letter-spacing: -3px; +} +.example .hero-copy { + max-width: 560px; + font-size: 17px; + margin: 22px 0 32px; +} +.hello-form { + margin-top: 36px; +} +.hello-form label { + display: block; + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} +.input-row { + display: flex; + gap: 12px; + align-items: stretch; +} +.input-row input { + flex: 1; + min-width: 0; + background: #fffdf7; + border: 1px solid #a3b1a4; + border-radius: 6px; + padding: 14px 16px; + color: #1e2824; + font-size: 17px; +} +.field-hint { + font-size: 13px; + color: #58665d; + margin: 12px 0 0; +} +.field-error { + color: #923b32; + font-size: 14px; + margin: 12px 0 0; +} +.greeting { + margin-top: 32px; + border-left: 3px solid #639167; + padding: 20px 24px; + background: #e8ece2; + border-radius: 0 6px 6px 0; + overflow-wrap: anywhere; +} +.greeting-label { + display: block; + color: #496052; + font-size: 11px; + letter-spacing: 1.5px; + text-transform: uppercase; + font-family: ui-monospace, monospace; + margin-bottom: 8px; +} +.greeting p { + font-size: 28px; + font-family: Georgia, "Times New Roman", serif; + margin: 0; + line-height: 1.4; +} +.no-script { + padding: 12px 0; + font-size: 14px; + color: #5a675e; +} +@media (max-width: 800px) { + .site-shell { + padding: 0 28px; + } + .site-header { + height: 88px; + } + .hero { + grid-template-columns: 1fr; + padding: 56px 0 40px; + min-height: auto; + gap: 40px; + } + h1 { + letter-spacing: -4px; + } + .hello-art { + width: min(100%, 330px); + justify-self: center; + } + .intro-line { + grid-template-columns: 1fr; + gap: 16px; + } + .example { + padding-top: 40px; + min-height: 610px; + } +} +@media (max-width: 480px) { + .site-shell { + padding: 0 22px; + } + .site-header nav { + gap: 16px; + font-size: 13px; + } + .brand { + font-size: 20px; + gap: 5px; + } + .source-link { + display: none; + } + .eyebrow { + font-size: 10px; + margin-bottom: 24px; + } + .input-row { + flex-direction: column; + } + .input-row .button { + width: 100%; + } + .site-footer span { + flex-basis: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition: none !important; + scroll-behavior: auto !important; + } +} diff --git a/src/ArcForges.Web.Site/SiteBuilder.cs b/src/ArcForges.Web.Site/SiteBuilder.cs new file mode 100644 index 0000000..cdd1b9a --- /dev/null +++ b/src/ArcForges.Web.Site/SiteBuilder.cs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text; +using ArcForges.Web.Ui; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; + +namespace ArcForges.Web.Site; + +/// +/// Builds the static public Site at build time with the first-party Razor HtmlRenderer. The output has no WebAssembly, +/// no JavaScript and no runtime dependency: every page is complete HTML and one content-hashed stylesheet, and two +/// builds of the same inputs produce the same bytes. +/// +public static class SiteBuilder +{ + /// The source link of a build without a source ref. + public const string RepositoryUrl = "https://github.com/ArcForges/Web"; + + private const string HtmlType = "text/html; charset=utf-8"; + private const string TextType = "text/plain; charset=utf-8"; + + /// Builds the Site. The result lists every file in ordinal path order. + public static async Task BuildAsync(SiteOptions options, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(options); + var sourceUrl = SourceUrl(options.SourceRef); + var stylesheet = StaticAssets.Bytes("site.css"); + var stylesheetPath = "assets/site." + Sha256Hex(stylesheet)[..16] + ".css"; + var stylesheetHref = "/" + stylesheetPath; + + var pages = new List(); + var html = new List(); + using var renderer = new HtmlRenderer(new ServiceCollection().BuildServiceProvider(), NullLoggerFactory.Instance); + foreach (var page in SitePages.All) + { + cancellationToken.ThrowIfCancellationRequested(); + var document = await RenderAsync(renderer, page, sourceUrl, stylesheetHref).ConfigureAwait(false); + html.Add(document); + pages.Add(new SiteFile(page.OutputPath, Encoding.UTF8.GetBytes(document), HtmlType)); + } + + var policy = SiteContentSecurityPolicy.FromPages(html); + var files = new List(pages) + { + new("404.html", StaticAssets.Bytes("404.html"), HtmlType), + new("404.css", StaticAssets.Bytes("404.css"), "text/css; charset=utf-8"), + new("robots.txt", StaticAssets.Bytes("robots.txt"), TextType), + new("favicon.svg", StaticAssets.Bytes("favicon.svg"), "image/svg+xml"), + new(stylesheetPath, stylesheet, "text/css; charset=utf-8"), + new("_headers", Encoding.UTF8.GetBytes(SiteSecurityHeaders.Render(policy)), TextType), + }; + return new SiteOutput(files.OrderBy(file => file.Path, StringComparer.Ordinal).ToArray()); + } + + /// Returns the source link for a source ref, which must be a full lower-case commit id when given. + public static string SourceUrl(string? sourceRef) + { + if (sourceRef is null) + return RepositoryUrl; + if (sourceRef.Length != 40 || !sourceRef.All(character => character is (>= '0' and <= '9') or (>= 'a' and <= 'f'))) + throw new ArgumentException("The source ref must be a 40-character lower-case commit id.", nameof(sourceRef)); + return RepositoryUrl + "/tree/" + sourceRef; + } + + private static async Task RenderAsync(HtmlRenderer renderer, SitePage page, string sourceUrl, string stylesheetHref) + { + RenderFragment body = builder => + { + builder.OpenComponent(0, page.Content); + builder.CloseComponent(); + }; + var parameters = ParameterView.FromDictionary(new Dictionary(StringComparer.Ordinal) + { + ["Title"] = page.Title, + ["Description"] = page.Description, + ["Stylesheet"] = stylesheetHref, + ["SourceUrl"] = sourceUrl, + ["Body"] = body, + }); + return await renderer.Dispatcher.InvokeAsync(async () => + { + var output = await renderer.RenderComponentAsync(parameters).ConfigureAwait(false); + return "" + StaticMarkup.RemoveRuntimeMarkers(output.ToHtmlString()); + }).ConfigureAwait(false); + } + + private static string Sha256Hex(byte[] content) => + Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(content)).ToLowerInvariant(); +} diff --git a/src/ArcForges.Web.Site/SiteContentSecurityPolicy.cs b/src/ArcForges.Web.Site/SiteContentSecurityPolicy.cs new file mode 100644 index 0000000..0bf7410 --- /dev/null +++ b/src/ArcForges.Web.Site/SiteContentSecurityPolicy.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Security.Cryptography; +using System.Text; + +namespace ArcForges.Web.Site; + +/// +/// The strict Content-Security-Policy of the public Site. The script sources are 'self' plus the SHA-256 hash of every +/// inline script body the pages emit (the React build's derivation). An external script is refused, and the policy +/// never carries unsafe-inline, unsafe-eval or wasm-unsafe-eval: the public pages need no WebAssembly. +/// +public static class SiteContentSecurityPolicy +{ + /// The Workers header line budget that the policy must fit within. + public const int HeaderLineBudget = 1800; + + private const string ScriptTag = "Returns the policy for the emitted pages. + /// A page emits an external script or an unterminated script element. + public static string FromPages(IEnumerable pages) + { + ArgumentNullException.ThrowIfNull(pages); + var hashes = new SortedSet(StringComparer.Ordinal); + foreach (var page in pages) + foreach (var body in InlineScriptBodies(page)) + if (body.Length > 0) + hashes.Add("'sha256-" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(body))) + "'"); + var scriptSources = string.Join(' ', new[] { "'self'" }.Concat(hashes)); + var policy = $"default-src 'self'; script-src {scriptSources}; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'"; + if (policy.Length >= HeaderLineBudget) + throw new InvalidOperationException("The Content-Security-Policy exceeds the Workers header line budget."); + return policy; + } + + private static IEnumerable InlineScriptBodies(string html) + { + var start = 0; + while (true) + { + var open = html.IndexOf(ScriptTag, start, StringComparison.OrdinalIgnoreCase); + if (open < 0) + yield break; + var tagEnd = html.IndexOf('>', open); + if (tagEnd < 0) + throw new InvalidOperationException("A script element is not terminated."); + // Fail closed: any source attribute, even inside another attribute value, is an external script. + if (html.AsSpan(open + ScriptTag.Length, tagEnd - open - ScriptTag.Length).Contains("src", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("An external script is refused by the public Site policy."); + var close = html.IndexOf(ScriptClose, tagEnd + 1, StringComparison.OrdinalIgnoreCase); + if (close < 0) + throw new InvalidOperationException("A script element is not closed."); + yield return html[(tagEnd + 1)..close]; + start = close + ScriptClose.Length; + } + } +} diff --git a/src/ArcForges.Web.Site/SiteDocument.razor b/src/ArcForges.Web.Site/SiteDocument.razor new file mode 100644 index 0000000..0156b2a --- /dev/null +++ b/src/ArcForges.Web.Site/SiteDocument.razor @@ -0,0 +1,23 @@ +@Title@if (Description is not null) { }@Body + +@code { + /// The document title, the exact React meta title. + [Parameter, EditorRequired] + public string Title { get; set; } = string.Empty; + + /// The meta description, when the page has one. + [Parameter] + public string? Description { get; set; } + + /// The content-hashed stylesheet path of the build. + [Parameter, EditorRequired] + public string Stylesheet { get; set; } = string.Empty; + + /// The source link of the build. + [Parameter, EditorRequired] + public string SourceUrl { get; set; } = string.Empty; + + /// The page content. + [Parameter, EditorRequired] + public RenderFragment Body { get; set; } = _ => { }; +} diff --git a/src/ArcForges.Web.Site/SiteOutput.cs b/src/ArcForges.Web.Site/SiteOutput.cs new file mode 100644 index 0000000..5b9a177 --- /dev/null +++ b/src/ArcForges.Web.Site/SiteOutput.cs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Security.Cryptography; + +namespace ArcForges.Web.Site; + +/// Build inputs of the static Site. +public sealed record SiteOptions +{ + /// + /// The 40-hex source commit of the build. When set, the source link points at its commit, as the React build did + /// from VITE_SOURCE_REF. When null, the link points at the repository. + /// + public string? SourceRef { get; init; } +} + +/// One file of the static Site output, at a path relative to the public root. +public sealed record SiteFile(string Path, byte[] Content, string ContentType) +{ + /// The lower-case SHA-256 of the content. + public string Sha256 => Convert.ToHexString(SHA256.HashData(Content)).ToLowerInvariant(); +} + +/// The complete static Site output: every file, sorted by path, with no timestamp or environment input. +public sealed record SiteOutput(IReadOnlyList Files) +{ + /// Returns the file at , or null when the Site does not publish it. + public SiteFile? Find(string path) => Files.FirstOrDefault(file => string.Equals(file.Path, path, StringComparison.Ordinal)); +} diff --git a/src/ArcForges.Web.Site/SitePages.cs b/src/ArcForges.Web.Site/SitePages.cs new file mode 100644 index 0000000..a76b377 --- /dev/null +++ b/src/ArcForges.Web.Site/SitePages.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using ArcForges.Web.Ui; +using Microsoft.AspNetCore.Components; + +namespace ArcForges.Web.Site; + +/// One public page of the Site: its output path, its exact title and description and its content component. +internal sealed record SitePage(string OutputPath, string Title, string? Description, Type Content); + +/// The public pages, in output order. The titles and descriptions are the React route meta values. +internal static class SitePages +{ + public static IReadOnlyList All { get; } = + [ + new("index.html", "Hello, world. — ArcForges", "A small, open-source beginning for ArcForges Web.", typeof(HomeContent)), + new("hello/index.html", "Your hello — ArcForges", null, typeof(HelloExample)), + new("cloud-hello/index.html", "Server connection — ArcForges", null, typeof(CloudHelloExample)), + ]; +} diff --git a/src/ArcForges.Web.Site/SiteSecurityHeaders.cs b/src/ArcForges.Web.Site/SiteSecurityHeaders.cs new file mode 100644 index 0000000..9656448 --- /dev/null +++ b/src/ArcForges.Web.Site/SiteSecurityHeaders.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Site; + +/// The Cloudflare _headers file of the public Site: the same rules and values as the React build. +public static class SiteSecurityHeaders +{ + /// Renders the file for the given policy. The text is the React securityHeaders template, byte for byte. + public static string Render(string contentSecurityPolicy) + { + ArgumentException.ThrowIfNullOrEmpty(contentSecurityPolicy); + if (contentSecurityPolicy.Length >= SiteContentSecurityPolicy.HeaderLineBudget) + throw new ArgumentException("The policy exceeds the Workers header line budget.", nameof(contentSecurityPolicy)); + return string.Join( + '\n', + "/*", + " Content-Security-Policy: " + contentSecurityPolicy, + " X-Content-Type-Options: nosniff", + " Referrer-Policy: no-referrer", + " Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()", + " X-Frame-Options: DENY", + " X-Robots-Tag: noindex, nofollow", + " Cache-Control: public, no-cache, no-transform", + "/assets/*", + " ! Cache-Control", + " Cache-Control: public, max-age=31536000, immutable, no-transform", + "/__build.json", + " ! Cache-Control", + " Cache-Control: no-store, no-transform", + "/__build-info.json", + " ! Cache-Control", + " Cache-Control: no-store, no-transform") + "\n"; + } +} diff --git a/src/ArcForges.Web.Site/StaticAssets.cs b/src/ArcForges.Web.Site/StaticAssets.cs new file mode 100644 index 0000000..772d09b --- /dev/null +++ b/src/ArcForges.Web.Site/StaticAssets.cs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Site; + +/// Reads the embedded static files of the Site (the stylesheet, the 404 page, the favicon and robots.txt). +internal static class StaticAssets +{ + private const string Prefix = "ArcForges.Web.Site.Assets."; + + /// Returns the exact bytes of an embedded asset. + /// The asset is not embedded in this assembly. + public static byte[] Bytes(string name) + { + using var stream = typeof(StaticAssets).Assembly.GetManifestResourceStream(Prefix + name) + ?? throw new InvalidOperationException("The static asset is not embedded: " + name); + using var buffer = new MemoryStream(); + stream.CopyTo(buffer); + return buffer.ToArray(); + } +} diff --git a/src/ArcForges.Web.Site/StaticMarkup.cs b/src/ArcForges.Web.Site/StaticMarkup.cs new file mode 100644 index 0000000..851d5e9 --- /dev/null +++ b/src/ArcForges.Web.Site/StaticMarkup.cs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.RegularExpressions; + +namespace ArcForges.Web.Site; + +/// Post-render normalisation of the static HTML: the public pages carry no Blazor runtime markers. +internal static partial class StaticMarkup +{ + /// + /// Removes the Blazor internal attribute markers (for example __internal_preventDefault_onsubmit, which a + /// form's @onsubmit:preventDefault renders). They only instruct the Blazor runtime, which the public pages + /// never load, so a static page must not carry them. + /// + public static string RemoveRuntimeMarkers(string html) => RuntimeMarker().Replace(html, string.Empty); + + [GeneratedRegex(@"\s__internal_[A-Za-z_]+(?=[\s>])", RegexOptions.CultureInvariant)] + private static partial Regex RuntimeMarker(); +} diff --git a/src/ArcForges.Web.Site/_Imports.razor b/src/ArcForges.Web.Site/_Imports.razor new file mode 100644 index 0000000..d16c9ac --- /dev/null +++ b/src/ArcForges.Web.Site/_Imports.razor @@ -0,0 +1,4 @@ +@* SPDX-License-Identifier: AGPL-3.0-only *@ +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Web +@using ArcForges.Web.Ui diff --git a/src/ArcForges.Web.Site/packages.lock.json b/src/ArcForges.Web.Site/packages.lock.json index a8b0c23..9a29600 100644 --- a/src/ArcForges.Web.Site/packages.lock.json +++ b/src/ArcForges.Web.Site/packages.lock.json @@ -7,6 +7,41 @@ "requested": "[1.0.0-ci.94.1, )", "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, + "arcforges.web.ui": { + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } } } } diff --git a/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj b/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj index 878ddcd..02c5fa1 100644 --- a/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj +++ b/src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj @@ -3,6 +3,9 @@ + + + AGPL AGPL-3.0-only diff --git a/src/ArcForges.Web.Ui/Arrow.razor b/src/ArcForges.Web.Ui/Arrow.razor new file mode 100644 index 0000000..73daffd --- /dev/null +++ b/src/ArcForges.Web.Ui/Arrow.razor @@ -0,0 +1 @@ + diff --git a/src/ArcForges.Web.Ui/Button.razor b/src/ArcForges.Web.Ui/Button.razor new file mode 100644 index 0000000..b06fd4a --- /dev/null +++ b/src/ArcForges.Web.Ui/Button.razor @@ -0,0 +1,11 @@ + + +@code { + /// The visible label, placed before the arrow. + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// Attributes passed through to the button element (type, disabled, event handlers). + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary? AdditionalAttributes { get; set; } +} diff --git a/src/ArcForges.Web.Ui/CloudHelloExample.razor b/src/ArcForges.Web.Ui/CloudHelloExample.razor new file mode 100644 index 0000000..74d0a45 --- /dev/null +++ b/src/ArcForges.Web.Ui/CloudHelloExample.razor @@ -0,0 +1,83 @@ +@implements IDisposable +
← Back to your hello

Connection example

A hello, from the server.

Send a fixed “ArcForges” greeting to check the server connection. Your name from the local example stays in your browser. Nothing is sent until you choose to connect.

Server response

@Message

+ +@code { + private enum ConnectionState + { + Idle, + Pending, + Success, + Error, + } + + private bool _ready; + private ConnectionState _state = ConnectionState.Idle; + private string _reply = string.Empty; + private CancellationTokenSource? _active; + + /// + /// The check that the host supplies (the App profile calls the server; the static Site supplies none, so the + /// control stays disabled, as the no-script page requires). + /// + [Parameter] + public Func>? CheckConnection { get; set; } + + private string ButtonLabel => _state == ConnectionState.Pending ? "Connecting…" : "Check connection"; + + private string RegionRole => _state == ConnectionState.Error ? "alert" : "status"; + + private string Message => _state switch + { + ConnectionState.Idle => "No request sent yet.", + ConnectionState.Pending => "Contacting the server…", + ConnectionState.Success => _reply, + _ => "The server is unavailable or returned an unexpected response. Try again later.", + }; + + /// Enables the control after the first interactive render, as the React example did after mount. + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && !_ready) + { + _ready = true; + StateHasChanged(); + } + } + + /// Cancels a check that is still running when the page is left, as the React cleanup did. + public void Dispose() => _active?.Cancel(); + + private async Task ConnectAsync() + { + if (CheckConnection is null || _active is not null) + return; + using var controller = new CancellationTokenSource(); + _active = controller; + _state = ConnectionState.Pending; + StateHasChanged(); + try + { + var message = await CheckConnection(controller.Token); + if (!controller.IsCancellationRequested) + { + _reply = message; + _state = ConnectionState.Success; + } + } + catch (OperationCanceledException) when (controller.IsCancellationRequested) + { + // The component was disposed while the check was running; nothing is shown. + } +#pragma warning disable CA1031 // Every failure shows the same visitor text, whatever the transport raised. + catch (Exception) +#pragma warning restore CA1031 + { + _state = ConnectionState.Error; + } + finally + { + if (ReferenceEquals(_active, controller)) + _active = null; + } + } +} diff --git a/src/ArcForges.Web.Ui/GrpcWebFrames.cs b/src/ArcForges.Web.Ui/GrpcWebFrames.cs new file mode 100644 index 0000000..8c04a66 --- /dev/null +++ b/src/ArcForges.Web.Ui/GrpcWebFrames.cs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Ui; + +/// +/// Length-prefixed gRPC-Web framing for one unary call: a data frame (flag 0) and a trailers frame (flag 0x80), +/// the binary application/grpc-web+proto form the hello example uses. Compressed frames are refused. +/// +public static class GrpcWebFrames +{ + /// The media type of the binary gRPC-Web form. + public const string MediaType = "application/grpc-web+proto"; + + /// The flag of an uncompressed protobuf data frame. + public const byte DataFlag = 0x00; + + /// The flag of the trailers frame. + public const byte TrailersFlag = 0x80; + + /// Encodes one frame: a flag byte, a big-endian 32-bit payload length and the payload. + public static byte[] Encode(ReadOnlySpan payload, byte flag) + { + var frame = new byte[payload.Length + 5]; + frame[0] = flag; + System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(1, 4), checked((uint)payload.Length)); + payload.CopyTo(frame.AsSpan(5)); + return frame; + } + + /// + /// Returns the single protobuf message of a unary reply and checks the trailers. The reply must be exactly one + /// uncompressed data frame followed by a trailers frame whose grpc-status is 0. + /// + /// The body is truncated, compressed, out of shape or not an OK status. + public static byte[] DecodeUnaryReply(ReadOnlySpan body) + { + var offset = 0; + var message = ReadFrame(body, ref offset); + if (message.Flag != DataFlag) + throw new ServerConnectionException("The reply frame is not an uncompressed protobuf message."); + var trailers = ReadFrame(body, ref offset); + if (trailers.Flag != TrailersFlag) + throw new ServerConnectionException("The reply has no trailers frame."); + if (offset != body.Length) + throw new ServerConnectionException("The reply has bytes after the trailers."); + if (GrpcStatus(body.Slice(trailers.Start, trailers.Length)) != "0") + throw new ServerConnectionException("The server reported a non-OK status."); + return body.Slice(message.Start, message.Length).ToArray(); + } + + private readonly record struct Frame(byte Flag, int Start, int Length); + + private static Frame ReadFrame(ReadOnlySpan body, ref int offset) + { + if (body.Length - offset < 5) + throw new ServerConnectionException("The reply frame header is truncated."); + var flag = body[offset]; + var length = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(body.Slice(offset + 1, 4)); + offset += 5; + if (length > body.Length - offset) + throw new ServerConnectionException("The reply frame length is beyond the body."); + var frame = new Frame(flag, offset, (int)length); + offset += (int)length; + return frame; + } + + private static string? GrpcStatus(ReadOnlySpan trailers) + { + var text = System.Text.Encoding.UTF8.GetString(trailers); + foreach (var line in text.Split('\n')) + { + var separator = line.IndexOf(':', StringComparison.Ordinal); + if (separator < 0) + continue; + if (string.Equals(line[..separator].Trim(), "grpc-status", StringComparison.OrdinalIgnoreCase)) + return line[(separator + 1)..].Trim(); + } + return null; + } +} diff --git a/src/ArcForges.Web.Ui/HelloExample.razor b/src/ArcForges.Web.Ui/HelloExample.razor new file mode 100644 index 0000000..c05ba7f --- /dev/null +++ b/src/ArcForges.Web.Ui/HelloExample.razor @@ -0,0 +1,35 @@ +
← Back home

Hello example

A hello, just for you.

Start with your name. This example runs in your browser, and your name stays on this page.

Up to 80 characters. No account needed.

@if (_error.Length > 0) { }
Your greeting

@_message

Check the server connection →

+ +@code { + private string _name = HelloGreeting.DefaultName; + private string _message = HelloGreeting.Greet(HelloGreeting.DefaultName); + private string _error = string.Empty; + private bool _ready; + + private string InvalidText => _error.Length > 0 ? "true" : "false"; + + private string DescribedBy => _error.Length > 0 ? "name-error name-hint" : "name-hint"; + + /// Enables the controls after the first interactive render, as the React example did after mount. + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && !_ready) + { + _ready = true; + StateHasChanged(); + } + } + + private void Submit(EventArgs _) + { + try + { + _message = HelloGreeting.Greet(_name); + _error = string.Empty; + } + catch (InvalidNameException) + { + _error = HelloGreeting.InvalidNameMessage; + } + } +} diff --git a/src/ArcForges.Web.Ui/HelloGreeting.cs b/src/ArcForges.Web.Ui/HelloGreeting.cs new file mode 100644 index 0000000..dfa63c6 --- /dev/null +++ b/src/ArcForges.Web.Ui/HelloGreeting.cs @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text; +using ArcForges.Contracts.Hello.V1; +using Google.Protobuf; + +namespace ArcForges.Web.Ui; + +/// A name that the hello example refuses. The message is the visitor-facing failure text. +public sealed class InvalidNameException : Exception +{ + /// Creates the refusal with the exact visitor-facing text. + public InvalidNameException() + : base(HelloGreeting.InvalidNameMessage) + { + } +} + +/// +/// The hello example greeting (the TypeScript apps/site/app/hello.ts port). The greeting is built from the published +/// SayHello request and response messages, so it is one real protobuf round trip and not a local string format only. +/// +public static class HelloGreeting +{ + /// The visitor-facing text for a refused name. + public const string InvalidNameMessage = "Enter a name between 1 and 80 characters, without control characters."; + + /// The name shown before the visitor enters one. + public const string DefaultName = "World"; + + /// The largest name, counted in Unicode code points before trimming. + public const int MaximumNameLength = 80; + + /// + /// Returns the greeting for . The name is refused when it is blank after trimming, longer + /// than 80 code points or contains a C0 control or DEL character. Surrounding whitespace is removed before the + /// request is encoded; an unpaired surrogate is encoded as U+FFFD, as the browser text encoder does. + /// + /// The name is not accepted. + public static string Greet(string name) + { + ArgumentNullException.ThrowIfNull(name); + if (ContainsControl(name) || CodePointCount(name) > MaximumNameLength || JsTrim(name).Length == 0) + throw new InvalidNameException(); + var trimmed = JsTrim(name); + var wireName = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(trimmed)); + var request = new SayHelloRequest { Name = wireName }; + var decoded = SayHelloRequest.Parser.ParseFrom(request.ToByteArray()); + var response = new SayHelloResponse { Message = $"Hello, {decoded.Name}!" }; + return SayHelloResponse.Parser.ParseFrom(response.ToByteArray()).Message; + } + + private static bool ContainsControl(string name) + { + foreach (var character in name) + if (character < 32 || character == 127) + return true; + return false; + } + + private static int CodePointCount(string name) + { + var count = 0; + foreach (var _ in name.EnumerateRunes()) + count++; + return count; + } + + /// Trims the characters that JavaScript String.prototype.trim removes. + private static string JsTrim(string value) + { + var start = 0; + var end = value.Length; + while (start < end && IsJsWhitespace(value[start])) + start++; + while (end > start && IsJsWhitespace(value[end - 1])) + end--; + return value[start..end]; + } + + private static bool IsJsWhitespace(char character) + { + var code = (int)character; + return code is 0x09 or 0x0A or 0x0B or 0x0C or 0x0D or 0x20 or 0xA0 or 0x1680 or 0x2028 or 0x2029 + or 0x202F or 0x205F or 0x3000 or 0xFEFF + || (code >= 0x2000 && code <= 0x200A); + } +} diff --git a/src/ArcForges.Web.Ui/HomeContent.razor b/src/ArcForges.Web.Ui/HomeContent.razor new file mode 100644 index 0000000..5ccfda3 --- /dev/null +++ b/src/ArcForges.Web.Ui/HomeContent.razor @@ -0,0 +1 @@ +

ArcForges Web · Early preview

Hello,
world.

Every good thing starts somewhere.
Welcome to the first page of ArcForges Web.

Make it your hello

A beginning you can explore.

This small preview has one job: say hello. Try the example, look around the source, and follow along as ArcForges takes shape.

diff --git a/src/ArcForges.Web.Ui/ServerConnection.cs b/src/ArcForges.Web.Ui/ServerConnection.cs new file mode 100644 index 0000000..cea1a46 --- /dev/null +++ b/src/ArcForges.Web.Ui/ServerConnection.cs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Net.Http.Headers; +using ArcForges.Contracts.Hello.V1; +using Google.Protobuf; + +namespace ArcForges.Web.Ui; + +/// A refused or failed server connection. The message is for logs, never for the visitor. +public sealed class ServerConnectionException : Exception +{ + /// Creates an exception with a diagnostic message. + public ServerConnectionException(string message) + : base(message) + { + } +} + +/// +/// The server connection check of the cloud-hello example (the TypeScript checkServerConnection port): one anonymous +/// binary gRPC-Web SayHello call to the same-origin /api path, with a ten second deadline. It sends no +/// credentials, follows no redirect and accepts only the fixed greeting. +/// +public sealed class ServerConnectionClient +{ + /// The fixed name sent to the server. + public const string RequestName = "ArcForges"; + + /// The only reply accepted for . + public const string ExpectedReply = "Hello, ArcForges!"; + + /// The API path of the SayHello method, relative to the origin. + public const string HelloApiPath = "/api/arcforges.hello.v1.HelloService/SayHello"; + + /// The overall deadline of one check (the TypeScript defaultTimeoutMs of 10000). + public static readonly TimeSpan Deadline = TimeSpan.FromSeconds(10); + + /// The largest reply body read, a cost control for an unexpected response. + public const int MaximumReplyBytes = 64 * 1024; + + private readonly HttpMessageInvoker _invoker; + + /// Creates a client over a message handler. The caller supplies a handler that keeps no cookies. + public ServerConnectionClient(HttpMessageHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + _invoker = new HttpMessageInvoker(handler, disposeHandler: true); + } + + /// + /// Sends the check to and returns the greeting. Throws + /// for any reply that is not the fixed greeting over the binary gRPC-Web form; the caller's cancellation propagates + /// as . + /// + public async Task CheckAsync(Uri origin, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(origin); + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + deadline.CancelAfter(Deadline); + var payload = new SayHelloRequest { Name = RequestName }.ToByteArray(); + using var request = new HttpRequestMessage(HttpMethod.Post, new Uri(origin, HelloApiPath)) + { + Content = new ByteArrayContent(GrpcWebFrames.Encode(payload, GrpcWebFrames.DataFlag)), + }; + request.Content.Headers.ContentType = new MediaTypeHeaderValue(GrpcWebFrames.MediaType); + request.Headers.TryAddWithoutValidation("x-grpc-web", "1"); + using var response = await SendAsync(request, deadline.Token).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + throw new ServerConnectionException($"The server answered HTTP {(int)response.StatusCode}."); + if (!string.Equals(response.Content.Headers.ContentType?.MediaType, GrpcWebFrames.MediaType, StringComparison.OrdinalIgnoreCase)) + throw new ServerConnectionException("The reply is not the binary gRPC-Web media type."); + var body = await ReadBoundedAsync(response.Content, deadline.Token).ConfigureAwait(false); + SayHelloResponse message; + try + { + message = SayHelloResponse.Parser.ParseFrom(GrpcWebFrames.DecodeUnaryReply(body)); + } + catch (InvalidProtocolBufferException) + { + throw new ServerConnectionException("The reply is not a valid SayHello message."); + } + if (message.Message != ExpectedReply) + throw new ServerConnectionException("Unexpected Hello response"); + return message.Message; + } + + private async Task SendAsync(HttpRequestMessage request, CancellationToken token) + { + try + { + return await _invoker.SendAsync(request, token).ConfigureAwait(false); + } + catch (HttpRequestException exception) + { + throw new ServerConnectionException("The server is unreachable: " + exception.GetType().Name); + } + catch (OperationCanceledException) when (!token.IsCancellationRequested) + { + throw new ServerConnectionException("The server did not answer before the deadline."); + } + } + + private static async Task ReadBoundedAsync(HttpContent content, CancellationToken token) + { + if (content.Headers.ContentLength is long declared && declared > MaximumReplyBytes) + throw new ServerConnectionException("The reply is larger than the limit."); + await using var stream = await content.ReadAsStreamAsync(token).ConfigureAwait(false); + using var buffer = new MemoryStream(); + var chunk = new byte[4096]; + while (true) + { + var read = await stream.ReadAsync(chunk, token).ConfigureAwait(false); + if (read == 0) + break; + if (buffer.Length + read > MaximumReplyBytes) + throw new ServerConnectionException("The reply is larger than the limit."); + buffer.Write(chunk, 0, read); + } + return buffer.ToArray(); + } +} diff --git a/src/ArcForges.Web.Ui/Shell.razor b/src/ArcForges.Web.Ui/Shell.razor new file mode 100644 index 0000000..8efc521 --- /dev/null +++ b/src/ArcForges.Web.Ui/Shell.razor @@ -0,0 +1,11 @@ + + +@code { + /// The source link. The Site build passes the commit URL, as the React build did from its source ref. + [Parameter] + public string SourceUrl { get; set; } = "https://github.com/ArcForges/Web"; + + /// The page content inside the main landmark. + [Parameter] + public RenderFragment? ChildContent { get; set; } +} diff --git a/src/ArcForges.Web.Ui/packages.lock.json b/src/ArcForges.Web.Ui/packages.lock.json index a8b0c23..a505077 100644 --- a/src/ArcForges.Web.Ui/packages.lock.json +++ b/src/ArcForges.Web.Ui/packages.lock.json @@ -7,6 +7,35 @@ "requested": "[1.0.0-ci.94.1, )", "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" + }, + "ArcForges.Contracts.PublicApi": { + "type": "Direct", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } + }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" } } } diff --git a/tests/ArcForges.Web.App.Tests/packages.lock.json b/tests/ArcForges.Web.App.Tests/packages.lock.json index 33e424a..9045e07 100644 --- a/tests/ArcForges.Web.App.Tests/packages.lock.json +++ b/tests/ArcForges.Web.App.Tests/packages.lock.json @@ -19,6 +19,24 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -356,7 +374,21 @@ } }, "arcforges.web.ui": { - "type": "Project" + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } }, "Microsoft.AspNetCore.Components.WebAssembly": { "type": "CentralTransitive", diff --git a/tests/ArcForges.Web.Policy.Tests/NuGetPackDownloadAdmissionTests.cs b/tests/ArcForges.Web.Policy.Tests/NuGetPackDownloadAdmissionTests.cs new file mode 100644 index 0000000..73521f9 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/NuGetPackDownloadAdmissionTests.cs @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using Xunit; + +namespace ArcForges.Web.Policy.Tests; + +/// +/// Checks the browser-wasm pack downloads (WEB.40 section 10). NuGet records PackageDownload items outside +/// packages.lock.json, so each declared item must be admitted at the exact version, and every admitted download must +/// be either declared in a project or added implicitly by the SDK. +/// +public sealed class NuGetPackDownloadAdmissionTests +{ + private static readonly Regex DeclaredDownload = new( + "[^\"]+)\"\\s+Version=\"\\[(?[^\\]]+)\\]\"", + RegexOptions.CultureInvariant); + + private static readonly string[] PermissiveLicences = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]; + + [Fact] + public void EveryDeclaredPackDownloadIsAdmittedAtTheExactVersion() + { + var root = RepositoryRoot.Find(); + var admitted = AdmittedDownloads(root); + var declared = DeclaredDownloads(root); + Assert.NotEmpty(declared); + foreach (var entry in declared) + Assert.True(admitted.ContainsKey(entry.Key), $"Unadmitted PackageDownload {entry.Key} in {entry.Value}."); + } + + [Fact] + public void EveryAdmittedPackDownloadIsDeclaredOrAddedImplicitlyBySdk() + { + var root = RepositoryRoot.Find(); + var admitted = AdmittedDownloads(root); + var declared = DeclaredDownloads(root).Select(entry => entry.Key).ToHashSet(StringComparer.Ordinal); + foreach (var key in admitted.Select(entry => entry.Key)) + { + if (declared.Contains(key)) + continue; + var declaredBy = admitted[key]?["declaredBy"]?.GetValue() ?? string.Empty; + Assert.StartsWith("implicit:", declaredBy, StringComparison.Ordinal); + } + } + + [Fact] + public void EveryAdmittedPackDownloadHasAContentHashSourceAndPermissiveLicence() + { + var admitted = AdmittedDownloads(RepositoryRoot.Find()); + Assert.NotEmpty(admitted); + foreach (var entry in admitted) + { + var row = entry.Value?.AsObject() ?? throw new InvalidDataException("Empty admission row: " + entry.Key); + Assert.Matches("^[A-Za-z0-9+/]{86}==$", row["contentHash"]?.GetValue() ?? string.Empty); + Assert.Matches("^[0-9a-f]{64}$", row["nuspecSha256"]?.GetValue() ?? string.Empty); + Assert.StartsWith("https://api.nuget.org/v3-flatcontainer/", row["source"]?.GetValue() ?? string.Empty, StringComparison.Ordinal); + var licence = row["licence"]?.GetValue() ?? string.Empty; + Assert.Contains(licence, PermissiveLicences); + Assert.EndsWith("/10.0.12", entry.Key, StringComparison.Ordinal); + } + } + + [Fact] + public void AspNetCoreBrowserWasmPackIsNotAdmitted() + { + Assert.False(AdmittedDownloads(RepositoryRoot.Find()).ContainsKey("microsoft.aspnetcore.app.runtime.browser-wasm/10.0.12")); + } + + private static JsonObject AdmittedDownloads(string root) + { + var policy = JsonNode.Parse(File.ReadAllText(Path.Combine(root, "eng", "policy", "dependency-policy.json")))!.AsObject(); + return policy["nugetPackDownloads"]!.AsObject(); + } + + private static List> DeclaredDownloads(string root) + { + var declared = new List>(); + foreach (var project in Directory.EnumerateFiles(root, "*.csproj", SearchOption.AllDirectories) + .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + && !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal))) + { + foreach (Match match in DeclaredDownload.Matches(File.ReadAllText(project))) + declared.Add(new KeyValuePair( + match.Groups["id"].Value.ToLowerInvariant() + "/" + match.Groups["version"].Value, + Path.GetRelativePath(root, project))); + } + return declared; + } +} diff --git a/tests/ArcForges.Web.Site.Tests/ContentSecurityPolicyTests.cs b/tests/ArcForges.Web.Site.Tests/ContentSecurityPolicyTests.cs new file mode 100644 index 0000000..d63d922 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/ContentSecurityPolicyTests.cs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Security.Cryptography; +using System.Text; +using ArcForges.Web.Site; +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class ContentSecurityPolicyTests +{ + [Fact] + public void PagesWithoutScriptsGetTheSelfOnlyScriptSource() + { + var policy = SiteContentSecurityPolicy.FromPages(["plain"]); + + Assert.Equal(SiteOutputTests.ExpectedPolicy, policy); + } + + [Fact] + public void AnInlineScriptBodyIsAddedAsItsSha256Hash() + { + const string body = "console.log(1);"; + var hash = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(body))); + + var policy = SiteContentSecurityPolicy.FromPages([$""]); + + Assert.Contains($"script-src 'self' 'sha256-{hash}';", policy, StringComparison.Ordinal); + Assert.DoesNotContain("unsafe-inline", policy, StringComparison.Ordinal); + } + + [Fact] + public void HashesAreSortedAndDeduplicatedAcrossPages() + { + var one = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes("a()"))); + var two = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes("b()"))); + + var policy = SiteContentSecurityPolicy.FromPages(["", ""]); + + var sorted = new[] { $"'sha256-{one}'", $"'sha256-{two}'" }.OrderBy(token => token, StringComparer.Ordinal); + Assert.Contains("script-src 'self' " + string.Join(' ', sorted) + ";", policy, StringComparison.Ordinal); + } + + [Fact] + public void AnEmptyInlineScriptIsNotHashed() + { + var policy = SiteContentSecurityPolicy.FromPages([""]); + + Assert.Equal(SiteOutputTests.ExpectedPolicy, policy); + } + + [Theory] + [InlineData("")] + [InlineData("")] + [InlineData("")] + [InlineData("")] + public void AnExternalScriptIsRefused(string html) + { + var error = Assert.Throws(() => SiteContentSecurityPolicy.FromPages([html])); + Assert.Contains("external script", error.Message, StringComparison.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(""]); + + Assert.DoesNotContain("unsafe-inline", policy, StringComparison.Ordinal); + Assert.DoesNotContain("unsafe-eval", policy, StringComparison.Ordinal); + Assert.DoesNotContain("wasm-unsafe-eval", policy, StringComparison.Ordinal); + Assert.True(policy.Length < SiteContentSecurityPolicy.HeaderLineBudget); + } + + [Fact] + public void APolicyThatExceedsTheHeaderBudgetIsRefusedByTheHeaderRenderer() + { + var oversized = "default-src 'self'; " + new string('x', SiteContentSecurityPolicy.HeaderLineBudget); + + Assert.Throws(() => SiteSecurityHeaders.Render(oversized)); + } +} diff --git a/tests/ArcForges.Web.Site.Tests/GreetingTests.cs b/tests/ArcForges.Web.Site.Tests/GreetingTests.cs new file mode 100644 index 0000000..21988c9 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/GreetingTests.cs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of tests/unit/hello.test.tsx (the greeting part: the published protobuf round trip). +using ArcForges.Web.Ui; +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class GreetingTests +{ + public static TheoryData InvalidNames => + [ + " ", + "", + new string('x', 81), + "hi" + (char)0, + "tab" + '\t', + "del" + (char)127, + string.Concat(Enumerable.Repeat("🌍", 81)), + ]; + + [Fact] + public void PublishedProtobufRoundTripPreservesUnicodeNames() + { + Assert.Equal("Hello, 世界 🌍!", HelloGreeting.Greet(" 世界 🌍 ")); + var longest = string.Concat(Enumerable.Repeat("🌍", 80)); + Assert.Equal("Hello, " + longest + "!", HelloGreeting.Greet(longest)); + } + + [Theory] + [MemberData(nameof(InvalidNames))] + public void InvalidNamesAreRefusedWithTheExactFailureText(string invalid) + { + var error = Assert.Throws(() => HelloGreeting.Greet(invalid)); + Assert.Equal("Enter a name between 1 and 80 characters, without control characters.", error.Message); + } + + [Fact] + public void TheInitialGreetingIsTheReactGreetingForTheDefaultName() + { + Assert.Equal("World", HelloGreeting.DefaultName); + Assert.Equal("Hello, World!", HelloGreeting.Greet(HelloGreeting.DefaultName)); + } + + [Fact] + public void UnpairedSurrogateIsEncodedAsTheReplacementCharacterLikeTheBrowserEncoder() + { + var lone = "a" + (char)0xD800 + "b"; + Assert.Equal("Hello, a�b!", HelloGreeting.Greet(lone)); + } +} diff --git a/tests/ArcForges.Web.Site.Tests/GrpcWebFixture.cs b/tests/ArcForges.Web.Site.Tests/GrpcWebFixture.cs new file mode 100644 index 0000000..17c5a93 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/GrpcWebFixture.cs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Test-only gRPC-Web fixture (the port of tests/fixtures/grpc-web.ts). It encodes and decodes the frames +// independently of the product framing code, so the tests do not check the framing with itself. +using System.Buffers.Binary; +using System.Text; +using ArcForges.Contracts.Hello.V1; +using Google.Protobuf; + +namespace ArcForges.Web.Site.Tests; + +internal static class GrpcWebFixture +{ + public const byte DataFlag = 0; + public const byte TrailersFlag = 0x80; + + public static byte[] Frame(byte[] payload, byte flag) + { + var bytes = new byte[payload.Length + 5]; + bytes[0] = flag; + BinaryPrimitives.WriteUInt32BigEndian(bytes.AsSpan(1, 4), (uint)payload.Length); + payload.CopyTo(bytes, 5); + return bytes; + } + + /// Returns the name of one uncompressed SayHello request frame, and fails for any other shape. + public static string DecodeHelloRequestName(byte[] bytes) + { + if (bytes.Length < 5 || bytes[0] != DataFlag || BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(1, 4)) != bytes.Length - 5) + throw new InvalidOperationException("Expected one uncompressed protobuf request frame"); + return SayHelloRequest.Parser.ParseFrom(bytes.AsSpan(5).ToArray()).Name; + } + + /// A unary OK reply: one data frame with the message, then the trailers frame with grpc-status 0. + public static byte[] HelloResponse(string message = "Hello, ArcForges!") + { + var data = Frame(new SayHelloResponse { Message = message }.ToByteArray(), DataFlag); + var trailers = Frame(Encoding.UTF8.GetBytes("grpc-status: 0\r\n"), TrailersFlag); + return [.. data, .. trailers]; + } + + public static byte[] Concat(params byte[][] parts) + { + var bytes = new List(); + foreach (var part in parts) + bytes.AddRange(part); + return bytes.ToArray(); + } +} diff --git a/tests/ArcForges.Web.Site.Tests/GrpcWebFramesTests.cs b/tests/ArcForges.Web.Site.Tests/GrpcWebFramesTests.cs new file mode 100644 index 0000000..1b7b82e --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/GrpcWebFramesTests.cs @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Failure cases of the unary gRPC-Web reply, ported from the malformed-frame and status cases of tests/unit/app-hello.test.ts. +using ArcForges.Contracts.Hello.V1; +using ArcForges.Web.Ui; +using Google.Protobuf; +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class GrpcWebFramesTests +{ + [Fact] + public void AWellFormedUnaryReplyYieldsTheMessageBytes() + { + var message = new SayHelloResponse { Message = "Hello, ArcForges!" }.ToByteArray(); + + var decoded = GrpcWebFrames.DecodeUnaryReply(GrpcWebFixture.HelloResponse()); + + Assert.Equal(message, decoded); + } + + [Fact] + public void FramesEncodedByTheProductAreReadBackByTheFixture() + { + var payload = new SayHelloRequest { Name = "ArcForges" }.ToByteArray(); + + var frame = GrpcWebFrames.Encode(payload, GrpcWebFrames.DataFlag); + + Assert.Equal("ArcForges", GrpcWebFixture.DecodeHelloRequestName(frame)); + } + + public static TheoryData MalformedReplies + { + get + { + var data = GrpcWebFixture.Frame(new SayHelloResponse { Message = "Hello, ArcForges!" }.ToByteArray(), GrpcWebFixture.DataFlag); + var trailersOk = GrpcWebFixture.Frame("grpc-status: 0\r\n"u8.ToArray(), GrpcWebFixture.TrailersFlag); + return new TheoryData + { + { "empty body", [] }, + { "truncated header", data[..3] }, + { "truncated message", data[..^2] }, + { "no trailers", data }, + { "trailers only", trailersOk }, + { "trailers without a status", GrpcWebFixture.Concat(data, GrpcWebFixture.Frame("x-other: 1\r\n"u8.ToArray(), GrpcWebFixture.TrailersFlag)) }, + { "unparsable status", GrpcWebFixture.Concat(data, GrpcWebFixture.Frame("grpc-status: ok\r\n"u8.ToArray(), GrpcWebFixture.TrailersFlag)) }, + { "non-OK status", GrpcWebFixture.Concat(data, GrpcWebFixture.Frame("grpc-status: 3\r\n"u8.ToArray(), GrpcWebFixture.TrailersFlag)) }, + { "compressed frame", GrpcWebFixture.Concat([1, .. data[1..]], trailersOk) }, + { "frame length beyond the body", GrpcWebFixture.Concat([0, 0, 0, 0, 200, .. "x"u8.ToArray()], trailersOk) }, + { "bytes after the trailers", GrpcWebFixture.Concat(data, trailersOk, [0]) }, + { "data frame after the trailers", GrpcWebFixture.Concat(data, trailersOk, data) }, + }; + } + } + + [Theory] + [MemberData(nameof(MalformedReplies))] + public void MalformedOrNonOkRepliesAreRefusedAndNeverResolve(string name, byte[] body) + { + var error = Assert.Throws(() => GrpcWebFrames.DecodeUnaryReply(body)); + Assert.False(string.IsNullOrEmpty(error.Message), name); + } + + [Fact] + public Task AMessageThatIsNotAProtobufSayHelloResponseIsRefusedByTheClient() + { + var garbage = GrpcWebFixture.Concat( + GrpcWebFixture.Frame([0xFF, 0xFF, 0xFF], GrpcWebFixture.DataFlag), + GrpcWebFixture.Frame("grpc-status: 0\r\n"u8.ToArray(), GrpcWebFixture.TrailersFlag)); + + var client = new ServerConnectionClient(new HttpMessageHandlerStub(garbage)); + + return Assert.ThrowsAsync(() => client.CheckAsync(new Uri("https://arcforges.com"), TestContext.Current.CancellationToken)); + } + + /// Answers one fixed binary body with the gRPC-Web media type. + private sealed class HttpMessageHandlerStub(byte[] body) : System.Net.Http.HttpMessageHandler + { + protected override Task SendAsync(System.Net.Http.HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new System.Net.Http.ByteArrayContent(body), + }; + response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(GrpcWebFrames.MediaType); + return Task.FromResult(response); + } + } +} diff --git a/tests/ArcForges.Web.Site.Tests/ServerConnectionTests.cs b/tests/ArcForges.Web.Site.Tests/ServerConnectionTests.cs new file mode 100644 index 0000000..d860bc5 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/ServerConnectionTests.cs @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of tests/unit/cloud-hello.test.ts (the server connection check) plus its failure and cancellation cases. +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using ArcForges.Web.Ui; +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class ServerConnectionTests +{ + private static readonly Uri Origin = new("https://arcforges.com"); + + [Fact] + public async Task ConnectionUsesThePublishedBinaryClientSameOriginApiAndNoCredentials() + { + var handler = new ScriptedHandler(_ => Reply(HttpStatusCode.OK, GrpcWebFixture.HelloResponse(), GrpcWebFrames.MediaType)); + var client = new ServerConnectionClient(handler); + + var message = await client.CheckAsync(Origin, TestContext.Current.CancellationToken); + + Assert.Equal("Hello, ArcForges!", message); + var seen = handler.Last ?? throw new InvalidOperationException("No request was sent."); + Assert.Equal("https://arcforges.com" + ServerConnectionClient.HelloApiPath, seen.Uri.AbsoluteUri); + Assert.Equal(HttpMethod.Post, seen.Method); + Assert.Contains("application/grpc-web+proto", seen.ContentType ?? string.Empty, StringComparison.Ordinal); + Assert.False(seen.HasAuthorization); + Assert.False(seen.HasCookie); + Assert.Equal("ArcForges", GrpcWebFixture.DecodeHelloRequestName(seen.Body)); + } + + [Fact] + public async Task TheSameCheckFromAnotherOriginPathUsesTheRootApiPath() + { + var handler = new ScriptedHandler(_ => Reply(HttpStatusCode.OK, GrpcWebFixture.HelloResponse(), GrpcWebFrames.MediaType)); + var client = new ServerConnectionClient(handler); + + await client.CheckAsync(new Uri("https://arcforges.com/hello/?name=ignored"), TestContext.Current.CancellationToken); + + Assert.Equal("https://arcforges.com" + ServerConnectionClient.HelloApiPath, handler.Last?.Uri.AbsoluteUri); + } + + [Fact] + public async Task MissingApiAndUnexpectedServerOutputNeverBecomeASuccessfulGreeting() + { + var answers = new (string Name, Func Answer)[] + { + ("404", () => new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("Not found") }), + ("405", () => new HttpResponseMessage(HttpStatusCode.MethodNotAllowed) { Content = new StringContent("Method not allowed") }), + ("html", () => Reply(HttpStatusCode.OK, Encoding.UTF8.GetBytes("Not an API"), "text/html")), + ("other service", () => Reply(HttpStatusCode.OK, GrpcWebFixture.HelloResponse("Hello, different service!"), GrpcWebFrames.MediaType)), + ("no content type", () => new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(GrpcWebFixture.HelloResponse()) }), + ("redirect refused", () => new HttpResponseMessage(HttpStatusCode.Redirect) { Headers = { Location = new Uri("https://other.example/") } }), + ("server error", () => new HttpResponseMessage(HttpStatusCode.InternalServerError)), + }; + foreach (var (name, answer) in answers) + { + var client = new ServerConnectionClient(new ScriptedHandler(_ => answer())); + var error = await Assert.ThrowsAsync(() => client.CheckAsync(Origin, TestContext.Current.CancellationToken)); + Assert.False(string.IsNullOrEmpty(error.Message), name); + } + } + + [Fact] + public async Task AnUnreachableServerIsATypedFailureNotAGreeting() + { + var client = new ServerConnectionClient(new ScriptedHandler(_ => throw new HttpRequestException("network down"))); + + await Assert.ThrowsAsync(() => client.CheckAsync(Origin, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task TheUsersCancellationAbortsTheRequestAndIsNotReportedAsAGreeting() + { + using var cancellation = new CancellationTokenSource(); + var handler = new ScriptedHandler(_ => throw new InvalidOperationException("the request must not complete"), waitForCancellation: true); + var client = new ServerConnectionClient(handler); + var pending = client.CheckAsync(Origin, cancellation.Token); + + await cancellation.CancelAsync(); + + await Assert.ThrowsAnyAsync(() => pending); + Assert.True(handler.WasCancelled); + } + + [Fact] + public async Task AlreadyCancelledCheckNeverReachesASuccess() + { + using var cancellation = new CancellationTokenSource(); + await cancellation.CancelAsync(); + var client = new ServerConnectionClient(new ScriptedHandler(_ => Reply(HttpStatusCode.OK, GrpcWebFixture.HelloResponse(), GrpcWebFrames.MediaType))); + + await Assert.ThrowsAnyAsync(() => client.CheckAsync(Origin, cancellation.Token)); + } + + [Fact] + public void TheDeadlineIsTenSecondsAsInTheReactExample() + { + Assert.Equal(TimeSpan.FromSeconds(10), ServerConnectionClient.Deadline); + } + + [Fact] + public async Task AReplyBeyondTheCostLimitIsRefusedBeforeItIsParsed() + { + var oversized = new byte[ServerConnectionClient.MaximumReplyBytes + 1]; + var client = new ServerConnectionClient(new ScriptedHandler(_ => Reply(HttpStatusCode.OK, oversized, GrpcWebFrames.MediaType))); + + var error = await Assert.ThrowsAsync(() => client.CheckAsync(Origin, TestContext.Current.CancellationToken)); + Assert.Contains("larger than the limit", error.Message, StringComparison.Ordinal); + } + + private static HttpResponseMessage Reply(HttpStatusCode status, byte[] body, string mediaType) + { + var response = new HttpResponseMessage(status) { Content = new ByteArrayContent(body) }; + response.Content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); + return response; + } + + private sealed record Captured(Uri Uri, HttpMethod Method, string? ContentType, bool HasAuthorization, bool HasCookie, byte[] Body); + + /// Answers each request with a scripted reply and records what was sent. + private sealed class ScriptedHandler(Func respond, bool waitForCancellation = false) : HttpMessageHandler + { + public Captured? Last { get; private set; } + + public bool WasCancelled { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var body = request.Content is null ? [] : await request.Content.ReadAsByteArrayAsync(cancellationToken); + Last = new Captured( + request.RequestUri ?? throw new InvalidOperationException("No request URI."), + request.Method, + request.Content?.Headers.ContentType?.MediaType, + request.Headers.Authorization is not null, + request.Headers.Contains("Cookie"), + body); + if (waitForCancellation) + { + try + { + await Task.Delay(Timeout.Infinite, cancellationToken); + } + catch (OperationCanceledException) + { + WasCancelled = true; + throw; + } + } + return respond(request); + } + } +} diff --git a/tests/ArcForges.Web.Site.Tests/SiteOutputTests.cs b/tests/ArcForges.Web.Site.Tests/SiteOutputTests.cs new file mode 100644 index 0000000..d786628 --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/SiteOutputTests.cs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Page inventory, determinism, no-script readability and the exact headers and policy of the static public Site. +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using ArcForges.Web.Site; +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class SiteOutputTests +{ + /// The exact public Site policy: no WebAssembly, no unsafe-inline, no unsafe-eval and no external origin. + internal const string ExpectedPolicy = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'"; + + private const string ExpectedHeaders = + "/*\n" + + " Content-Security-Policy: " + ExpectedPolicy + "\n" + + " X-Content-Type-Options: nosniff\n" + + " Referrer-Policy: no-referrer\n" + + " Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()\n" + + " X-Frame-Options: DENY\n" + + " X-Robots-Tag: noindex, nofollow\n" + + " Cache-Control: public, no-cache, no-transform\n" + + "/assets/*\n" + + " ! Cache-Control\n" + + " Cache-Control: public, max-age=31536000, immutable, no-transform\n" + + "/__build.json\n" + + " ! Cache-Control\n" + + " Cache-Control: no-store, no-transform\n" + + "/__build-info.json\n" + + " ! Cache-Control\n" + + " Cache-Control: no-store, no-transform\n"; + + private static readonly string[] HtmlPaths = ["index.html", "hello/index.html", "cloud-hello/index.html"]; + + private static readonly Regex StylesheetPath = new(@"^assets/site\.[0-9a-f]{16}\.css$", RegexOptions.CultureInvariant); + + private static readonly Regex EventHandlerAttribute = new(@"\son[a-z]+\s*=", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + [Fact] + public async Task TheOutputIsExactlyThePublicPageSetAndItsHeadersAndAssets() + { + var site = await SiteBuilder.BuildAsync(new SiteOptions(), TestContext.Current.CancellationToken); + + var paths = site.Files.Select(file => file.Path).ToArray(); + var stylesheet = Assert.Single(paths, path => StylesheetPath.IsMatch(path)); + Assert.Equal( + new[] { "404.css", "404.html", "_headers", "cloud-hello/index.html", "favicon.svg", "hello/index.html", "index.html", "robots.txt", stylesheet }.OrderBy(path => path, StringComparer.Ordinal), + paths); + Assert.Equal(paths.OrderBy(path => path, StringComparer.Ordinal), paths); + } + + [Fact] + public async Task TwoBuildsOfTheSameInputsProduceTheSameBytes() + { + var first = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = new string('a', 40) }, TestContext.Current.CancellationToken); + var second = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = new string('a', 40) }, TestContext.Current.CancellationToken); + + Assert.Equal(first.Files.Select(file => file.Path), second.Files.Select(file => file.Path)); + foreach (var (left, right) in first.Files.Zip(second.Files)) + { + Assert.Equal(left.Path, right.Path); + Assert.Equal(left.Sha256, right.Sha256); + Assert.True(left.Content.AsSpan().SequenceEqual(right.Content), left.Path); + } + } + + [Theory] + [MemberData(nameof(HtmlFiles))] + public async Task EveryPageIsReadableWithScriptingDisabled(string path) + { + var html = await PageHtml(path); + + Assert.DoesNotContain("world.", html, StringComparison.Ordinal); + Assert.Contains("Every good thing starts somewhere.", html, StringComparison.Ordinal); + Assert.Contains("Welcome to the first page of ArcForges Web.", html, StringComparison.Ordinal); + Assert.Contains("href=\"/hello\"", html, StringComparison.Ordinal); + Assert.Contains("Make it your hello", html, StringComparison.Ordinal); + Assert.Contains("A beginning you can explore.", html, StringComparison.Ordinal); + Assert.Contains("This small preview has one job: say hello.", html, StringComparison.Ordinal); + Assert.Contains("aria-labelledby=\"intro-title\"", html, StringComparison.Ordinal); + } + + [Fact] + public async Task TheHelloPageIsTheReactPrerenderWithItsControlsDisabledAndTheDefaultGreeting() + { + var html = await PageHtml("hello/index.html"); + + Assert.Equal("Your hello — ArcForges", Title(html)); + Assert.Contains("A hello, just for you.", html, StringComparison.Ordinal); + Assert.Contains("Start with your name. This example runs in your browser, and your name stays on this page.", html, StringComparison.Ordinal); + Assert.Contains("", html, StringComparison.Ordinal); + Assert.Matches(new Regex("]*id=\"hello-name\"[^>]*disabled[^>]*value=\"World\"", RegexOptions.CultureInvariant), html); + Assert.Contains("aria-invalid=\"false\"", html, StringComparison.Ordinal); + Assert.Contains("aria-describedby=\"name-hint\"", html, StringComparison.Ordinal); + Assert.Contains("Up to 80 characters. No account needed.", html, StringComparison.Ordinal); + Assert.DoesNotContain("role=\"alert\"", html, StringComparison.Ordinal); + Assert.Contains("The example greeting is shown below. Enable JavaScript to personalize it.", html, StringComparison.Ordinal); + Assert.Contains("Your greeting", html, StringComparison.Ordinal); + Assert.Contains("Hello, World!", html, StringComparison.Ordinal); + Assert.Contains("href=\"/cloud-hello/\"", html, StringComparison.Ordinal); + Assert.Matches(new Regex("]*disabled[^>]*>Say hello", RegexOptions.CultureInvariant), html); + } + + [Fact] + public async Task TheCloudHelloPageIsTheReactPrerenderWithNoRequestSent() + { + var html = await PageHtml("cloud-hello/index.html"); + + Assert.Equal("Server connection — ArcForges", Title(html)); + Assert.Contains("A hello, from the server.", html, StringComparison.Ordinal); + Assert.Contains("Send a fixed “ArcForges” greeting to check the server connection.", html, StringComparison.Ordinal); + Assert.Contains("Nothing is sent until you choose to connect.", html, StringComparison.Ordinal); + Assert.Matches(new Regex("]*disabled[^>]*>Check connection", RegexOptions.CultureInvariant), html); + Assert.Contains("Enable JavaScript to check the server connection.", html, StringComparison.Ordinal); + Assert.Contains("Server response", html, StringComparison.Ordinal); + Assert.Contains("No request sent yet.", html, StringComparison.Ordinal); + Assert.Contains("href=\"/hello/\"", html, StringComparison.Ordinal); + Assert.DoesNotContain("api/", html, StringComparison.Ordinal); + } + + [Fact] + public async Task EveryPageLinksTheShellNavigationAndTheSourceLinkOfTheBuild() + { + foreach (var path in HtmlPaths) + { + var html = await PageHtml(path); + Assert.Contains("href=\"#main\"", html, StringComparison.Ordinal); + Assert.Contains("aria-label=\"ArcForges home\"", html, StringComparison.Ordinal); + Assert.Contains("aria-label=\"Main navigation\"", html, StringComparison.Ordinal); + Assert.Contains("href=\"https://github.com/ArcForges/Web\"", html, StringComparison.Ordinal); + Assert.Contains("href=\"/license.txt\"", html, StringComparison.Ordinal); + Assert.Contains("href=\"/third-party-notices.txt\"", html, StringComparison.Ordinal); + Assert.Contains("A small beginning. Built in the open.", html, StringComparison.Ordinal); + } + } + + [Fact] + public async Task TheSourceLinkPointsAtTheBuildCommitWhenItIsKnown() + { + var commit = new string('f', 40); + var site = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = commit }, TestContext.Current.CancellationToken); + + var html = Encoding.UTF8.GetString(site.Find("index.html")!.Content); + Assert.Contains("href=\"https://github.com/ArcForges/Web/tree/" + commit + "\"", html, StringComparison.Ordinal); + Assert.DoesNotContain("href=\"https://github.com/ArcForges/Web\"", html, StringComparison.Ordinal); + } + + [Theory] + [InlineData("")] + [InlineData("ABCDEF0123456789ABCDEF0123456789ABCDEF01")] + [InlineData("0123456789abcdef0123456789abcdef0123456")] + [InlineData("0123456789abcdef0123456789abcdef012345678")] + [InlineData("0123456789abcdef0123456789abcdef01234\"567")] + public void ASourceRefThatIsNotAFullLowerCaseCommitIsRefused(string sourceRef) + { + Assert.Throws(() => SiteBuilder.SourceUrl(sourceRef)); + } + + [Fact] + public async Task EveryPageLinksTheContentHashedStylesheetThatTheSiteEmits() + { + var site = await SiteBuilder.BuildAsync(new SiteOptions(), TestContext.Current.CancellationToken); + + var stylesheet = site.Files.Single(file => StylesheetPath.IsMatch(file.Path)); + foreach (var path in HtmlPaths) + { + var html = Encoding.UTF8.GetString(site.Find(path)!.Content); + Assert.Contains("Page not found — ArcForges", notFound, StringComparison.Ordinal); + Assert.Contains("

Page not found.

", notFound, StringComparison.Ordinal); + Assert.Contains("This page is not part of the preview.", notFound, StringComparison.Ordinal); + Assert.Contains("href=\"/\"", notFound, StringComparison.Ordinal); + Assert.DoesNotContain(" HtmlFiles => new(HtmlPaths); + + private static async Task PageHtml(string path) + { + var site = await SiteBuilder.BuildAsync(new SiteOptions(), TestContext.Current.CancellationToken); + return Encoding.UTF8.GetString(site.Find(path)?.Content ?? throw new InvalidOperationException("Missing page: " + path)); + } + + private static string Title(string html) + { + var match = Regex.Match(html, "(.*?)", RegexOptions.Singleline | RegexOptions.CultureInvariant); + Assert.True(match.Success, "No title element."); + return WebUtility.HtmlDecode(match.Groups[1].Value); + } +} diff --git a/tests/ArcForges.Web.Site.Tests/packages.lock.json b/tests/ArcForges.Web.Site.Tests/packages.lock.json index 239d750..633c6d6 100644 --- a/tests/ArcForges.Web.Site.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Site.Tests/packages.lock.json @@ -19,6 +19,24 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -128,7 +146,27 @@ } }, "arcforges.web.site": { - "type": "Project" + "type": "Project", + "dependencies": { + "ArcForges.Web.Ui": "[1.0.0, )" + } + }, + "arcforges.web.ui": { + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } } } } diff --git a/tests/ArcForges.Web.Ui.Tests/CloudHelloExampleTests.cs b/tests/ArcForges.Web.Ui.Tests/CloudHelloExampleTests.cs new file mode 100644 index 0000000..ea9818b --- /dev/null +++ b/tests/ArcForges.Web.Ui.Tests/CloudHelloExampleTests.cs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The cloud-hello control states: the fixed greeting on success and the exact failure text on any error. +using Bunit; +using Xunit; + +namespace ArcForges.Web.Ui.Tests; + +public sealed class CloudHelloExampleTests +{ + private const string FailureText = "The server is unavailable or returned an unexpected response. Try again later."; + + [Fact] + public void TheCheckShowsTheServerGreetingOnSuccess() + { + using var context = new BunitContext(); + var cut = context.Render(parameters => parameters.Add(component => component.CheckConnection, _ => Task.FromResult("Hello, ArcForges!"))); + + cut.Find("button").Click(); + + cut.WaitForAssertion(() => Assert.Equal("Hello, ArcForges!", cut.Find("[role=status] p").TextContent)); + } + + [Fact] + public void AnyFailureShowsTheExactFailureTextAsAnAlert() + { + using var context = new BunitContext(); + var cut = context.Render(parameters => parameters.Add(component => component.CheckConnection, _ => Task.FromException(new ServerConnectionException("refused")))); + + cut.Find("button").Click(); + + cut.WaitForAssertion(() => Assert.Equal(FailureText, cut.Find("[role=alert] p").TextContent)); + } + + [Fact] + public void TheControlDoesNothingWhenNoCheckIsSupplied() + { + using var context = new BunitContext(); + + var cut = context.Render(); + + Assert.Equal("No request sent yet.", cut.Find("[role=status] p").TextContent); + Assert.Equal("Check connection", cut.Find("button").TextContent.Trim()); + cut.Find("button").Click(); + Assert.Equal("No request sent yet.", cut.Find("[role=status] p").TextContent); + } +} diff --git a/tests/ArcForges.Web.Ui.Tests/HelloExampleTests.cs b/tests/ArcForges.Web.Ui.Tests/HelloExampleTests.cs new file mode 100644 index 0000000..8075999 --- /dev/null +++ b/tests/ArcForges.Web.Ui.Tests/HelloExampleTests.cs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of tests/unit/hello.test.tsx (the form part): validation, recovery and untrusted names rendered as text. +using Bunit; +using Xunit; + +namespace ArcForges.Web.Ui.Tests; + +public sealed class HelloExampleTests +{ + [Fact] + public void FormValidatesRecoversAndRendersUntrustedNamesAsText() + { + using var context = new BunitContext(); + var cut = context.Render(); + var input = cut.Find("#hello-name"); + + input.Input(string.Empty); + cut.Find("form").Submit(); + Assert.Equal("Enter a name between 1 and 80 characters, without control characters.", cut.Find("[role=alert]").TextContent); + + input.Input(""); + cut.Find("form").Submit(); + Assert.Empty(cut.FindAll("[role=alert]")); + Assert.Equal("Hello, !", cut.Find("[role=status] p").TextContent); + Assert.Empty(cut.FindAll("img")); + } + + [Fact] + public void TheControlsAreEnabledAfterTheFirstInteractiveRenderAndTheDefaultGreetingIsShown() + { + using var context = new BunitContext(); + + var cut = context.Render(); + + Assert.Null(cut.Find("#hello-name").GetAttribute("disabled")); + Assert.Equal("Hello, World!", cut.Find("[role=status] p").TextContent); + Assert.Equal("false", cut.Find("#hello-name").GetAttribute("aria-invalid")); + Assert.Equal("name-hint", cut.Find("#hello-name").GetAttribute("aria-describedby")); + } + + [Fact] + public void AnInvalidNameMarksTheFieldAndDescribesTheError() + { + using var context = new BunitContext(); + var cut = context.Render(); + + cut.Find("#hello-name").Input(" "); + cut.Find("form").Submit(); + + Assert.Equal("true", cut.Find("#hello-name").GetAttribute("aria-invalid")); + Assert.Equal("name-error name-hint", cut.Find("#hello-name").GetAttribute("aria-describedby")); + } +} diff --git a/tests/ArcForges.Web.Ui.Tests/packages.lock.json b/tests/ArcForges.Web.Ui.Tests/packages.lock.json index 8fd23c3..2b49ecc 100644 --- a/tests/ArcForges.Web.Ui.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Ui.Tests/packages.lock.json @@ -61,6 +61,24 @@ "AngleSharp.Css": "1.0.0-beta.144" } }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -434,7 +452,21 @@ } }, "arcforges.web.ui": { - "type": "Project" + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } }, "Microsoft.AspNetCore.Components.WebAssembly": { "type": "CentralTransitive", From e01b6d20db1fc3db4974372425ce8523c56ee57c Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 20:02:09 +0100 Subject: [PATCH 03/38] [WEB.40] Admit the Blazor WebAssembly profile closure (web-40-admission-r3) Step 3, unit U1, first commit: the dependency admission for the Account and Chat profiles and the Operations skeleton, and the project files that carry it. - eng/policy/dependency-reviews/web-40-admission-r3.json supersedes r2 (r2 is unchanged). Its review reviewer field is the pre-assigned independent reviewer w-deku-20261008-rev-web-40, not PENDING. - eng/policy/dependency-policy.json binds 39 input digests (every csproj, props, targets and packages.lock.json, plus the earlier named inputs; CRLF is normalised to LF as the policy digests them) and 65 nugetClosure rows, each read from the restored lock files and checked against its nuspec. - Directory.Packages.props, win.slnx, the App and Operations project files, and the App, Operations and test packages.lock.json files are the write-scope project files for these packages. - Restored offline-checked with dotnet 10.0.401 (global.json, no adapter needed). Co-Authored-By: Claude Haiku 5.5 --- Directory.Packages.props | 3 + eng/policy/dependency-policy.json | 124 ++- .../web-40-admission-r3.json | 995 ++++++++++++++++++ .../ArcForges.Web.App.csproj | 2 + src/ArcForges.Web.App/packages.lock.json | 30 + .../packages.lock.json | 40 +- .../ArcForges.Web.App.Tests.csproj | 1 + .../packages.lock.json | 111 ++ .../ArcForges.Web.Operations.Tests.csproj | 1 + .../packages.lock.json | 119 ++- win.slnx | 1 + 11 files changed, 1387 insertions(+), 40 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r3.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 8aa135b..7dbcb6d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,7 +7,10 @@ + + + diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 4a3b685..9f3c677 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -9,87 +9,91 @@ "packages/ui/package.json" ], "inputs": { + ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", - "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", - "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", - "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", - "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", - "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", - ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", + "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", - "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", - "Directory.Packages.props": "1f12df7c9a4f0be605147f19028a2909a4e5175e4b7d6536f053cfa7c27ecca6", - "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", - "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "72cd82a690be6cfa33c5435a43222deec18ba5d00bc34355092f5ed4d1178707", - "src/ArcForges.Web.App/packages.lock.json": "1b43bb0452f1e2afab5139ad6ea8b30a872097f0293ba7fae3d2ec1c4291329d", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "89f8b63f55195b4e2a4acab4d3fda62cef5746cea75164a2fa8bd6928fff2991", "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", - "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Operations/packages.lock.json": "b9ac8f3f8b997294fa229042e5cbb4451900ad4849c1847fb648be5689191457", "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", - "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", - "tests/ArcForges.Web.App.Tests/packages.lock.json": "c8b392108a5fdb80ef8ed7abeb81e7636f97b5945b41a52f46fea2e20bb8b49e", - "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", - "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "91076f8a58e052f5e4afc90ac543a2e6b492def2caf37ba2cc8116706e3a791d", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "ba9ed72f6455e68fcc7026995263eb8199f9b85df70991c49e42fd79842d76a3", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "aa12e838198e5e764c1e8552044a27d22275b797566bc128dd2c6d9e314bc28a", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", - "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36" }, "review": { "owner": "Web dependency maintainer", - "reviewer": "w-deku-20261008-web-40 (Haiku 5.5 execution agent); independent exact-head review pending on the pull request", + "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-08", - "rationale": "WEB.40 step 2 (static Site) completes two section 10 items before the Site code is admitted. (a) CON.07 identity: the NuGet identity is verified from the Plan ledger record con-07.md (Contracts PR 74, merge ca45f36cccbdd31380f76b8f6cdecc958fdcb430, candidate 1.0.0-ci.287.1, publication run 36966686225) and from the nuget.org flat-container indexes and nuspecs of the twelve packable Contracts projects; every one lists 1.0.0-ci.287.1, carries the repository commit ca45f36 and is Apache-2.0 (the table is in the con07Identity field of this receipt). Only the four packages that the Site and the Ui class library restore are admitted: ArcForges.Contracts.PublicApi (direct, Ui), ArcForges.Contracts.Foundation, Google.Protobuf 3.36.1 and Grpc.Core.Api 2.84.0 (transitive). The Contracts are published packages consumed by exact version, never imported from the Contracts source tree. (b) Browser-wasm packs: the Mono browser-wasm runtime pack and the WebAssembly SDK pack are admitted at 10.0.12, the SDK 10.0.401 bundled runtime version, in the nugetPackDownloads section because NuGet records PackageDownload items outside packages.lock.json. The first is added implicitly for the BlazorWebAssembly profile, the second is declared in ArcForges.Web.App.csproj. Microsoft.AspNetCore.App.Runtime.browser-wasm is not admitted because no such package exists on nuget.org (flat-container BlobNotFound on 2026-10-08); the ASP.NET Core browser pieces ship in the framework reference. The npm closure, package.json, package-lock.json and every npm manifest are unchanged. Every changed csproj, props and packages.lock.json input is re-hashed in inputs.", + "rationale": "WEB.40 step 3 (Blazor WebAssembly Account and Chat profiles). Adds the gRPC-Web client transport (Grpc.Net.Client and Grpc.Net.Client.Web 2.84.0, published 2026-09-21, Apache-2.0) that the generated Contracts hello client uses, and the Microsoft.Playwright 1.62.0 (published 2026-08-11, Apache-2.0) test-only local opt-in browser tooling. The transitive closure is the nugetClosure rows: every row was read from the restored lock files and its nuspec fetched from the nuget.org flat container. Playwright is never run in CI (LocalOptIn gate). The input digests are rebound over every csproj, props, targets and packages.lock.json in the worktree.", "inputs": { + ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", - "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", - "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", - "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", - "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", - "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", - ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", + "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", - "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", - "Directory.Packages.props": "1f12df7c9a4f0be605147f19028a2909a4e5175e4b7d6536f053cfa7c27ecca6", - "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", - "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "72cd82a690be6cfa33c5435a43222deec18ba5d00bc34355092f5ed4d1178707", - "src/ArcForges.Web.App/packages.lock.json": "1b43bb0452f1e2afab5139ad6ea8b30a872097f0293ba7fae3d2ec1c4291329d", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "89f8b63f55195b4e2a4acab4d3fda62cef5746cea75164a2fa8bd6928fff2991", "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", - "src/ArcForges.Web.Operations/packages.lock.json": "44893a821771b709c25c9160409dba17975509e4caafde96170fa8cb117d88c6", + "src/ArcForges.Web.Operations/packages.lock.json": "b9ac8f3f8b997294fa229042e5cbb4451900ad4849c1847fb648be5689191457", "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", - "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d25992dbda756b373199a9f1816ea28bbd8b4a3db780ba20b18c51bfdc799d8a", - "tests/ArcForges.Web.App.Tests/packages.lock.json": "c8b392108a5fdb80ef8ed7abeb81e7636f97b5945b41a52f46fea2e20bb8b49e", - "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "80cb8accb604f58e44fc222143555092b0d643ed86745c9a1341e379708eb40c", - "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "1f4c7621817358e1a0e21b7ae8f72edcb9d86a60792e326011c951468c2c0c59", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "91076f8a58e052f5e4afc90ac543a2e6b492def2caf37ba2cc8116706e3a791d", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "ba9ed72f6455e68fcc7026995263eb8199f9b85df70991c49e42fd79842d76a3", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "aa12e838198e5e764c1e8552044a27d22275b797566bc128dd2c6d9e314bc28a", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", - "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33" + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36" }, "checks": [ "locked-restore", @@ -2223,7 +2227,9 @@ }, "registry": "https://registry.npmjs.org/", "channel": "foundation-candidate", - "artifactRecords": ["browser-resources-r10"], + "artifactRecords": [ + "browser-resources-r10" + ], "native": { "mode": "existing-artifact-closure-only", "evidence": [ @@ -2237,7 +2243,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r2.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r3.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", @@ -2316,6 +2322,27 @@ "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, "microsoft.applicationinsights/2.23.0": { "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", "licence": "MIT", @@ -2323,6 +2350,13 @@ "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, "microsoft.aspnetcore.authorization/10.0.10": { "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", "licence": "MIT", @@ -2568,6 +2602,13 @@ "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, "microsoft.testing.extensions.telemetry/2.4.0": { "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", "licence": "MIT", @@ -2603,6 +2644,13 @@ "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, "system.security.accesscontrol/6.0.1": { "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", "licence": "MIT", diff --git a/eng/policy/dependency-reviews/web-40-admission-r3.json b/eng/policy/dependency-reviews/web-40-admission-r3.json new file mode 100644 index 0000000..0304e26 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r3.json @@ -0,0 +1,995 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-08", + "rationale": "WEB.40 step 3 (Blazor WebAssembly Account and Chat profiles). Adds the gRPC-Web client transport (Grpc.Net.Client and Grpc.Net.Client.Web 2.84.0, published 2026-09-21, Apache-2.0) that the generated Contracts hello client uses, and the Microsoft.Playwright 1.62.0 (published 2026-08-11, Apache-2.0) test-only local opt-in browser tooling. The transitive closure is the nugetClosure rows: every row was read from the restored lock files and its nuspec fetched from the nuget.org flat container. Playwright is never run in CI (LocalOptIn gate). The input digests are rebound over every csproj, props, targets and packages.lock.json in the worktree.", + "inputs": { + ".github/workflows/ci.yml": "bd8facf68d4758b846db01d87e0cda803d9387ef9e3db66c6e860e0896d01fb5", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", + "eng/provenance/profiles/browser-resources-r10.json": "f1cfc05a10dbf988096d2f7f6da86971d55f0fa033e1e12ee3a3a4bebf3117c5", + "eng/provenance/records/browser-resources-r10.json": "01b6b16095c78de16005b18c06d41a9b3e0aede3cfd5d686cc8df98e0450f07b", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "89f8b63f55195b4e2a4acab4d3fda62cef5746cea75164a2fa8bd6928fff2991", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "b9ac8f3f8b997294fa229042e5cbb4451900ad4849c1847fb648be5689191457", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "91076f8a58e052f5e4afc90ac543a2e6b492def2caf37ba2cc8116706e3a791d", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "ba9ed72f6455e68fcc7026995263eb8199f9b85df70991c49e42fd79842d76a3", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "aa12e838198e5e764c1e8552044a27d22275b797566bc128dd2c6d9e314bc28a", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "8414f9ee72d10cd960a9cebe1de8ab2b8b0ee91fdc8be646378be25e1bc360e8", + "tools/ArcForges.Web.Tooling/packages.lock.json": "7019b7a06d181c6252a1db00bf8d3ffd9de7ea066a5c0151917de92e2470fe33", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "Step 2 restores and compiles the Razor class library, the static Site generator and their xUnit and bUnit test projects against the admitted closure. No browser-wasm build, publish, AOT compilation, browser run or deployment is performed by this admission. The browser-wasm packs are admitted for the later Blazor profile steps and are not executed here.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r1.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@adobe/css-tools@4.5.0": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "@arcforges/api-client@1.0.0-ci.287.1": "sha512-Nzr25VLIi8nSRQcO46gs2QobNp8yiw51Tf+5th4ZpB19Q6DoweWVs0CYSDhbtAAH8uAIC1IzLiKywEsoD48Geg==", + "@arcforges/proto@1.0.0-ci.287.1": "sha512-lJNdv4ReiG1KF54tziiJpkHgDHS96XsO504no07Ly7cnXCqqK9O0d5o6AaufIn08K7J7C5IR+Hpbq2ZFCrL8+w==", + "@asamuzakjp/css-color@7.0.0": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", + "lru-cache@11.5.2": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "@asamuzakjp/dom-selector@9.2.0": "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw==", + "@axe-core/playwright@4.13.0": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "@babel/code-frame@7.29.7": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "@babel/compat-data@7.29.7": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "@babel/core@7.29.7": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "semver@6.3.1": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "@babel/generator@7.29.8": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "@babel/helper-compilation-targets@7.29.7": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "@babel/helper-globals@7.29.7": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "@babel/helper-module-imports@7.29.7": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "@babel/helper-module-transforms@7.29.7": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "@babel/helper-string-parser@7.29.7": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "@babel/helper-validator-identifier@7.29.7": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "@babel/helper-validator-option@7.29.7": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "@babel/helpers@7.29.7": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "@babel/parser@7.29.8": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "@babel/runtime@7.29.7": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "@babel/template@7.29.7": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "@babel/traverse@7.29.8": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "@babel/types@7.29.8": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "@biomejs/biome@2.5.14": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", + "@biomejs/cli-darwin-arm64@2.5.14": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", + "@biomejs/cli-darwin-x64@2.5.14": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", + "@biomejs/cli-linux-arm64@2.5.14": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", + "@biomejs/cli-linux-arm64-musl@2.5.14": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", + "@biomejs/cli-linux-x64@2.5.14": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", + "@biomejs/cli-linux-x64-musl@2.5.14": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", + "@biomejs/cli-win32-arm64@2.5.14": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", + "@biomejs/cli-win32-x64@2.5.14": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", + "@bramus/specificity@2.4.2": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "@bufbuild/protobuf@2.15.0": "sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==", + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@connectrpc/connect@2.2.0": "sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==", + "@connectrpc/connect-web@2.2.0": "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@csstools/color-helpers@6.1.1": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "@csstools/css-calc@3.4.0": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "@csstools/css-color-parser@4.2.3": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "@csstools/css-parser-algorithms@4.0.0": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "@csstools/css-syntax-patches-for-csstree@1.1.14": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "@csstools/css-tokenizer@4.0.1": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@exodus/bytes@1.15.1": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/gen-mapping@0.3.13": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "@jridgewell/remapping@2.3.5": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@jridgewell/trace-mapping@0.3.31": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "@oxc-project/types@0.149.0": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "@playwright/test@1.63.0": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@react-router/dev@8.4.0": "sha512-GKznpn6h8c52xGXwtsypD5ZwNiWFSeqoKPdyGq7SfG+T9rFjGO8F0i/R/yECfZqHXaNz1NVmpODuBamBy9FfMg==", + "@react-router/node@8.4.0": "sha512-1teZHtk7XDm1pteQ2Plgtvtjjup5C5sIwFlOMYVIyoOH7X3bId3Qb//kUdbydX6NRIxcGK28UnfZlz4SrD/mkg==", + "@remix-run/node-fetch-server@0.14.1": "sha512-pKenF5ysIN5lDCnCyvoUxDhKP0tfnGagvGbZh01xxHG6DJXSIXWVC9NpLSDGjxcxd1CKdCOKuZFleXBF3pChIg==", + "@remix-run/route-pattern@0.22.1": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "@rolldown/binding-android-arm-eabi@1.2.8": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "@rolldown/binding-android-arm64@1.2.8": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "@rolldown/binding-darwin-arm64@1.2.8": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "@rolldown/binding-darwin-x64@1.2.8": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "@rolldown/binding-freebsd-x64@1.2.8": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "@rolldown/binding-linux-arm-gnueabihf@1.2.8": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "@rolldown/binding-linux-arm64-gnu@1.2.8": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "@rolldown/binding-linux-arm64-musl@1.2.8": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "@rolldown/binding-linux-ppc64-gnu@1.2.8": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "@rolldown/binding-linux-s390x-gnu@1.2.8": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "@rolldown/binding-linux-x64-gnu@1.2.8": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "@rolldown/binding-linux-x64-musl@1.2.8": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "@rolldown/binding-openharmony-arm64@1.2.8": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "@rolldown/binding-win32-arm64-msvc@1.2.8": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "@rolldown/binding-win32-x64-msvc@1.2.8": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "@rolldown/pluginutils@1.0.1": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@tailwindcss/node@4.3.3": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "@tailwindcss/oxide@4.3.3": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "@tailwindcss/oxide-android-arm64@4.3.3": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "@tailwindcss/oxide-darwin-arm64@4.3.3": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "@tailwindcss/oxide-darwin-x64@4.3.3": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "@tailwindcss/oxide-freebsd-x64@4.3.3": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "@tailwindcss/oxide-linux-x64-musl@4.3.3": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "@tailwindcss/oxide-wasm32-wasi@4.3.3": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "@tailwindcss/vite@4.3.3": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "@testing-library/dom@10.4.2": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "@testing-library/jest-dom@7.0.1": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dom-accessibility-api@0.6.3": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "@testing-library/react@16.3.3": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "@types/aria-query@5.0.4": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "@types/chai@5.2.3": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "@types/deep-eql@4.0.2": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "@types/estree@1.0.9": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@types/react@19.3.0": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "@types/react-dom@19.3.0": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "@vitest/mocker@5.0.1": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "magic-string@1.4.1": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "@vitest/spy@5.0.1": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "ansi-regex@5.0.1": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "ansi-styles@5.2.0": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "aria-query@5.3.0": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "assertion-error@2.0.1": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "axe-core@4.13.0": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "babel-dead-code-elimination@1.0.12": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "baseline-browser-mapping@2.11.24": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "bidi-js@1.1.0": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "browserslist@4.29.0": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "caniuse-lite@1.0.30001810": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "chai@6.2.2": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "chokidar@5.0.0": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "confbox@0.3.1": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "convert-source-map@2.0.0": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "cookie-es@3.1.1": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "css-tree@3.2.1": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "css.escape@1.5.1": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "csstype@3.2.3": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "data-urls@7.0.0": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "whatwg-url@16.0.1": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "debug@4.4.3": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "decimal.js@10.6.0": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dedent@1.7.2": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dequal@2.0.3": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dom-accessibility-api@0.5.16": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "electron-to-chromium@1.5.429": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==", + "enhanced-resolve@5.25.1": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "entities@8.1.0": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "es-module-lexer@2.3.2": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "escalade@3.2.0": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "estree-walker@3.0.3": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "exit-hook@5.1.0": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "expect-type@1.4.0": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "exsolve@1.1.1": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "fdir@6.5.0": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "gensync@1.0.0-beta.2": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "graceful-fs@4.2.11": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "html-encoding-sniffer@6.0.0": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "indent-string@4.0.0": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "is-potential-custom-element-name@1.0.1": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "isbot@5.2.2": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "jiti@2.7.0": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "js-tokens@4.0.0": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "jsdom@30.1.0": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "jsesc@3.1.0": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "json5@2.2.3": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "lightningcss@1.32.0": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "lightningcss-android-arm64@1.32.0": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "lightningcss-darwin-arm64@1.32.0": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "lightningcss-darwin-x64@1.32.0": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "lightningcss-freebsd-x64@1.32.0": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "lightningcss-linux-arm-gnueabihf@1.32.0": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "lightningcss-linux-arm64-gnu@1.32.0": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "lightningcss-linux-arm64-musl@1.32.0": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "lightningcss-linux-x64-gnu@1.32.0": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "lightningcss-linux-x64-musl@1.32.0": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "lightningcss-win32-arm64-msvc@1.32.0": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "lightningcss-win32-x64-msvc@1.32.0": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "lodash@4.18.1": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "lru-cache@5.1.1": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "lz-string@1.5.0": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "magic-string@0.30.21": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "mdn-data@2.27.1": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "min-indent@1.0.1": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "ms@2.1.3": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "nanoid@3.3.19": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node-releases@2.0.55": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "obug@2.2.1": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "p-map@7.0.8": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "parse5@8.0.1": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "picocolors@1.1.1": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "picomatch@4.0.7": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "pkg-types@2.3.3": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "playwright@1.63.0": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "playwright-core@1.63.0": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "postcss@8.5.28": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "prettier@3.9.8": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "pretty-format@27.5.1": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "punycode@2.3.1": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "react@19.3.0": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "react-dom@19.3.0": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "react-is@17.0.2": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "react-refresh@0.18.0": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "react-router@8.4.0": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "readdirp@5.1.1": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "redent@3.0.0": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "require-from-string@2.0.2": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "rolldown@1.2.8": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "saxes@6.0.0": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "scheduler@0.28.0": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "siginfo@2.0.0": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "source-map-js@1.2.2": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "stackback@0.0.2": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "std-env@4.2.0": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "strip-indent@3.0.0": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tailwindcss@4.3.3": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "tapable@2.3.3": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "tinybench@6.1.4": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "tinyexec@1.3.0": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "tinyglobby@0.2.17": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "tldts@7.4.13": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", + "tldts-core@7.4.13": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", + "tough-cookie@6.0.2": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "tr46@6.0.0": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici@8.10.2": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "update-browserslist-db@1.3.3": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "valibot@1.5.0": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "vite@8.3.0": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "lightningcss@1.33.0": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "lightningcss-android-arm64@1.33.0": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "lightningcss-darwin-arm64@1.33.0": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "lightningcss-darwin-x64@1.33.0": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "lightningcss-freebsd-x64@1.33.0": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "lightningcss-linux-arm-gnueabihf@1.33.0": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "lightningcss-linux-arm64-gnu@1.33.0": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "lightningcss-linux-arm64-musl@1.33.0": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "lightningcss-linux-x64-gnu@1.33.0": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "lightningcss-linux-x64-musl@1.33.0": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "lightningcss-win32-arm64-msvc@1.33.0": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "lightningcss-win32-x64-msvc@1.33.0": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "vitest@5.0.1": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "w3c-xmlserializer@5.0.0": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "webidl-conversions@8.0.1": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "whatwg-mimetype@5.0.0": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "whatwg-url@17.1.1": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "why-is-node-running@2.3.0": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "xml-name-validator@5.0.0": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "xmlchars@2.2.0": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "yallist@3.1.1": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r2.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/src/ArcForges.Web.App/ArcForges.Web.App.csproj b/src/ArcForges.Web.App/ArcForges.Web.App.csproj index 68c8fc9..14899e8 100644 --- a/src/ArcForges.Web.App/ArcForges.Web.App.csproj +++ b/src/ArcForges.Web.App/ArcForges.Web.App.csproj @@ -6,6 +6,8 @@
+ + diff --git a/src/ArcForges.Web.App/packages.lock.json b/src/ArcForges.Web.App/packages.lock.json index b030892..ee11a52 100644 --- a/src/ArcForges.Web.App/packages.lock.json +++ b/src/ArcForges.Web.App/packages.lock.json @@ -8,6 +8,28 @@ "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" }, + "Grpc.Net.Client": { + "type": "Direct", + "requested": "[2.84.0, )", + "resolved": "2.84.0", + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "dependencies": { + "Grpc.Net.Common": "2.84.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1" + } + }, + "Grpc.Net.Client.Web": { + "type": "Direct", + "requested": "[2.84.0, )", + "resolved": "2.84.0", + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==" + }, + "Microsoft.AspNetCore.App.Internal.Assets": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==" + }, "Microsoft.AspNetCore.Components.WebAssembly": { "type": "Direct", "requested": "[10.0.10, )", @@ -51,6 +73,14 @@ "resolved": "2.84.0", "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "dependencies": { + "Grpc.Core.Api": "2.84.0" + } + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "10.0.10", diff --git a/src/ArcForges.Web.Operations/packages.lock.json b/src/ArcForges.Web.Operations/packages.lock.json index f971269..80c7f1a 100644 --- a/src/ArcForges.Web.Operations/packages.lock.json +++ b/src/ArcForges.Web.Operations/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" }, + "Microsoft.AspNetCore.App.Internal.Assets": { + "type": "Direct", + "requested": "[10.0.12, )", + "resolved": "10.0.12", + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==" + }, "Microsoft.AspNetCore.Components.WebAssembly": { "type": "Direct", "requested": "[10.0.10, )", @@ -33,6 +39,24 @@ "resolved": "10.0.12", "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==" }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, "Microsoft.AspNetCore.Authorization": { "type": "Transitive", "resolved": "10.0.10", @@ -255,7 +279,21 @@ } }, "arcforges.web.ui": { - "type": "Project" + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } } }, "net10.0/browser-wasm": {} diff --git a/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj b/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj index cda1cc9..bae9161 100644 --- a/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj +++ b/tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj @@ -6,6 +6,7 @@
+ diff --git a/tests/ArcForges.Web.App.Tests/packages.lock.json b/tests/ArcForges.Web.App.Tests/packages.lock.json index 9045e07..f015575 100644 --- a/tests/ArcForges.Web.App.Tests/packages.lock.json +++ b/tests/ArcForges.Web.App.Tests/packages.lock.json @@ -8,6 +8,26 @@ "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" }, + "bunit": { + "type": "Direct", + "requested": "[2.9.0, )", + "resolved": "2.9.0", + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "dependencies": { + "AngleSharp": "1.7.0", + "AngleSharp.Css": "1.0.1", + "AngleSharp.Diffing": "1.1.1", + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.AspNetCore.Components.WebAssembly": "10.0.10", + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Localization.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, "xunit.v3.mtp-v2": { "type": "Direct", "requested": "[4.0.1, )", @@ -19,6 +39,28 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, + "AngleSharp": { + "type": "Transitive", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "dependencies": { + "AngleSharp": "[1.5.0, 2.0.0)" + } + }, + "AngleSharp.Diffing": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "dependencies": { + "AngleSharp": "1.1.2", + "AngleSharp.Css": "1.0.0-beta.144" + } + }, "ArcForges.Contracts.Foundation": { "type": "Transitive", "resolved": "1.0.0-ci.287.1", @@ -37,6 +79,14 @@ "resolved": "2.84.0", "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" }, + "Grpc.Net.Common": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "dependencies": { + "Grpc.Core.Api": "2.84.0" + } + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -67,6 +117,15 @@ "resolved": "10.0.10", "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" }, + "Microsoft.AspNetCore.Components.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components": "10.0.10" + } + }, "Microsoft.AspNetCore.Components.Forms": { "type": "Transitive", "resolved": "10.0.10", @@ -88,6 +147,15 @@ "Microsoft.JSInterop": "10.0.10" } }, + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "dependencies": { + "Microsoft.AspNetCore.Components.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Web": "10.0.10" + } + }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "10.0.10", @@ -98,6 +166,26 @@ "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.10", @@ -202,6 +290,11 @@ "resolved": "10.0.10", "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==" + }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.10", @@ -370,6 +463,8 @@ "type": "Project", "dependencies": { "ArcForges.Web.Ui": "[1.0.0, )", + "Grpc.Net.Client": "[2.84.0, )", + "Grpc.Net.Client.Web": "[2.84.0, )", "Microsoft.AspNetCore.Components.WebAssembly": "[10.0.10, )" } }, @@ -390,6 +485,22 @@ "Grpc.Core.Api": "2.84.0" } }, + "Grpc.Net.Client": { + "type": "CentralTransitive", + "requested": "[2.84.0, )", + "resolved": "2.84.0", + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "dependencies": { + "Grpc.Net.Common": "2.84.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.1" + } + }, + "Grpc.Net.Client.Web": { + "type": "CentralTransitive", + "requested": "[2.84.0, )", + "resolved": "2.84.0", + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==" + }, "Microsoft.AspNetCore.Components.WebAssembly": { "type": "CentralTransitive", "requested": "[10.0.10, )", diff --git a/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj b/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj index a23e80c..ea656cd 100644 --- a/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj +++ b/tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/tests/ArcForges.Web.Operations.Tests/packages.lock.json b/tests/ArcForges.Web.Operations.Tests/packages.lock.json index 0223f59..a896d45 100644 --- a/tests/ArcForges.Web.Operations.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Operations.Tests/packages.lock.json @@ -8,6 +8,26 @@ "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" }, + "bunit": { + "type": "Direct", + "requested": "[2.9.0, )", + "resolved": "2.9.0", + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "dependencies": { + "AngleSharp": "1.7.0", + "AngleSharp.Css": "1.0.1", + "AngleSharp.Diffing": "1.1.1", + "Microsoft.AspNetCore.Components": "10.0.10", + "Microsoft.AspNetCore.Components.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Web": "10.0.10", + "Microsoft.AspNetCore.Components.WebAssembly": "10.0.10", + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": "10.0.10", + "Microsoft.Extensions.Caching.Memory": "10.0.10", + "Microsoft.Extensions.Localization.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + } + }, "xunit.v3.mtp-v2": { "type": "Direct", "requested": "[4.0.1, )", @@ -19,6 +39,46 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, + "AngleSharp": { + "type": "Transitive", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "dependencies": { + "AngleSharp": "[1.5.0, 2.0.0)" + } + }, + "AngleSharp.Diffing": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "dependencies": { + "AngleSharp": "1.1.2", + "AngleSharp.Css": "1.0.0-beta.144" + } + }, + "ArcForges.Contracts.Foundation": { + "type": "Transitive", + "resolved": "1.0.0-ci.287.1", + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "dependencies": { + "Google.Protobuf": "3.36.1" + } + }, + "Google.Protobuf": { + "type": "Transitive", + "resolved": "3.36.1", + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==" + }, + "Grpc.Core.Api": { + "type": "Transitive", + "resolved": "2.84.0", + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==" + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -49,6 +109,15 @@ "resolved": "10.0.10", "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==" }, + "Microsoft.AspNetCore.Components.Authorization": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components": "10.0.10" + } + }, "Microsoft.AspNetCore.Components.Forms": { "type": "Transitive", "resolved": "10.0.10", @@ -70,6 +139,15 @@ "Microsoft.JSInterop": "10.0.10" } }, + "Microsoft.AspNetCore.Components.WebAssembly.Authentication": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "dependencies": { + "Microsoft.AspNetCore.Components.Authorization": "10.0.10", + "Microsoft.AspNetCore.Components.Web": "10.0.10" + } + }, "Microsoft.AspNetCore.Metadata": { "type": "Transitive", "resolved": "10.0.10", @@ -80,6 +158,26 @@ "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.10", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", + "Microsoft.Extensions.Logging.Abstractions": "10.0.10", + "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Primitives": "10.0.10" + } + }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.10", @@ -184,6 +282,11 @@ "resolved": "10.0.10", "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==" }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "10.0.10", + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==" + }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.10", @@ -356,7 +459,21 @@ } }, "arcforges.web.ui": { - "type": "Project" + "type": "Project", + "dependencies": { + "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" + } + }, + "ArcForges.Contracts.PublicApi": { + "type": "CentralTransitive", + "requested": "[1.0.0-ci.287.1, )", + "resolved": "1.0.0-ci.287.1", + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "dependencies": { + "ArcForges.Contracts.Foundation": "1.0.0-ci.287.1", + "Google.Protobuf": "3.36.1", + "Grpc.Core.Api": "2.84.0" + } }, "Microsoft.AspNetCore.Components.WebAssembly": { "type": "CentralTransitive", diff --git a/win.slnx b/win.slnx index 084104b..54a9ece 100644 --- a/win.slnx +++ b/win.slnx @@ -18,5 +18,6 @@ + From 472741922f85bcef8e9fdf1f73f0abdd8d9dd115 Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 20:02:23 +0100 Subject: [PATCH 04/38] [WEB.40] Blazor WebAssembly Account and Chat profiles and the Operations skeleton Step 3, unit U1, second commit: the standalone Blazor WebAssembly source of the Account and Chat deployment profiles (ArcForges.Web.App, replacing apps/app) and the Operations profile skeleton (ArcForges.Web.Operations), plus the shared profile CSP helper in ArcForges.Web.Ui. - Probe/HelloProbe.cs: the anonymous binary gRPC-Web greeting through the generated Contracts client. Fixed in this unit: the frame watcher read the frame length from the flag byte plus three length bytes, so a trailers frame never reported its grpc-status (trailers-only answers became malformed); the watcher now reads the four length bytes after the flag. The watched content also copies bytes through the serialize path the library uses to buffer an answer, so the status is seen on every path. Bytes are never changed. - Pages/Chat.razor and Pages/Account.razor, Probe/*.cs, App.razor, layouts and wwwroot: the same semantics as apps/app (routes/chat.tsx, probe/*.ts): one message at a time, the Sending state, Cancel, cancellation on leaving the page, the last twenty transcript entries, fixed failure text, and cookie-session, CSRF and Origin rules unchanged. - Operations: the project, shell route and separate-origin configuration only; no operator feature code (OPS.05 and OPS.11 build those). - Ui/WasmContentSecurityPolicy.cs: the exact script-src token set for the App and Operations profiles ('self' 'wasm-unsafe-eval' plus required hashes; never unsafe-eval or unsafe-inline). Co-Authored-By: Claude Haiku 5.5 --- src/ArcForges.Web.App/App.razor | 13 + .../Layout/ProfileLayout.razor | 15 + src/ArcForges.Web.App/Pages/Account.razor | 173 +++++++ src/ArcForges.Web.App/Pages/Chat.razor | 98 ++++ src/ArcForges.Web.App/Probe/BoundedBody.cs | 74 +++ src/ArcForges.Web.App/Probe/ExactUnsigned.cs | 31 ++ src/ArcForges.Web.App/Probe/HelloProbe.cs | 354 ++++++++++++++ src/ArcForges.Web.App/Probe/ProbeFailure.cs | 100 ++++ src/ArcForges.Web.App/Probe/SessionProbe.cs | 156 ++++++ src/ArcForges.Web.App/Probe/WireJson.cs | 57 +++ src/ArcForges.Web.App/Program.cs | 13 + src/ArcForges.Web.App/_Imports.razor | 8 + src/ArcForges.Web.App/wwwroot/app.css | 450 ++++++++++++++++++ src/ArcForges.Web.App/wwwroot/favicon.svg | 1 + src/ArcForges.Web.App/wwwroot/index.html | 22 + src/ArcForges.Web.App/wwwroot/robots.txt | 2 + src/ArcForges.Web.Operations/App.razor | 13 + .../Layout/OperationsLayout.razor | 15 + .../Pages/Shell.razor | 11 + .../ProfileBoundary.cs | 16 + src/ArcForges.Web.Operations/Program.cs | 3 + src/ArcForges.Web.Operations/_Imports.razor | 6 + src/ArcForges.Web.Operations/wwwroot/app.css | 450 ++++++++++++++++++ .../wwwroot/favicon.svg | 1 + .../wwwroot/index.html | 22 + .../wwwroot/robots.txt | 2 + .../WasmContentSecurityPolicy.cs | 96 ++++ 27 files changed, 2202 insertions(+) create mode 100644 src/ArcForges.Web.App/App.razor create mode 100644 src/ArcForges.Web.App/Layout/ProfileLayout.razor create mode 100644 src/ArcForges.Web.App/Pages/Account.razor create mode 100644 src/ArcForges.Web.App/Pages/Chat.razor create mode 100644 src/ArcForges.Web.App/Probe/BoundedBody.cs create mode 100644 src/ArcForges.Web.App/Probe/ExactUnsigned.cs create mode 100644 src/ArcForges.Web.App/Probe/HelloProbe.cs create mode 100644 src/ArcForges.Web.App/Probe/ProbeFailure.cs create mode 100644 src/ArcForges.Web.App/Probe/SessionProbe.cs create mode 100644 src/ArcForges.Web.App/Probe/WireJson.cs create mode 100644 src/ArcForges.Web.App/_Imports.razor create mode 100644 src/ArcForges.Web.App/wwwroot/app.css create mode 100644 src/ArcForges.Web.App/wwwroot/favicon.svg create mode 100644 src/ArcForges.Web.App/wwwroot/index.html create mode 100644 src/ArcForges.Web.App/wwwroot/robots.txt create mode 100644 src/ArcForges.Web.Operations/App.razor create mode 100644 src/ArcForges.Web.Operations/Layout/OperationsLayout.razor create mode 100644 src/ArcForges.Web.Operations/Pages/Shell.razor create mode 100644 src/ArcForges.Web.Operations/ProfileBoundary.cs create mode 100644 src/ArcForges.Web.Operations/_Imports.razor create mode 100644 src/ArcForges.Web.Operations/wwwroot/app.css create mode 100644 src/ArcForges.Web.Operations/wwwroot/favicon.svg create mode 100644 src/ArcForges.Web.Operations/wwwroot/index.html create mode 100644 src/ArcForges.Web.Operations/wwwroot/robots.txt create mode 100644 src/ArcForges.Web.Ui/WasmContentSecurityPolicy.cs diff --git a/src/ArcForges.Web.App/App.razor b/src/ArcForges.Web.App/App.razor new file mode 100644 index 0000000..bbd599a --- /dev/null +++ b/src/ArcForges.Web.App/App.razor @@ -0,0 +1,13 @@ + + + + + + +
+

Page not found.

+

This profile has a single page.

+
+
+
+
diff --git a/src/ArcForges.Web.App/Layout/ProfileLayout.razor b/src/ArcForges.Web.App/Layout/ProfileLayout.razor new file mode 100644 index 0000000..5f562a9 --- /dev/null +++ b/src/ArcForges.Web.App/Layout/ProfileLayout.razor @@ -0,0 +1,15 @@ +@inherits LayoutComponentBase + +
+ + +
+ @Body +
+
+ Minimal proof profile. Not a product surface. +
+
diff --git a/src/ArcForges.Web.App/Pages/Account.razor b/src/ArcForges.Web.App/Pages/Account.razor new file mode 100644 index 0000000..144de90 --- /dev/null +++ b/src/ArcForges.Web.App/Pages/Account.razor @@ -0,0 +1,173 @@ +@page "/account" +@implements IDisposable +@inject SessionProbe Session + +Account session — ArcForges + +
+

Account profile

+

Your session, as the server sees it.

+ +
+ @if (_phase == Phase.Loading) + { +

Checking your session…

+ } + @if (_phase == Phase.Ended) + { +

You are signed out. Reload to check again.

+ } + @if (_phase == Phase.Failed) + { +

@FailureText.For(_failure)

+ } + @if (_session is AnonymousSession) + { +

You are not signed in.

+ } + @if (_session is AuthenticatedSession session) + { +
+
Signed in as
+
@(session.DisplayName ?? "(no display name)")
+
Locale
+
@(session.Locale ?? "—")
+
Time zone
+
@(session.Timezone ?? "—")
+
Workspaces
+
@session.Workspaces
+
Session ends
+
@session.ExpiresAt
+
Idle expiry
+
@(session.IdleExpiresAt ?? "—")
+
Recovery generation
+
@session.RecoveryGeneration
+
+ } +
+
+ @if (_session is AuthenticatedSession signedIn) + { + + } + @if (_phase == Phase.Failed) + { + + } +
+
+ +@code { + private enum Phase + { + Loading, + Ready, + Failed, + Ended, + } + + private Phase _phase = Phase.Loading; + private SessionSnapshot? _session; + private FailureKind _failure; + private bool _working; + private bool _disposed; + private CancellationTokenSource? _active; + + /// The session is read once when the page opens. + protected override async Task OnInitializedAsync() + { + await CheckAsync(); + } + + /// Reads the session again. A new read replaces a read that is still in flight. + private async Task CheckAsync() + { + _phase = Phase.Loading; + _session = null; + SessionSnapshot? session = null; + var completed = await RunAsync(async token => + { + session = await Session.ReadSessionAsync(token); + }); + if (completed && session is not null) + { + _session = session; + _phase = Phase.Ready; + } + Refresh(); + } + + private async Task SignOutAsync(string csrfToken) + { + _working = true; + var completed = await RunAsync(token => Session.EndSessionAsync(csrfToken, token)); + _working = false; + if (completed) + { + _session = null; + _phase = Phase.Ended; + } + Refresh(); + } + + /// + /// Runs one call and reports whether it completed. A cancelled call changes nothing. A typed failure shows the page's + /// failure state; an ended session is reported as ended, never as a transient fault. Any other exception is an + /// unexpected failure, so no exception leaves the page. + /// + private async Task RunAsync(Func task) + { + _active?.Cancel(); + var controller = new CancellationTokenSource(); + _active = controller; + try + { + await task(controller.Token); + return !controller.IsCancellationRequested; + } + catch (Exception exception) + { + if (!controller.IsCancellationRequested) + { + _session = null; + var kind = exception is ProbeFailureException failure ? failure.Kind : FailureKind.Unexpected; + if (kind == FailureKind.Unauthenticated) + { + _phase = Phase.Ended; + } + else + { + _phase = Phase.Failed; + _failure = kind; + } + } + return false; + } + finally + { + if (ReferenceEquals(_active, controller)) + { + _active = null; + } + controller.Dispose(); + } + } + + /// Re-renders unless the page has left; a call that ends after the unmount changes nothing on screen. + private void Refresh() + { + if (!_disposed) + { + StateHasChanged(); + } + } + + /// Leaving the page cancels the call that is still in flight. + public void Dispose() + { + _disposed = true; + _active?.Cancel(); + } +} diff --git a/src/ArcForges.Web.App/Pages/Chat.razor b/src/ArcForges.Web.App/Pages/Chat.razor new file mode 100644 index 0000000..69cfe65 --- /dev/null +++ b/src/ArcForges.Web.App/Pages/Chat.razor @@ -0,0 +1,98 @@ +@page "/chat" +@implements IDisposable +@inject HelloProbe Hello + +Chat probe — ArcForges + +
+

Chat profile

+

Say hello, to the server.

+

Each message is one anonymous gRPC-Web call. Nothing is sent until you choose to send.

+ +
+ + + @if (_pending) + { + + } +
+
    + @foreach (var entry in _entries) + { +
  1. @entry.Kind@entry.Text
  2. + } +
+
+ +@code { + /// The transcript keeps the last twenty entries. + public const int MaxEntries = 20; + + private sealed record Entry(int Id, string Kind, string Text); + + private readonly List _entries = []; + private string _name = "ArcForges"; + private bool _pending; + private int _counter; + private CancellationTokenSource? _active; + + private void Add(string kind, string text) + { + _entries.Add(new Entry(++_counter, kind, text)); + if (_entries.Count > MaxEntries) + { + _entries.RemoveRange(0, _entries.Count - MaxEntries); + } + } + + /// Sends one greeting. A second send while one is pending does nothing. + private async Task SendAsync() + { + if (_active is not null) + { + return; + } + var controller = new CancellationTokenSource(); + _active = controller; + _pending = true; + Add("you", _name); + try + { + var reply = await Hello.SayHelloAsync(_name, controller.Token); + Add("server", reply); + } + catch (Exception exception) + { + // A typed failure keeps its kind; any other exception is unexpected, and a cancelled send is cancelled. + var kind = exception is ProbeFailureException failure + ? failure.Kind + : controller.IsCancellationRequested ? FailureKind.Cancelled : FailureKind.Unexpected; + Add("notice", FailureText.For(kind)); + } + finally + { + if (ReferenceEquals(_active, controller)) + { + _active = null; + } + controller.Dispose(); + _pending = false; + } + } + + private void Cancel() + { + _active?.Cancel(); + } + + /// Leaving the page cancels the message that is still pending. + public void Dispose() + { + _active?.Cancel(); + } +} diff --git a/src/ArcForges.Web.App/Probe/BoundedBody.cs b/src/ArcForges.Web.App/Probe/BoundedBody.cs new file mode 100644 index 0000000..a25a914 --- /dev/null +++ b/src/ArcForges.Web.App/Probe/BoundedBody.cs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Globalization; +using System.Net.Http; + +namespace ArcForges.Web.App.Probe; + +/// +/// Reads a response body without ever holding more than limit bytes (the TypeScript readBounded rule). A declared +/// length above the limit is refused without reading; a stream above it is refused on the first byte beyond the limit, and +/// the stream is disposed so the transfer is cut off. +/// +public static class BoundedBody +{ + private const int ChunkSize = 4096; + + /// Reads at most bytes, or fails as malformed (above the limit) or unavailable/cancelled (transport). + public static async Task ReadAsync(HttpContent content, int limit, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(content); + if (content.Headers.TryGetValues("Content-Length", out var declaredValues)) + { + var declared = declaredValues.ToArray(); + if (declared.Length != 1 + || !long.TryParse(declared[0], NumberStyles.None, CultureInfo.InvariantCulture, out var length) + || length > limit) + throw new ProbeFailureException(FailureKind.Malformed); + } + + Stream stream; + try + { + stream = await content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (exception is not ProbeFailureException) + { + throw FromTransport(exception, cancellationToken); + } + + await using (stream) + { + using var buffer = new MemoryStream(); + var chunk = new byte[ChunkSize]; + long total = 0; + while (true) + { + // Never request more than one byte beyond the limit, so the first refused byte is the limit plus one. + var wanted = (int)Math.Min(chunk.Length, limit + 1L - total); + int read; + try + { + read = await stream.ReadAsync(chunk.AsMemory(0, wanted), cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (exception is not ProbeFailureException) + { + throw FromTransport(exception, cancellationToken); + } + + if (read == 0) + break; + total += read; + if (total > limit) + throw new ProbeFailureException(FailureKind.Malformed); + buffer.Write(chunk, 0, read); + } + return buffer.ToArray(); + } + } + + /// The transport failure kind: the caller's own cancellation is cancelled, anything else is unavailable. + internal static ProbeFailureException FromTransport(Exception cause, CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested + ? new ProbeFailureException(FailureKind.Cancelled, cause) + : new ProbeFailureException(FailureKind.Unavailable, cause); +} diff --git a/src/ArcForges.Web.App/Probe/ExactUnsigned.cs b/src/ArcForges.Web.App/Probe/ExactUnsigned.cs new file mode 100644 index 0000000..943be07 --- /dev/null +++ b/src/ArcForges.Web.App/Probe/ExactUnsigned.cs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Globalization; + +namespace ArcForges.Web.App.Probe; + +/// +/// A uint64 that crossed the wire as canonical decimal text. The text is validated, held as a and shown +/// as the same canonical text: no value passes through a floating-point number. +/// +public readonly record struct ExactUnsigned(ulong Value, string Text); + +/// The exact 64-bit parser (the TypeScript parseUInt64 rule: canonical digits, no sign, no leading zero, no space). +public static class Exact +{ + /// Parses canonical uint64 text, or returns false for any other text, including values above the uint64 maximum. + public static bool TryUnsigned(string? wire, out ExactUnsigned result) + { + result = default; + if (string.IsNullOrEmpty(wire)) + return false; + foreach (var character in wire) + if (character is < '0' or > '9') + return false; + if (wire.Length > 1 && wire[0] == '0') + return false; + if (!ulong.TryParse(wire, NumberStyles.None, CultureInfo.InvariantCulture, out var value)) + return false; + result = new ExactUnsigned(value, value.ToString(CultureInfo.InvariantCulture)); + return true; + } +} diff --git a/src/ArcForges.Web.App/Probe/HelloProbe.cs b/src/ArcForges.Web.App/Probe/HelloProbe.cs new file mode 100644 index 0000000..f4a5628 --- /dev/null +++ b/src/ArcForges.Web.App/Probe/HelloProbe.cs @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Globalization; +using System.Net; +using System.Text; +using ArcForges.Contracts.Hello.V1; +using Grpc.Core; +using Grpc.Net.Client; +using Grpc.Net.Client.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Http; + +namespace ArcForges.Web.App.Probe; + +/// +/// One anonymous greeting over binary gRPC-Web to same-origin /api (the TypeScript hello.ts port). The call uses the +/// generated Contracts over . It sends no cookie, +/// follows no redirect and ends at a ten second deadline. The failure kind is decided from what the transport saw before the +/// gRPC library mapped it, so a lossy library code never decides the kind alone. +/// +public sealed class HelloProbe +{ + /// The API path of the SayHello method, relative to the origin. + public const string HelloApiPath = "/api/arcforges.hello.v1.HelloService/SayHello"; + + /// The base of the gRPC path (the API prefix of the same origin). + public const string ApiBasePath = "/api"; + + /// The overall deadline of one greeting. + public static readonly TimeSpan DefaultDeadline = TimeSpan.FromSeconds(10); + + private readonly HttpMessageHandler _transport; + private readonly Uri _apiBase; + private readonly TimeSpan _deadline; + + /// Creates the probe with the ten second deadline. + public HelloProbe(HttpMessageHandler transport, ProbeOrigin origin) + : this(transport, origin, DefaultDeadline) + { + } + + /// Creates the probe with an explicit deadline (tests shorten it; the page uses the default). + public HelloProbe(HttpMessageHandler transport, ProbeOrigin origin, TimeSpan deadline) + { + ArgumentNullException.ThrowIfNull(transport); + ArgumentNullException.ThrowIfNull(origin); + if (deadline <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(deadline)); + _transport = transport; + _apiBase = new Uri(origin.Value, ApiBasePath); + _deadline = deadline; + } + + /// Sends one anonymous greeting and returns the reply, which must be the greeting for this name. + /// Any typed failure of the call. + public async Task SayHelloAsync(string name, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(name); + var seen = new Observation(); + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + deadline.CancelAfter(_deadline); + using var channel = GrpcChannel.ForAddress(_apiBase, new GrpcChannelOptions + { + HttpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new ObservingHandler(_transport, seen)), + DisposeHttpClient = false, + ThrowOperationCanceledOnCancellation = true, + }); + try + { + var client = new HelloService.HelloServiceClient(channel); + var options = new CallOptions( + deadline: DateTime.UtcNow.Add(_deadline), + cancellationToken: deadline.Token); + var reply = await client.SayHelloAsync(new SayHelloRequest { Name = name }, options).ResponseAsync.ConfigureAwait(false); + if (reply.Message != $"Hello, {name}!") + throw new ProbeFailureException(FailureKind.Malformed); + return reply.Message; + } + catch (Exception exception) when (exception is not ProbeFailureException) + { + throw Classify(exception, seen, cancellationToken, deadline.IsCancellationRequested); + } + } + + /// + /// The failure of a call that did not return a greeting. The caller's token and the transport decide first; the status the + /// server sent in its trailers, as the transport read it, decides the rest. Library texts never decide a kind. + /// + internal ProbeFailureException Classify(Exception exception, Observation seen, CancellationToken userToken, bool deadlineFired) + { + if (userToken.IsCancellationRequested) + return new ProbeFailureException(FailureKind.Cancelled, exception); + if (seen.Foreign) + return new ProbeFailureException(FailureKind.Malformed, exception); + if (seen.Status is int status && status != (int)HttpStatusCode.OK) + return new ProbeFailureException(FailureMapping.FromStatus(status), exception); + if (seen.Network) + return new ProbeFailureException(deadlineFired ? FailureKind.Timeout : FailureKind.Unavailable, exception); + if (deadlineFired) + return new ProbeFailureException(FailureKind.Timeout, exception); + // A non-OK server status is the server's answer, whatever exception the library raised for it. A successful status + // with no reply, or no readable status, is a malformed answer. + if (seen.TrailerStatus is int code && code != (int)StatusCode.OK) + return new ProbeFailureException(FailureMapping.FromGrpc((StatusCode)code), exception); + // An answer that the page received and could not read is malformed; no answer at all is unexpected. + return new ProbeFailureException(seen.Status is not null ? FailureKind.Malformed : FailureKind.Unexpected, exception); + } + + /// Parses a gRPC status number (0 to 16, canonical digits), or returns null for any other text. + internal static int? ParseStatus(string? text) + { + if (text is null || !int.TryParse(text.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var number)) + return null; + return number is >= 0 and <= 16 ? number : null; + } + + /// What the transport saw of one call, before the gRPC library mapped it. + internal sealed class Observation + { + /// The HTTP status of the answer, when one arrived. + public int? Status { get; set; } + + /// True when the transport failed before an answer, including a cancelled transport. + public bool Network { get; set; } + + /// True when a 200 answer was not gRPC-Web. + public bool Foreign { get; set; } + + /// The grpc-status the server sent (in the HTTP headers or a trailers frame), or null when none was readable. + public int? TrailerStatus { get; set; } + } + + /// The anonymous transport: no credentials, and the observation of the answer. + private sealed class ObservingHandler(HttpMessageHandler inner, Observation seen) : DelegatingHandler(inner) + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // The gRPC library resolves the method path against the authority and drops the base path, so the same-origin + // API prefix is restored here, once, and never doubled. + var requested = request.RequestUri ?? throw new InvalidOperationException("The request has no URL."); + if (!requested.AbsolutePath.StartsWith(ApiBasePath + "/", StringComparison.Ordinal)) + request.RequestUri = new UriBuilder(requested) { Path = ApiBasePath + requested.AbsolutePath }.Uri; + // The greeting is anonymous: no cookie is sent with it. + request.SetBrowserRequestCredentials(BrowserRequestCredentials.Omit); + HttpResponseMessage response; + try + { + response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + seen.Network = true; + throw; + } + + if ((int)response.StatusCode == 0) + { + // An opaque redirect the page refuses to follow is a refused transport, not an answer. + seen.Network = true; + response.Dispose(); + throw new HttpRequestException("A redirect is refused."); + } + seen.Status = (int)response.StatusCode; + var mediaType = response.Content.Headers.ContentType?.MediaType ?? string.Empty; + if (response.StatusCode == HttpStatusCode.OK + && !mediaType.StartsWith("application/grpc-web", StringComparison.OrdinalIgnoreCase)) + { + seen.Foreign = true; + response.Dispose(); + throw new InvalidOperationException("The answer is not gRPC-Web."); + } + if (response.Headers.TryGetValues("grpc-status", out var headerStatus)) + seen.TrailerStatus = ParseStatus(headerStatus.FirstOrDefault()); + // The body is read by the library through the watcher, so the status of its trailers frame is seen here first. + if (response.StatusCode == HttpStatusCode.OK && response.Content is not null) + response.Content = new WatchedContent(response.Content, seen); + return response; + } + } + + /// + /// The answer body as the library reads it: the same bytes, passed through a frame watcher that records the grpc-status of + /// a trailers frame (flag 0x80). The watcher reads no more than the library reads and never changes a byte. + /// + private sealed class WatchedContent : HttpContent + { + private readonly HttpContent _inner; + private readonly FrameWatcher _watcher; + + public WatchedContent(HttpContent inner, Observation seen) + { + _inner = inner; + _watcher = new FrameWatcher(seen); + foreach (var header in inner.Headers) + Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + /// + /// The library may buffer the whole answer through this path (for example for a trailers-only answer), so every byte + /// copied here is watched as well. The bytes written are exactly the bytes read. + /// + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + using var source = await _inner.ReadAsStreamAsync().ConfigureAwait(false); + var buffer = new byte[8192]; + int read; + while ((read = await source.ReadAsync(buffer.AsMemory()).ConfigureAwait(false)) > 0) + { + _watcher.Feed(buffer.AsSpan(0, read)); + await stream.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false); + } + } + + protected override bool TryComputeLength(out long length) + { + length = 0; + return false; + } + + protected override async Task CreateContentReadStreamAsync() => + new WatchedStream(await _inner.ReadAsStreamAsync().ConfigureAwait(false), _watcher); + + protected override void Dispose(bool disposing) + { + if (disposing) + _inner.Dispose(); + base.Dispose(disposing); + } + } + + /// A read-only stream that hands every byte it returns to the frame watcher. + private sealed class WatchedStream(Stream inner, FrameWatcher watcher) : Stream + { + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + var read = inner.Read(buffer, offset, count); + watcher.Feed(buffer.AsSpan(offset, read)); + return read; + } + + public override int Read(Span buffer) + { + var read = inner.Read(buffer); + watcher.Feed(buffer[..read]); + return read; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var read = await inner.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + watcher.Feed(buffer.Span[..read]); + return read; + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (disposing) + inner.Dispose(); + base.Dispose(disposing); + } + } + + /// + /// Follows gRPC-Web frames (one flag byte, a big-endian 32-bit length and the payload) and records the grpc-status of a + /// trailers frame. A trailers frame above is not read; a frame that never completes records + /// nothing, so the answer is then malformed. + /// + private sealed class FrameWatcher(Observation seen) + { + /// The largest trailers frame read for its status (a cost control; real trailers are far smaller). + public const int MaxTrailerBytes = 4096; + + private readonly byte[] _header = new byte[5]; + private int _headerLength; + private bool _inPayload; + private bool _isTrailer; + private long _remaining; + private byte[]? _trailer; + private int _trailerLength; + + public void Feed(ReadOnlySpan bytes) + { + var index = 0; + while (index < bytes.Length) + { + if (!_inPayload) + { + var take = Math.Min(_header.Length - _headerLength, bytes.Length - index); + bytes.Slice(index, take).CopyTo(_header.AsSpan(_headerLength)); + _headerLength += take; + index += take; + if (_headerLength < _header.Length) + return; + _headerLength = 0; + _isTrailer = (_header[0] & 0x80) != 0; + _remaining = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(_header.AsSpan(1, 4)); + _inPayload = true; + _trailer = _isTrailer && _remaining <= MaxTrailerBytes ? new byte[_remaining] : null; + _trailerLength = 0; + if (_remaining == 0) + FinishFrame(); + continue; + } + + var chunk = (int)Math.Min(_remaining, bytes.Length - index); + if (_trailer is not null) + { + bytes.Slice(index, chunk).CopyTo(_trailer.AsSpan(_trailerLength)); + _trailerLength += chunk; + } + index += chunk; + _remaining -= chunk; + if (_remaining == 0) + FinishFrame(); + } + } + + private void FinishFrame() + { + if (_trailer is not null) + RecordStatus(Encoding.UTF8.GetString(_trailer, 0, _trailerLength)); + _inPayload = false; + _trailer = null; + } + + private void RecordStatus(string trailers) + { + foreach (var line in trailers.Split('\n')) + { + var colon = line.IndexOf(':', StringComparison.Ordinal); + if (colon <= 0 || !string.Equals(line[..colon].Trim(), "grpc-status", StringComparison.OrdinalIgnoreCase)) + continue; + seen.TrailerStatus = ParseStatus(line[(colon + 1)..]); + } + } + } +} diff --git a/src/ArcForges.Web.App/Probe/ProbeFailure.cs b/src/ArcForges.Web.App/Probe/ProbeFailure.cs new file mode 100644 index 0000000..68fe202 --- /dev/null +++ b/src/ArcForges.Web.App/Probe/ProbeFailure.cs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using Grpc.Core; + +namespace ArcForges.Web.App.Probe; + +/// The closed set of reasons a probe call ends without a usable answer (the React failure kinds). +public enum FailureKind +{ + /// The user cancelled the call. + Cancelled, + /// The server did not answer before the deadline. + Timeout, + /// The server or the network is unavailable. + Unavailable, + /// The session has ended. + Unauthenticated, + /// The server refused the request. + Forbidden, + /// The server rejected the request. + Rejected, + /// The request exceeded a server limit. + Limit, + /// The answer is not something this page can read. + Malformed, + /// Anything else. + Unexpected, +} + +/// The fixed visible text of each failure kind. Server text is never echoed into a message. +public static class FailureText +{ + /// Returns the fixed text of the kind. + public static string For(FailureKind kind) => kind switch + { + FailureKind.Cancelled => "The request was cancelled.", + FailureKind.Timeout => "The server did not answer in time.", + FailureKind.Unavailable => "The server is unavailable. Try again later.", + FailureKind.Unauthenticated => "Your session has ended. Reload to check it again.", + FailureKind.Forbidden => "The server refused this request.", + FailureKind.Rejected => "The server rejected the request.", + FailureKind.Limit => "The request exceeded a server limit.", + FailureKind.Malformed => "The server answered with something this page cannot read.", + FailureKind.Unexpected => "The server failed unexpectedly. Try again later.", + _ => throw new ArgumentOutOfRangeException(nameof(kind)), + }; +} + +/// A typed probe failure. Its message is the fixed text of its kind, never server or request content. +public sealed class ProbeFailureException : Exception +{ + /// Creates the failure of one kind. + public ProbeFailureException(FailureKind kind, Exception? cause = null) + : base(FailureText.For(kind), cause) + { + Kind = kind; + } + + /// The closed failure kind. + public FailureKind Kind { get; } +} + +/// The status-to-kind mappings of the same-origin session routes and of the gRPC-Web hello call. +public static class FailureMapping +{ + /// The kind of an HTTP status of a session route (the failureFromStatus table). A 200 is never passed here. + public static FailureKind FromStatus(int status) + { + if (status == 401) + return FailureKind.Unauthenticated; + if (status == 403) + return FailureKind.Forbidden; + if (status is 408 or 504) + return FailureKind.Timeout; + if (status is 413 or 429) + return FailureKind.Limit; + if (status is 502 or 503) + return FailureKind.Unavailable; + if (status >= 500) + return FailureKind.Unexpected; + if (status >= 400) + return FailureKind.Rejected; + return FailureKind.Malformed; + } + + /// + /// The kind of a server gRPC status. A Cancelled status the user did not ask for is an unexpected server answer; + /// the user's own cancellation is detected from the caller's token. + /// + public static FailureKind FromGrpc(StatusCode code) => code switch + { + StatusCode.DeadlineExceeded => FailureKind.Timeout, + StatusCode.Unavailable or StatusCode.Aborted => FailureKind.Unavailable, + StatusCode.Unauthenticated => FailureKind.Unauthenticated, + StatusCode.PermissionDenied => FailureKind.Forbidden, + StatusCode.ResourceExhausted => FailureKind.Limit, + StatusCode.InvalidArgument or StatusCode.NotFound or StatusCode.AlreadyExists + or StatusCode.FailedPrecondition or StatusCode.OutOfRange or StatusCode.Unimplemented => FailureKind.Rejected, + _ => FailureKind.Unexpected, + }; +} diff --git a/src/ArcForges.Web.App/Probe/SessionProbe.cs b/src/ArcForges.Web.App/Probe/SessionProbe.cs new file mode 100644 index 0000000..191e27e --- /dev/null +++ b/src/ArcForges.Web.App/Probe/SessionProbe.cs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using ArcForges.Contracts.PublicApi.Http.V1.Browser; +using Microsoft.AspNetCore.Components.WebAssembly.Http; + +namespace ArcForges.Web.App.Probe; + +/// What the page may show about the current session. Ids and the cookie are never part of it. +public abstract record SessionSnapshot(string CsrfToken); + +/// The visitor has no session; only the CSRF token of the anonymous bootstrap is kept. +public sealed record AnonymousSession(string CsrfToken) : SessionSnapshot(CsrfToken); + +/// The visitor has a session. The recovery generation is the exact canonical decimal text. +public sealed record AuthenticatedSession( + string CsrfToken, + string? DisplayName, + string? Locale, + string? Timezone, + string ExpiresAt, + string? IdleExpiresAt, + int Workspaces, + string RecoveryGeneration) : SessionSnapshot(CsrfToken); + +/// The same-origin base address of the probes (scheme and authority of the page). +public sealed record ProbeOrigin(Uri Value); + +/// +/// The same-origin session exception routes (the TypeScript session.ts port): the bootstrap GET and the logout POST with +/// the X-AF-CSRF header. Route paths and methods come from the generated catalogue. +/// Requests carry the session cookie of the origin, follow no redirect in script, and are never cached. +/// +public sealed partial class SessionProbe +{ + /// The CSRF request header of cookie-authenticated unsafe operations (Design AU-08). + public const string CsrfHeader = "X-AF-CSRF"; + + /// The bootstrap body bound, from the generated schema (x-arcforges-max-bytes). + public const int BootstrapBodyLimit = 16384; + + /// The logout receipt body bound. + public const int ReceiptBodyLimit = 4096; + + /// The CSRF token shape of the bootstrap schema: 16 to 128 URL-safe characters. + [GeneratedRegex("^[A-Za-z0-9_-]{16,128}$", RegexOptions.CultureInvariant)] + private static partial Regex CsrfToken(); + + private static readonly BrowserSessionRoute BootstrapRoute = Route("browser.bootstrap"); + private static readonly BrowserSessionRoute LogoutRoute = Route("browser.logout"); + + private readonly HttpMessageInvoker _invoker; + private readonly Uri _origin; + + /// Creates the probe over a transport handler that keeps no state of its own. + public SessionProbe(HttpMessageHandler handler, ProbeOrigin origin) + { + ArgumentNullException.ThrowIfNull(handler); + ArgumentNullException.ThrowIfNull(origin); + _invoker = new HttpMessageInvoker(handler, disposeHandler: false); + _origin = origin.Value; + } + + /// GETs the bootstrap route, the cookie riding along, and returns what the page may show. + /// Any typed failure of the call. + public async Task ReadSessionAsync(CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(_origin, BootstrapRoute.Path)); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.SetBrowserRequestCredentials(BrowserRequestCredentials.SameOrigin); + request.SetBrowserRequestCache(BrowserRequestCache.NoStore); + using var response = await SendAsync(request, cancellationToken).ConfigureAwait(false); + var bytes = await JsonBodyAsync(response, BootstrapBodyLimit, cancellationToken).ConfigureAwait(false); + var body = WireJson.Decode(bytes, BrowserBootstrapResponseJsonContext.Default.BrowserBootstrapResponse); + return ToSnapshot(body); + } + + /// + /// POSTs the logout route with the CSRF token and no body. Only the receipt effect happened ends the session on + /// the page; didNotHappen and unknown are failures, because the cookie may still be valid. + /// + /// Any typed failure of the call. + public async Task EndSessionAsync(string csrfToken, CancellationToken cancellationToken) + { + ArgumentException.ThrowIfNullOrEmpty(csrfToken); + using var request = new HttpRequestMessage(HttpMethod.Post, new Uri(_origin, LogoutRoute.Path)); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.TryAddWithoutValidation(CsrfHeader, csrfToken); + request.SetBrowserRequestCredentials(BrowserRequestCredentials.SameOrigin); + request.SetBrowserRequestCache(BrowserRequestCache.NoStore); + using var response = await SendAsync(request, cancellationToken).ConfigureAwait(false); + var bytes = await JsonBodyAsync(response, ReceiptBodyLimit, cancellationToken).ConfigureAwait(false); + var receipt = WireJson.Decode(bytes, BrowserReceiptJsonContext.Default.BrowserReceipt); + if (receipt.Effect == "happened") + return; + throw new ProbeFailureException(receipt.Effect == "didNotHappen" ? FailureKind.Rejected : FailureKind.Unexpected); + } + + /// The page snapshot of a bootstrap answer. A contradictory answer is malformed, never a session. + internal static SessionSnapshot ToSnapshot(BrowserBootstrapResponse body) + { + ArgumentNullException.ThrowIfNull(body); + var csrf = body.CsrfToken; + if (csrf is null || !CsrfToken().IsMatch(csrf)) + throw new ProbeFailureException(FailureKind.Malformed); + if (!body.Authenticated) + { + // An anonymous answer that still carries a session or profile is contradictory, not anonymous. + if (body.Session is not null || body.Profile is not null) + throw new ProbeFailureException(FailureKind.Malformed); + return new AnonymousSession(csrf); + } + var session = body.Session ?? throw new ProbeFailureException(FailureKind.Malformed); + if (!Exact.TryUnsigned(session.RecoveryGeneration, out var generation)) + throw new ProbeFailureException(FailureKind.Malformed); + var profile = body.Profile; + return new AuthenticatedSession( + csrf, + profile?.DisplayName, + profile?.Locale, + profile?.Timezone, + session.ExpiresAt, + session.IdleExpiresAt, + session.WorkspaceIds.Count(), + generation.Text); + } + + private async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + return await _invoker.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) when (exception is not ProbeFailureException) + { + throw BoundedBody.FromTransport(exception, cancellationToken); + } + } + + private static async Task JsonBodyAsync(HttpResponseMessage response, int limit, CancellationToken cancellationToken) + { + // A status of zero is an opaque redirect the page refuses to follow: the answer is unavailable, as a refused fetch is. + if ((int)response.StatusCode == 0) + throw new ProbeFailureException(FailureKind.Unavailable); + if (response.StatusCode != System.Net.HttpStatusCode.OK) + throw new ProbeFailureException(FailureMapping.FromStatus((int)response.StatusCode)); + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (!string.Equals(mediaType, "application/json", StringComparison.OrdinalIgnoreCase)) + throw new ProbeFailureException(FailureKind.Malformed); + return await BoundedBody.ReadAsync(response.Content, limit, cancellationToken).ConfigureAwait(false); + } + + private static BrowserSessionRoute Route(string id) => + BrowserSessionRoutes.All.FirstOrDefault(candidate => candidate.Id == id) + ?? throw new InvalidOperationException($"The generated Contracts catalogue has no browser session route {id}."); +} diff --git a/src/ArcForges.Web.App/Probe/WireJson.cs b/src/ArcForges.Web.App/Probe/WireJson.cs new file mode 100644 index 0000000..7997666 --- /dev/null +++ b/src/ArcForges.Web.App/Probe/WireJson.cs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace ArcForges.Web.App.Probe; + +/// +/// Decodes a same-origin session or hello document strictly: the generated Contracts JSON context is the only record +/// source, and a document that repeats a property name at any depth is refused, because the generated reader keeps the +/// last value of a repeated name. Every decoding failure is a malformed answer. +/// +public static class WireJson +{ + /// Decodes one document with the generated type information, or fails as malformed. + public static T Decode(ReadOnlySpan bytes, JsonTypeInfo typeInfo) + where T : class + { + try + { + RejectDuplicateNames(bytes); + return JsonSerializer.Deserialize(bytes, typeInfo) + ?? throw new JsonException("The document is null."); + } + catch (JsonException exception) + { + throw new ProbeFailureException(FailureKind.Malformed, exception); + } + } + + private static void RejectDuplicateNames(ReadOnlySpan bytes) + { + var reader = new Utf8JsonReader(bytes); + // Each open container: the names of an object, or null for an array. + var scopes = new Stack?>(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + scopes.Push(new HashSet(StringComparer.Ordinal)); + break; + case JsonTokenType.StartArray: + scopes.Push(null); + break; + case JsonTokenType.EndObject: + case JsonTokenType.EndArray: + scopes.Pop(); + break; + case JsonTokenType.PropertyName: + var name = reader.GetString() ?? throw new JsonException("A property name is null."); + if (!scopes.Peek()!.Add(name)) + throw new JsonException("A property name is repeated."); + break; + } + } + } +} diff --git a/src/ArcForges.Web.App/Program.cs b/src/ArcForges.Web.App/Program.cs index 1ac0a18..858b1ae 100644 --- a/src/ArcForges.Web.App/Program.cs +++ b/src/ArcForges.Web.App/Program.cs @@ -1,5 +1,18 @@ // SPDX-License-Identifier: AGPL-3.0-only +using ArcForges.Web.App.Probe; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +// The same origin as the page: the cookie session and the greeting go to this origin only. +var origin = new ProbeOrigin(new Uri(builder.HostEnvironment.BaseAddress)); +builder.Services.AddSingleton(origin); +// Redirects are not followed by the page: a redirect answer is a refusal, not a second request to another place. +builder.Services.AddSingleton(_ => new HttpClientHandler { AllowAutoRedirect = false }); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + await builder.Build().RunAsync(); diff --git a/src/ArcForges.Web.App/_Imports.razor b/src/ArcForges.Web.App/_Imports.razor new file mode 100644 index 0000000..bec99b9 --- /dev/null +++ b/src/ArcForges.Web.App/_Imports.razor @@ -0,0 +1,8 @@ +@* SPDX-License-Identifier: AGPL-3.0-only *@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using ArcForges.Web.App.Probe +@using ArcForges.Web.Ui diff --git a/src/ArcForges.Web.App/wwwroot/app.css b/src/ArcForges.Web.App/wwwroot/app.css new file mode 100644 index 0000000..92ff8a0 --- /dev/null +++ b/src/ArcForges.Web.App/wwwroot/app.css @@ -0,0 +1,450 @@ +/* SPDX-License-Identifier: AGPL-3.0-only */ +@import "tailwindcss" source(none); +@source "../../../apps/site/app"; +@source "./"; + +@theme { + --color-paper: #f4f3ed; + --color-ink: #1e2824; + --color-forest: #235844; + --font-sans: "Segoe UI", "Helvetica Neue", Arial, sans-serif; +} + +:root { + font-family: var(--font-sans); + color: #1e2824; + background: #f4f3ed; + color-scheme: light; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-width: 320px; +} +button, +input { + font: inherit; +} +a { + color: inherit; + text-decoration: none; +} +a:hover { + text-decoration: underline; + text-underline-offset: 5px; +} +button, +a, +input { + -webkit-tap-highlight-color: transparent; +} +:focus-visible { + outline: 3px solid #235844; + outline-offset: 5px; +} +.site-shell { + max-width: 1440px; + margin: 0 auto; + padding: 0 64px; +} +.site-header { + height: 112px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + border-bottom: 1px solid #d5d9cf; +} +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 23px; + font-weight: 650; + letter-spacing: -0.8px; +} +.brand:hover { + text-decoration: none; +} +.site-header nav { + display: flex; + align-items: center; + gap: 32px; + font-size: 14px; + font-weight: 550; +} +.source-link { + color: #4c5d53; +} +.skip-link { + position: absolute; + top: -100px; + left: 24px; + padding: 12px 16px; + background: white; + z-index: 10; +} +.skip-link:focus { + top: 12px; +} +.hero { + display: grid; + grid-template-columns: 1.15fr 0.85fr; + gap: 40px; + align-items: center; + min-height: 600px; + padding: 76px 0; +} +.eyebrow { + display: flex; + align-items: center; + gap: 9px; + color: #496052; + font-family: ui-monospace, monospace; + font-size: 12px; + letter-spacing: 1.6px; + text-transform: uppercase; + margin: 0 0 32px; +} +.status-dot { + display: inline-block; + height: 7px; + width: 7px; + background: #36734a; + border-radius: 50%; +} +h1 { + font-size: clamp(64px, 7.8vw, 112px); + font-weight: 450; + line-height: 1.02; + letter-spacing: -7px; + margin: 0; +} +h1 em { + font-family: Georgia, "Times New Roman", serif; + font-weight: 400; + color: #235844; +} +.hero-copy { + max-width: 370px; + font-size: 18px; + line-height: 1.7; + color: #5a675e; + margin: 28px 0 32px; +} +.button { + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 40px; + min-height: 52px; + padding: 14px 22px; + border: 1px solid #235844; + border-radius: 6px; + background: #235844; + color: #fff; + font-size: 15px; + font-weight: 550; + cursor: pointer; + transition: background 140ms ease; +} +.button:hover { + background: #143d2e; + text-decoration: none; +} +.button:focus-visible { + outline-offset: 4px; +} +.hello-art { + position: relative; + display: grid; + place-items: center; + aspect-ratio: 1; + width: 100%; + max-width: 430px; + justify-self: end; + background: #e8ece2; + border: 1px solid #d5ddcc; + border-radius: 50%; + overflow: hidden; +} +.hello-art::before, +.hello-art::after { + content: ""; + position: absolute; + width: 72%; + height: 72%; + border: 1px solid #bbcdb7; + border-radius: 50%; + transform: rotate(-22deg) scaleX(0.5); +} +.hello-art::after { + transform: rotate(22deg) scaleX(0.5); +} +.hello-art span { + z-index: 1; + font-family: Georgia, "Times New Roman", serif; + font-size: clamp(76px, 10vw, 136px); + font-style: italic; + color: #235844; + letter-spacing: -8px; + transform: rotate(-8deg); +} +.intro-line { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + padding: 32px 0 44px; + border-top: 1px solid #d5d9cf; +} +.intro-line h2 { + font-size: 21px; + font-weight: 500; + margin: 0; +} +.intro-line p { + color: #5a675e; + line-height: 1.7; + margin: 0; + max-width: 510px; +} +.site-footer { + display: flex; + flex-wrap: wrap; + gap: 12px 28px; + padding: 28px 0 32px; + border-top: 1px solid #d5d9cf; + font-size: 12px; + color: #58665d; +} +.site-footer span { + margin-right: auto; +} +.example { + max-width: 720px; + margin: 0 auto; + padding: 72px 0 80px; + min-height: 640px; +} +.back-link { + display: inline-block; + margin-bottom: 32px; + color: #496052; + font-size: 14px; +} +.example h1 { + font-size: clamp(42px, 5vw, 64px); + letter-spacing: -3px; +} +.example .hero-copy { + max-width: 560px; + font-size: 17px; + margin: 22px 0 32px; +} +.hello-form { + margin-top: 36px; +} +.hello-form label { + display: block; + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} +.input-row { + display: flex; + gap: 12px; + align-items: stretch; +} +.input-row input { + flex: 1; + min-width: 0; + background: #fffdf7; + border: 1px solid #a3b1a4; + border-radius: 6px; + padding: 14px 16px; + color: #1e2824; + font-size: 17px; +} +.field-hint { + font-size: 13px; + color: #58665d; + margin: 12px 0 0; +} +.field-error { + color: #923b32; + font-size: 14px; + margin: 12px 0 0; +} +.greeting { + margin-top: 32px; + border-left: 3px solid #639167; + padding: 20px 24px; + background: #e8ece2; + border-radius: 0 6px 6px 0; + overflow-wrap: anywhere; +} +.greeting-label { + display: block; + color: #496052; + font-size: 11px; + letter-spacing: 1.5px; + text-transform: uppercase; + font-family: ui-monospace, monospace; + margin-bottom: 8px; +} +.greeting p { + font-size: 28px; + font-family: Georgia, "Times New Roman", serif; + margin: 0; + line-height: 1.4; +} +.no-script { + padding: 12px 0; + font-size: 14px; + color: #5a675e; +} +@media (max-width: 800px) { + .site-shell { + padding: 0 28px; + } + .site-header { + height: 88px; + } + .hero { + grid-template-columns: 1fr; + padding: 56px 0 40px; + min-height: auto; + gap: 40px; + } + h1 { + letter-spacing: -4px; + } + .hello-art { + width: min(100%, 330px); + justify-self: center; + } + .intro-line { + grid-template-columns: 1fr; + gap: 16px; + } + .example { + padding-top: 40px; + min-height: 610px; + } +} +@media (max-width: 480px) { + .site-shell { + padding: 0 22px; + } + .site-header nav { + gap: 16px; + font-size: 13px; + } + .brand { + font-size: 20px; + gap: 5px; + } + .source-link { + display: none; + } + .eyebrow { + font-size: 10px; + margin-bottom: 24px; + } + .input-row { + flex-direction: column; + } + .input-row .button { + width: 100%; + } + .site-footer span { + flex-basis: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition: none !important; + scroll-behavior: auto !important; + } +} +/* SPDX-License-Identifier: AGPL-3.0-only */ +.profile-note { + color: #4c5d53; + font-size: 14px; + font-weight: 550; +} +.panel { + margin-top: 32px; + max-width: 640px; +} +.facts { + display: grid; + grid-template-columns: max-content 1fr; + gap: 8px 24px; + margin: 0; +} +.facts dt { + color: #4c5d53; + font-size: 14px; +} +.facts dd { + margin: 0; + overflow-wrap: anywhere; +} +.exact { + font-family: ui-monospace, monospace; +} +.field { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + margin: 24px 0; +} +.field input { + min-width: 240px; + padding: 12px 14px; + border: 1px solid #b9c1b4; + border-radius: 6px; + background: white; +} +.transcript { + list-style: none; + margin: 24px 0 0; + padding: 0; + display: grid; + gap: 12px; +} +.transcript li { + padding: 12px 14px; + border: 1px solid #d5d9cf; + border-radius: 6px; +} +.transcript .kind { + display: block; + color: #4c5d53; + font-size: 12px; + letter-spacing: 1px; + text-transform: uppercase; +} + +#blazor-error-ui { + background: #fff3cd; + bottom: 0; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem; + position: fixed; + width: 100%; + z-index: 1000; +} +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} diff --git a/src/ArcForges.Web.App/wwwroot/favicon.svg b/src/ArcForges.Web.App/wwwroot/favicon.svg new file mode 100644 index 0000000..e2325c1 --- /dev/null +++ b/src/ArcForges.Web.App/wwwroot/favicon.svg @@ -0,0 +1 @@ + diff --git a/src/ArcForges.Web.App/wwwroot/index.html b/src/ArcForges.Web.App/wwwroot/index.html new file mode 100644 index 0000000..4ef797d --- /dev/null +++ b/src/ArcForges.Web.App/wwwroot/index.html @@ -0,0 +1,22 @@ + + + + + + + + + ArcForges + + + + +
Loading…
+
+ An unhandled error has occurred. + Reload + × +
+ + + diff --git a/src/ArcForges.Web.App/wwwroot/robots.txt b/src/ArcForges.Web.App/wwwroot/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/src/ArcForges.Web.App/wwwroot/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/src/ArcForges.Web.Operations/App.razor b/src/ArcForges.Web.Operations/App.razor new file mode 100644 index 0000000..8a81075 --- /dev/null +++ b/src/ArcForges.Web.Operations/App.razor @@ -0,0 +1,13 @@ + + + + + + +
+

Page not found.

+

The operations console has a single shell page in this build.

+
+
+
+
diff --git a/src/ArcForges.Web.Operations/Layout/OperationsLayout.razor b/src/ArcForges.Web.Operations/Layout/OperationsLayout.razor new file mode 100644 index 0000000..8ef4034 --- /dev/null +++ b/src/ArcForges.Web.Operations/Layout/OperationsLayout.razor @@ -0,0 +1,15 @@ +@inherits LayoutComponentBase + +
+ + +
+ @Body +
+
+ Operations shell. No operator feature is in this build. +
+
diff --git a/src/ArcForges.Web.Operations/Pages/Shell.razor b/src/ArcForges.Web.Operations/Pages/Shell.razor new file mode 100644 index 0000000..655298b --- /dev/null +++ b/src/ArcForges.Web.Operations/Pages/Shell.razor @@ -0,0 +1,11 @@ +@page "/" + +Operations — ArcForges + +
+

Operations profile

+

Operations console shell.

+

+ This standalone profile runs on its own origin with its own identity. The operator features are built by later tasks. +

+
diff --git a/src/ArcForges.Web.Operations/ProfileBoundary.cs b/src/ArcForges.Web.Operations/ProfileBoundary.cs new file mode 100644 index 0000000..55fbff6 --- /dev/null +++ b/src/ArcForges.Web.Operations/ProfileBoundary.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Operations; + +/// +/// The separate-origin boundary of the Operations console (WEB.40 skeleton; P2-021 item 2). The console keeps its own origin +/// and its own identity: no cookie, session or CSRF value is shared with the Account and Chat profiles, which are served from +/// another origin. The boundary is a value the tests assert, so a later change has to change the record too. +/// +public static class ProfileBoundary +{ + /// The origins whose cookies or session values this profile may share. None. + public static IReadOnlyList SharedCookieOrigins { get; } = []; + + /// The Content-Security-Policy scope: the profile's own origin only. + public const string PolicyScope = "own-origin"; +} diff --git a/src/ArcForges.Web.Operations/Program.cs b/src/ArcForges.Web.Operations/Program.cs index 1ac0a18..d0855ee 100644 --- a/src/ArcForges.Web.Operations/Program.cs +++ b/src/ArcForges.Web.Operations/Program.cs @@ -1,5 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); await builder.Build().RunAsync(); diff --git a/src/ArcForges.Web.Operations/_Imports.razor b/src/ArcForges.Web.Operations/_Imports.razor new file mode 100644 index 0000000..5e6dedf --- /dev/null +++ b/src/ArcForges.Web.Operations/_Imports.razor @@ -0,0 +1,6 @@ +@* SPDX-License-Identifier: AGPL-3.0-only *@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using ArcForges.Web.Ui diff --git a/src/ArcForges.Web.Operations/wwwroot/app.css b/src/ArcForges.Web.Operations/wwwroot/app.css new file mode 100644 index 0000000..92ff8a0 --- /dev/null +++ b/src/ArcForges.Web.Operations/wwwroot/app.css @@ -0,0 +1,450 @@ +/* SPDX-License-Identifier: AGPL-3.0-only */ +@import "tailwindcss" source(none); +@source "../../../apps/site/app"; +@source "./"; + +@theme { + --color-paper: #f4f3ed; + --color-ink: #1e2824; + --color-forest: #235844; + --font-sans: "Segoe UI", "Helvetica Neue", Arial, sans-serif; +} + +:root { + font-family: var(--font-sans); + color: #1e2824; + background: #f4f3ed; + color-scheme: light; +} +* { + box-sizing: border-box; +} +body { + margin: 0; + min-width: 320px; +} +button, +input { + font: inherit; +} +a { + color: inherit; + text-decoration: none; +} +a:hover { + text-decoration: underline; + text-underline-offset: 5px; +} +button, +a, +input { + -webkit-tap-highlight-color: transparent; +} +:focus-visible { + outline: 3px solid #235844; + outline-offset: 5px; +} +.site-shell { + max-width: 1440px; + margin: 0 auto; + padding: 0 64px; +} +.site-header { + height: 112px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + border-bottom: 1px solid #d5d9cf; +} +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 23px; + font-weight: 650; + letter-spacing: -0.8px; +} +.brand:hover { + text-decoration: none; +} +.site-header nav { + display: flex; + align-items: center; + gap: 32px; + font-size: 14px; + font-weight: 550; +} +.source-link { + color: #4c5d53; +} +.skip-link { + position: absolute; + top: -100px; + left: 24px; + padding: 12px 16px; + background: white; + z-index: 10; +} +.skip-link:focus { + top: 12px; +} +.hero { + display: grid; + grid-template-columns: 1.15fr 0.85fr; + gap: 40px; + align-items: center; + min-height: 600px; + padding: 76px 0; +} +.eyebrow { + display: flex; + align-items: center; + gap: 9px; + color: #496052; + font-family: ui-monospace, monospace; + font-size: 12px; + letter-spacing: 1.6px; + text-transform: uppercase; + margin: 0 0 32px; +} +.status-dot { + display: inline-block; + height: 7px; + width: 7px; + background: #36734a; + border-radius: 50%; +} +h1 { + font-size: clamp(64px, 7.8vw, 112px); + font-weight: 450; + line-height: 1.02; + letter-spacing: -7px; + margin: 0; +} +h1 em { + font-family: Georgia, "Times New Roman", serif; + font-weight: 400; + color: #235844; +} +.hero-copy { + max-width: 370px; + font-size: 18px; + line-height: 1.7; + color: #5a675e; + margin: 28px 0 32px; +} +.button { + display: inline-flex; + align-items: center; + justify-content: space-between; + gap: 40px; + min-height: 52px; + padding: 14px 22px; + border: 1px solid #235844; + border-radius: 6px; + background: #235844; + color: #fff; + font-size: 15px; + font-weight: 550; + cursor: pointer; + transition: background 140ms ease; +} +.button:hover { + background: #143d2e; + text-decoration: none; +} +.button:focus-visible { + outline-offset: 4px; +} +.hello-art { + position: relative; + display: grid; + place-items: center; + aspect-ratio: 1; + width: 100%; + max-width: 430px; + justify-self: end; + background: #e8ece2; + border: 1px solid #d5ddcc; + border-radius: 50%; + overflow: hidden; +} +.hello-art::before, +.hello-art::after { + content: ""; + position: absolute; + width: 72%; + height: 72%; + border: 1px solid #bbcdb7; + border-radius: 50%; + transform: rotate(-22deg) scaleX(0.5); +} +.hello-art::after { + transform: rotate(22deg) scaleX(0.5); +} +.hello-art span { + z-index: 1; + font-family: Georgia, "Times New Roman", serif; + font-size: clamp(76px, 10vw, 136px); + font-style: italic; + color: #235844; + letter-spacing: -8px; + transform: rotate(-8deg); +} +.intro-line { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + padding: 32px 0 44px; + border-top: 1px solid #d5d9cf; +} +.intro-line h2 { + font-size: 21px; + font-weight: 500; + margin: 0; +} +.intro-line p { + color: #5a675e; + line-height: 1.7; + margin: 0; + max-width: 510px; +} +.site-footer { + display: flex; + flex-wrap: wrap; + gap: 12px 28px; + padding: 28px 0 32px; + border-top: 1px solid #d5d9cf; + font-size: 12px; + color: #58665d; +} +.site-footer span { + margin-right: auto; +} +.example { + max-width: 720px; + margin: 0 auto; + padding: 72px 0 80px; + min-height: 640px; +} +.back-link { + display: inline-block; + margin-bottom: 32px; + color: #496052; + font-size: 14px; +} +.example h1 { + font-size: clamp(42px, 5vw, 64px); + letter-spacing: -3px; +} +.example .hero-copy { + max-width: 560px; + font-size: 17px; + margin: 22px 0 32px; +} +.hello-form { + margin-top: 36px; +} +.hello-form label { + display: block; + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; +} +.input-row { + display: flex; + gap: 12px; + align-items: stretch; +} +.input-row input { + flex: 1; + min-width: 0; + background: #fffdf7; + border: 1px solid #a3b1a4; + border-radius: 6px; + padding: 14px 16px; + color: #1e2824; + font-size: 17px; +} +.field-hint { + font-size: 13px; + color: #58665d; + margin: 12px 0 0; +} +.field-error { + color: #923b32; + font-size: 14px; + margin: 12px 0 0; +} +.greeting { + margin-top: 32px; + border-left: 3px solid #639167; + padding: 20px 24px; + background: #e8ece2; + border-radius: 0 6px 6px 0; + overflow-wrap: anywhere; +} +.greeting-label { + display: block; + color: #496052; + font-size: 11px; + letter-spacing: 1.5px; + text-transform: uppercase; + font-family: ui-monospace, monospace; + margin-bottom: 8px; +} +.greeting p { + font-size: 28px; + font-family: Georgia, "Times New Roman", serif; + margin: 0; + line-height: 1.4; +} +.no-script { + padding: 12px 0; + font-size: 14px; + color: #5a675e; +} +@media (max-width: 800px) { + .site-shell { + padding: 0 28px; + } + .site-header { + height: 88px; + } + .hero { + grid-template-columns: 1fr; + padding: 56px 0 40px; + min-height: auto; + gap: 40px; + } + h1 { + letter-spacing: -4px; + } + .hello-art { + width: min(100%, 330px); + justify-self: center; + } + .intro-line { + grid-template-columns: 1fr; + gap: 16px; + } + .example { + padding-top: 40px; + min-height: 610px; + } +} +@media (max-width: 480px) { + .site-shell { + padding: 0 22px; + } + .site-header nav { + gap: 16px; + font-size: 13px; + } + .brand { + font-size: 20px; + gap: 5px; + } + .source-link { + display: none; + } + .eyebrow { + font-size: 10px; + margin-bottom: 24px; + } + .input-row { + flex-direction: column; + } + .input-row .button { + width: 100%; + } + .site-footer span { + flex-basis: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + transition: none !important; + scroll-behavior: auto !important; + } +} +/* SPDX-License-Identifier: AGPL-3.0-only */ +.profile-note { + color: #4c5d53; + font-size: 14px; + font-weight: 550; +} +.panel { + margin-top: 32px; + max-width: 640px; +} +.facts { + display: grid; + grid-template-columns: max-content 1fr; + gap: 8px 24px; + margin: 0; +} +.facts dt { + color: #4c5d53; + font-size: 14px; +} +.facts dd { + margin: 0; + overflow-wrap: anywhere; +} +.exact { + font-family: ui-monospace, monospace; +} +.field { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + margin: 24px 0; +} +.field input { + min-width: 240px; + padding: 12px 14px; + border: 1px solid #b9c1b4; + border-radius: 6px; + background: white; +} +.transcript { + list-style: none; + margin: 24px 0 0; + padding: 0; + display: grid; + gap: 12px; +} +.transcript li { + padding: 12px 14px; + border: 1px solid #d5d9cf; + border-radius: 6px; +} +.transcript .kind { + display: block; + color: #4c5d53; + font-size: 12px; + letter-spacing: 1px; + text-transform: uppercase; +} + +#blazor-error-ui { + background: #fff3cd; + bottom: 0; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem; + position: fixed; + width: 100%; + z-index: 1000; +} +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} diff --git a/src/ArcForges.Web.Operations/wwwroot/favicon.svg b/src/ArcForges.Web.Operations/wwwroot/favicon.svg new file mode 100644 index 0000000..e2325c1 --- /dev/null +++ b/src/ArcForges.Web.Operations/wwwroot/favicon.svg @@ -0,0 +1 @@ + diff --git a/src/ArcForges.Web.Operations/wwwroot/index.html b/src/ArcForges.Web.Operations/wwwroot/index.html new file mode 100644 index 0000000..e667080 --- /dev/null +++ b/src/ArcForges.Web.Operations/wwwroot/index.html @@ -0,0 +1,22 @@ + + + + + + + + + ArcForges Operations + + + + +
Loading…
+
+ An unhandled error has occurred. + Reload + × +
+ + + diff --git a/src/ArcForges.Web.Operations/wwwroot/robots.txt b/src/ArcForges.Web.Operations/wwwroot/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/src/ArcForges.Web.Operations/wwwroot/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/src/ArcForges.Web.Ui/WasmContentSecurityPolicy.cs b/src/ArcForges.Web.Ui/WasmContentSecurityPolicy.cs new file mode 100644 index 0000000..590f0c8 --- /dev/null +++ b/src/ArcForges.Web.Ui/WasmContentSecurityPolicy.cs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Security.Cryptography; +using System.Text; + +namespace ArcForges.Web.Ui; + +/// +/// The Content-Security-Policy of the Blazor WebAssembly standalone profiles (Account, Chat and Operations; P2-021 item 2). +/// The script sources are exactly 'self' and 'wasm-unsafe-eval' plus the SHA-256 hash of every inline script +/// body of the host page. style-src stays 'self', so components that need inline styles are not used. The +/// policy never carries unsafe-inline or unsafe-eval, and an absolute external script is refused. +/// +public static class WasmContentSecurityPolicy +{ + /// The Cloudflare header line budget that the policy must fit within (the React _headers rule). + public const int HeaderLineBudget = 1800; + + /// The script-src token that lets the WebAssembly runtime compile; the only eval-like token allowed. + public const string WasmUnsafeEval = "'wasm-unsafe-eval'"; + + private const string ScriptTag = "Returns the policy for the given host pages. + /// A page has an absolute external script, an unterminated element, or the policy exceeds the line budget. + public static string FromHostPages(IEnumerable pages) + { + ArgumentNullException.ThrowIfNull(pages); + var hashes = new SortedSet(StringComparer.Ordinal); + foreach (var page in pages) + foreach (var body in InlineScriptBodies(page)) + if (body.Length > 0) + hashes.Add("'sha256-" + Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(body))) + "'"); + var scriptSources = string.Join(' ', new[] { "'self'", WasmUnsafeEval }.Concat(hashes)); + var policy = "default-src 'self'; script-src " + scriptSources + + "; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'"; + if (policy.Length >= HeaderLineBudget) + throw new InvalidOperationException("The Content-Security-Policy exceeds the Workers header line budget."); + return policy; + } + + /// Splits a policy into its directives, each mapped to its ordered source tokens. + public static IReadOnlyDictionary> ParseDirectives(string policy) + { + ArgumentNullException.ThrowIfNull(policy); + var directives = new Dictionary>(StringComparer.Ordinal); + foreach (var part in policy.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) + { + var tokens = part.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (!directives.TryAdd(tokens[0], tokens[1..])) + throw new InvalidOperationException("Duplicate directive " + tokens[0]); + } + return directives; + } + + private static IEnumerable InlineScriptBodies(string html) + { + var start = 0; + while (true) + { + var open = html.IndexOf(ScriptTag, start, StringComparison.OrdinalIgnoreCase); + if (open < 0) + yield break; + var tagEnd = html.IndexOf('>', open); + if (tagEnd < 0) + throw new InvalidOperationException("A script element is not terminated."); + // Fail closed: a source attribute of any form is an external script the policy does not admit. + var attributes = html[(open + ScriptTag.Length)..tagEnd]; + if (attributes.Contains("src", StringComparison.OrdinalIgnoreCase)) + ValidateSameOriginSource(attributes.ToString()); + var close = html.IndexOf(ScriptClose, tagEnd + 1, StringComparison.OrdinalIgnoreCase); + if (close < 0) + throw new InvalidOperationException("A script element is not closed."); + if (!attributes.Contains("src", StringComparison.OrdinalIgnoreCase)) + yield return html[(tagEnd + 1)..close]; + start = close + ScriptClose.Length; + } + } + + private static void ValidateSameOriginSource(string attributes) + { + var index = attributes.IndexOf("src", StringComparison.OrdinalIgnoreCase); + var equals = index < 0 ? -1 : attributes.IndexOf('=', index); + if (equals < 0) + throw new InvalidOperationException("A script source without a quoted value is refused."); + var rest = attributes[(equals + 1)..].TrimStart(); + if (rest.Length == 0 || (rest[0] != '"' && rest[0] != '\'')) + throw new InvalidOperationException("A script source without a quoted value is refused."); + var closing = rest.IndexOf(rest[0], 1); + if (closing < 0) + throw new InvalidOperationException("An unterminated script source is refused."); + var value = rest[1..closing]; + if (value.StartsWith("//", StringComparison.Ordinal) || value.Contains("://", StringComparison.Ordinal)) + throw new InvalidOperationException("An external script is refused by the WebAssembly profile policy."); + } +} From be0d91687bf33000f789f30ef77dacde11abd5d3 Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 20:02:30 +0100 Subject: [PATCH 05/38] [WEB.40] Ports and negative tests for the Blazor profiles, and the local browser opt-in Step 3, unit U1, third commit: the xUnit and bUnit successors of the Account and Chat probe cases (tests/unit/app-*.test.tsx and app-hello.test.ts), the profile CSP token-set assertions, and the Operations skeleton tests. - ArcForges.Web.App.Tests: Account, Chat, hello, session, exact unsigned and profile policy cases with a test-only same-origin double (TestDoubles). Fixes in this unit, without weakening any assertion: - LeavingTheChatPageCancelsTheMessageThatIsStillPending waits for the request to start through a completion source and disposes the rendered components with DisposeComponentsAsync, the bUnit API that disposes components (the context dispose only releases services). - The cancellation token of the wait uses the xUnit test context token. - ArcForges.Web.Operations.Tests: skeleton only (the profile, its origin and the exact CSP token set). - ArcForges.Web.Ui.Tests: WasmContentSecurityPolicyTests with a passing and a failing example for each token rule. - tests/browser/ArcForges.Web.Browser.Tests: Microsoft.Playwright for .NET, local opt-in test-only tooling (LocalOptIn gate; not run in CI). The local browser test is skipped unless the opt-in is set, as recorded by the test. Results on this worktree (dotnet 10.0.401, through the build slot, after the admission r3 restore): Ui 14 passed; App 49 passed; Operations 5 passed; Browser 6 passed and 1 local opt-in skipped; Policy 9 passed; Site 65 passed. Co-Authored-By: Claude Haiku 5.5 --- .../AccountPageTests.cs | 236 +++++++++++++ .../ArcForges.Web.App.Tests/ChatPageTests.cs | 162 +++++++++ .../ExactUnsignedTests.cs | 58 ++++ .../Fixtures/ProbeFixtures.cs | 85 +++++ .../HelloProbeTests.cs | 222 ++++++++++++ .../ArcForges.Web.App.Tests/RepositoryRoot.cs | 20 ++ .../SessionProbeTests.cs | 319 ++++++++++++++++++ .../TestDoubles/ScriptedServer.cs | 162 +++++++++ .../WasmProfilePolicyTests.cs | 97 ++++++ .../OperationsProfileTests.cs | 50 +++ .../RepositoryRoot.cs | 20 ++ .../WasmContentSecurityPolicyTests.cs | 70 ++++ .../ArcForges.Web.Browser.Tests.csproj | 17 + .../LocalBrowserTests.cs | 41 +++ .../ArcForges.Web.Browser.Tests/LocalOptIn.cs | 46 +++ .../LocalOptInTests.cs | 48 +++ .../packages.lock.json | 147 ++++++++ 17 files changed, 1800 insertions(+) create mode 100644 tests/ArcForges.Web.App.Tests/AccountPageTests.cs create mode 100644 tests/ArcForges.Web.App.Tests/ChatPageTests.cs create mode 100644 tests/ArcForges.Web.App.Tests/ExactUnsignedTests.cs create mode 100644 tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs create mode 100644 tests/ArcForges.Web.App.Tests/HelloProbeTests.cs create mode 100644 tests/ArcForges.Web.App.Tests/RepositoryRoot.cs create mode 100644 tests/ArcForges.Web.App.Tests/SessionProbeTests.cs create mode 100644 tests/ArcForges.Web.App.Tests/TestDoubles/ScriptedServer.cs create mode 100644 tests/ArcForges.Web.App.Tests/WasmProfilePolicyTests.cs create mode 100644 tests/ArcForges.Web.Operations.Tests/OperationsProfileTests.cs create mode 100644 tests/ArcForges.Web.Operations.Tests/RepositoryRoot.cs create mode 100644 tests/ArcForges.Web.Ui.Tests/WasmContentSecurityPolicyTests.cs create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/LocalBrowserTests.cs create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/LocalOptIn.cs create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/LocalOptInTests.cs create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json diff --git a/tests/ArcForges.Web.App.Tests/AccountPageTests.cs b/tests/ArcForges.Web.App.Tests/AccountPageTests.cs new file mode 100644 index 0000000..0ad2f50 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/AccountPageTests.cs @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of the account cases of tests/unit/app-routes.test.tsx: the Account page rendered with bUnit over the same-origin +// probe. The server is a test-only double; the wire documents are fixtures. +using System.Net; +using System.Text; +using AngleSharp.Dom; +using Bunit; +using ArcForges.Web.App.Pages; +using Microsoft.Extensions.DependencyInjection; +using ArcForges.Web.App.Probe; +using ArcForges.Web.App.Tests.Fixtures; +using ArcForges.Web.App.Tests.TestDoubles; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class AccountPageTests +{ + internal const string Ended = "You are signed out. Reload to check again."; + + /// Registers the probes over one scripted server. The handler is the only external input. + internal static void RegisterProbes(BunitContext context, HttpMessageHandler server) + { + context.Services.AddSingleton(new ProbeOrigin(ProbeFixtures.Origin)); + context.Services.AddSingleton(server); + context.Services.AddSingleton(); + context.Services.AddSingleton(); + } + + internal static IElement Button(IRenderedComponent cut, string text) => + cut.FindAll("button").Single(button => button.TextContent.Trim() == text); + + internal static bool HasButton(IRenderedComponent cut, string text) => + cut.FindAll("button").Any(button => button.TextContent.Trim() == text); + + [Fact] + public void AccountReadsTheSessionOnceOnOpenAndPresentsItAsTextWithTheExactGeneration() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Contains("Ada Lovelace", cut.Markup, StringComparison.Ordinal)); + + Assert.Equal(1, server.Count); + Assert.Equal($"{ProbeFixtures.Origin.Scheme}://{ProbeFixtures.Origin.Authority}/session/v1/bootstrap", Assert.Single(server.Requests).Url.ToString()); + Assert.Equal("18446744073709551615", cut.Find("dd.exact").TextContent); + Assert.Contains("en-GB", cut.Markup, StringComparison.Ordinal); + Assert.Contains("Europe/London", cut.Markup, StringComparison.Ordinal); + Assert.Contains("2026-10-03T08:00:00.000000Z", cut.Markup, StringComparison.Ordinal); + var text = cut.Markup; + foreach (var secret in new[] + { + ProbeFixtures.AuthenticatedCsrf, + "6d1d4c2a-62a0-4b86-9d3f-0c6a3d0b5c11", + "0f0e6a30-5d1c-4c7e-8b53-5b6b7f9a4a10", + "8f2a1d77-1c1b-4b0a-9a55-2f1d0e5c7b21", + }) + Assert.DoesNotContain(secret, text, StringComparison.Ordinal); + } + + [Fact] + public async Task WhileTheReadIsInFlightTheCheckingStateIsShownAndThenTheAnswerReplacesIt() + { + var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new ScriptedServer((_, _) => gate.Task); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + await WaitUntilAsync(() => server.Count == 1); + Assert.Contains("Checking your session…", cut.Markup, StringComparison.Ordinal); + + gate.SetResult(Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson))); + cut.WaitForAssertion(() => Assert.Contains("You are not signed in.", cut.Markup, StringComparison.Ordinal)); + Assert.DoesNotContain("Checking your session…", cut.Markup, StringComparison.Ordinal); + } + + [Fact] + public void AccountRendersAnAnonymousVisitorWithNoSignOutControl() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Contains("You are not signed in.", cut.Markup, StringComparison.Ordinal)); + Assert.Empty(cut.FindAll("button")); + } + + [Fact] + public void AccountRendersDisplayTextFromTheServerAsTextNeverMarkup() + { + var node = ProbeFixtures.AuthenticatedNode(); + node["profile"]!["displayName"] = ""; + var server = ScriptedServer.Always(() => Responses.Json(ProbeFixtures.Bytes(node))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Contains("<img src=x onerror=alert(1)>", cut.Markup, StringComparison.Ordinal)); + Assert.Empty(cut.FindAll("img")); + } + + [Fact] + public void SignOutPostsTheCsrfTokenFromThisPagesBootstrapAndThenShowsTheEndedState() + { + var answers = new Queue>( + [ + () => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson)), + () => Responses.Json(ProbeFixtures.ReceiptBody("happened")), + ]); + var server = ScriptedServer.Always(() => answers.Dequeue()()); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Sign out"))); + Button(cut, "Sign out").Click(); + cut.WaitForAssertion(() => Assert.Contains(Ended, cut.Markup, StringComparison.Ordinal)); + + Assert.Equal(2, server.Count); + var logout = server.Requests[1]; + Assert.Equal(HttpMethod.Post, logout.Method); + Assert.Equal($"{ProbeFixtures.Origin.Scheme}://{ProbeFixtures.Origin.Authority}/session/v1/logout", logout.Url.ToString()); + Assert.Equal(ProbeFixtures.AuthenticatedCsrf, logout.CsrfHeader); + Assert.DoesNotContain("Ada Lovelace", cut.Markup, StringComparison.Ordinal); + } + + [Fact] + public void ASessionThatEndedOnTheServerIsShownAsEndedNotAsATransientFault() + { + var answers = new Queue>( + [ + () => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson)), + () => Responses.Text("{}", HttpStatusCode.Unauthorized), + ]); + using var context = new BunitContext(); + RegisterProbes(context, ScriptedServer.Always(() => answers.Dequeue()())); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Sign out"))); + Button(cut, "Sign out").Click(); + cut.WaitForAssertion(() => Assert.Contains(Ended, cut.Markup, StringComparison.Ordinal)); + Assert.Empty(cut.FindAll("[role=alert]")); + } + + [Fact] + public void ARefusedOrUnconfirmedSignOutIsReportedAsAFailureAndOffersAFreshRead() + { + var cases = new (Func Answer, string Text)[] + { + (() => Responses.Text("{}", HttpStatusCode.Forbidden), "The server refused this request."), + (() => Responses.Json(ProbeFixtures.ReceiptBody("didNotHappen")), "The server rejected the request."), + (() => Responses.Json(ProbeFixtures.ReceiptBody("unknown")), "The server failed unexpectedly. Try again later."), + }; + foreach (var (answer, text) in cases) + { + var answers = new Queue>( + [ + () => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson)), + answer, + ]); + using var context = new BunitContext(); + RegisterProbes(context, ScriptedServer.Always(() => answers.Dequeue()())); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Sign out"))); + Button(cut, "Sign out").Click(); + cut.WaitForAssertion(() => Assert.Equal(text, cut.Find("[role=alert]").TextContent)); + Assert.DoesNotContain(Ended, cut.Markup, StringComparison.Ordinal); + // The page does not claim a sign-out; it offers a fresh read (a new session and token). + Assert.True(HasButton(cut, "Try again")); + Assert.False(HasButton(cut, "Sign out")); + } + } + + [Fact] + public void AFailedReadIsAnnouncedAndCanBeRetried() + { + var answers = new Queue>( + [ + () => Responses.Text("down", HttpStatusCode.ServiceUnavailable), + () => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson)), + ]); + var server = ScriptedServer.Always(() => answers.Dequeue()()); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Equal("The server is unavailable. Try again later.", cut.Find("[role=alert]").TextContent)); + Button(cut, "Try again").Click(); + cut.WaitForAssertion(() => Assert.Contains("You are not signed in.", cut.Markup, StringComparison.Ordinal)); + Assert.Equal(2, server.Count); + } + + [Fact] + public void AnUnreadableAnswerIsShownAsMalformedAndLeavesNoSession() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes("{\"csrfToken\":\"x\"}"))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Equal("The server answered with something this page cannot read.", cut.Find("[role=alert]").TextContent)); + } + + [Fact] + public async Task LeavingThePageCancelsTheReadThatIsStillInFlight() + { + CancellationToken observed = default; + var server = new ScriptedServer(async (_, token) => + { + observed = token; + await Task.Delay(Timeout.Infinite, token); + return Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson)); + }); + using var context = new BunitContext(); + RegisterProbes(context, server); + + context.Render(); + await WaitUntilAsync(() => server.Count == 1); + Assert.False(observed.IsCancellationRequested); + // Disposing the rendered tree unmounts the page, which must cancel the read in flight. + context.Dispose(); + Assert.True(observed.IsCancellationRequested); + } + + internal static async Task WaitUntilAsync(Func condition) + { + for (var attempt = 0; attempt < 500 && !condition(); attempt++) + await Task.Delay(10); + Assert.True(condition(), "The condition was not reached in time."); + } +} diff --git a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs new file mode 100644 index 0000000..d9fce88 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of the chat cases of tests/unit/app-routes.test.tsx: the Chat page rendered with bUnit over the same-origin gRPC-Web +// probe. The server is a test-only double; every reply is a real gRPC-Web frame sequence from the generated protobuf type. +using ArcForges.Contracts.Hello.V1; +using ArcForges.Web.App.Pages; +using ArcForges.Web.App.Probe; +using ArcForges.Web.App.Tests.Fixtures; +using ArcForges.Web.App.Tests.TestDoubles; +using AngleSharp.Dom; +using Bunit; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class ChatPageTests +{ + private const string Unavailable = "The server is unavailable. Try again later."; + + private static IElement SendButton(IRenderedComponent cut) => + cut.FindAll("button").Single(button => button.TextContent.Trim() is "Send" or "Sending…"); + + private static IElement? CancelButton(IRenderedComponent cut) => + cut.FindAll("button").SingleOrDefault(button => button.TextContent.Trim() == "Cancel"); + + private static HttpResponseMessage Greeting(string name) => + Responses.Raw(ProbeFixtures.HelloReply($"Hello, {name}!"), "application/grpc-web+proto"); + + private static string NameOf(CapturedRequest request) => + SayHelloRequest.Parser.ParseFrom(request.Body.AsSpan(5).ToArray()).Name; + + private static void WaitIdle(IRenderedComponent cut) => + cut.WaitForAssertion(() => Assert.DoesNotContain("Sending…", cut.Markup, StringComparison.Ordinal)); + + [Fact] + public void ChatSendsNothingUntilAskedThenShowsBothSidesOfOneAnonymousExchange() + { + var server = ScriptedServer.Always(() => Responses.Raw(ProbeFixtures.HelloReply("Hello, ArcForges!"), "application/grpc-web+proto")); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + Assert.Equal(0, server.Count); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Hello, ArcForges!", cut.Markup, StringComparison.Ordinal)); + + Assert.Equal(1, server.Count); + var request = Assert.Single(server.Requests); + Assert.Equal($"{ProbeFixtures.Origin.Scheme}://{ProbeFixtures.Origin.Authority}{HelloProbe.HelloApiPath}", request.Url.ToString()); + Assert.False(request.HasCookieHeader); + var items = cut.FindAll("li").Select(item => item.TextContent).ToList(); + Assert.Equal(new[] { "youArcForges", "serverHello, ArcForges!" }, items); + } + + [Fact] + public void ChatShowsTheServersTypedRefusalAsAFixedNoticeAndKeepsWorking() + { + var answers = new Queue>( + [ + () => Responses.Raw( + ProbeFixtures.Trailers("grpc-status: 3\r\ngrpc-message: Name%20must%20not%20be%20empty.\r\n"), + "application/grpc-web+proto"), + () => Responses.Raw(ProbeFixtures.HelloReply("Hello, again!"), "application/grpc-web+proto"), + ]); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, ScriptedServer.Always(() => answers.Dequeue()())); + + var cut = context.Render(); + cut.Find("input").Input(string.Empty); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("The server rejected the request.", cut.Markup, StringComparison.Ordinal)); + Assert.DoesNotContain("must not be empty", cut.Markup, StringComparison.Ordinal); + + cut.Find("input").Input("again"); + WaitIdle(cut); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Hello, again!", cut.Markup, StringComparison.Ordinal)); + } + + [Fact] + public void ChatCanCancelAPendingMessageReportsItAsCancelledAndDoesNotDoubleSend() + { + var server = new ScriptedServer(async (_, token) => + { + await Task.Delay(Timeout.Infinite, token); + return Greeting("unused"); + }); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); + Assert.True(SendButton(cut).HasAttribute("disabled")); + // A second submit while one is pending does not send again. + cut.Find("form").Submit(); + Assert.Equal(1, server.Count); + + CancelButton(cut)!.Click(); + cut.WaitForAssertion(() => Assert.Contains("The request was cancelled.", cut.Markup, StringComparison.Ordinal)); + Assert.Null(CancelButton(cut)); + Assert.False(SendButton(cut).HasAttribute("disabled")); + } + + [Fact] + public void ChatRendersUntrustedNamesAsTextAndKeepsOnlyTheLastTwentyEntries() + { + var echo = new ScriptedServer((request, _) => Task.FromResult(Greeting(NameOf(request)))); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, echo); + + var cut = context.Render(); + cut.Find("input").Input("n"); + for (var index = 0; index < 11; index++) + { + SendButton(cut).Click(); + WaitIdle(cut); + } + Assert.Equal(20, cut.FindAll("li").Count); + Assert.Equal(11, echo.Count); + + cut.Find("input").Input(""); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("<img src=x onerror=alert(1)>", cut.Markup, StringComparison.Ordinal)); + Assert.Empty(cut.FindAll("img")); + } + + [Fact] + public async Task LeavingTheChatPageCancelsTheMessageThatIsStillPending() + { + // The double hands over the token of the request it is answering; the request is pending until it is cancelled. + var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new ScriptedServer(async (_, token) => + { + started.TrySetResult(token); + await Task.Delay(Timeout.Infinite, token); + return Greeting("unused"); + }); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + SendButton(cut).Click(); + var observed = await started.Task.WaitAsync(TimeSpan.FromSeconds(10), Xunit.TestContext.Current.CancellationToken); + Assert.False(observed.IsCancellationRequested); + // Leaving the page disposes the rendered component (bUnit disposes components through DisposeComponentsAsync; the + // context dispose only releases services), and the component cancels the message that is still pending. + await context.DisposeComponentsAsync(); + Assert.True(observed.IsCancellationRequested); + } + + [Fact] + public void TheTransportFailureIsUnavailableAndItIsAFixedNotice() + { + var server = new ScriptedServer((_, _) => throw new HttpRequestException("Failed to fetch")); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains(Unavailable, cut.Markup, StringComparison.Ordinal)); + } +} diff --git a/tests/ArcForges.Web.App.Tests/ExactUnsignedTests.cs b/tests/ArcForges.Web.App.Tests/ExactUnsignedTests.cs new file mode 100644 index 0000000..0e0509a --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/ExactUnsignedTests.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of the 64-bit case of tests/unit/app-session.test.ts: canonical decimal text in, the same text out, and no value passes +// through a floating-point number. +using ArcForges.Web.App.Probe; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class ExactUnsignedTests +{ + [Fact] + public void SixtyFourBitValuesStayExactCanonicalTextInTheSameTextOut() + { + foreach (var value in new[] + { + "0", + "9007199254740991", + "9007199254740993", + "9223372036854775807", + "18446744073709551615", + }) + { + Assert.True(Exact.TryUnsigned(value, out var exact), value); + Assert.Equal(value, exact.Text); + Assert.Equal(ulong.Parse(value, System.Globalization.CultureInfo.InvariantCulture), exact.Value); + } + } + + [Fact] + public void AFloatingPointNumberCannotHoldTheseValuesApartButTheExactPathCan() + { + // A double cannot tell 9007199254740993 from 9007199254740992; the exact path can. + Assert.Equal((double)9007199254740993UL, (double)9007199254740992UL); + Assert.True(Exact.TryUnsigned("9007199254740993", out var odd)); + Assert.True(Exact.TryUnsigned("9007199254740992", out var even)); + Assert.NotEqual(odd.Value, even.Value); + } + + [Fact] + public void AnythingThatIsNotCanonicalUint64TextIsRefused() + { + foreach (var value in new[] + { + string.Empty, + "01", + "-1", + "+1", + "1.0", + "1e3", + " 1", + "1 ", + "0x10", + "18446744073709551616", + }) + Assert.False(Exact.TryUnsigned(value, out _), value); + Assert.False(Exact.TryUnsigned(null, out _)); + } +} diff --git a/tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs b/tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs new file mode 100644 index 0000000..f1f1262 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Test-only fixtures for the Account and Chat probes. They are wire documents and frames, byte for byte what the same-origin +// server sends; they are produced with the generated Contracts types and never part of a release graph. +using System.Text; +using System.Text.Json.Nodes; +using ArcForges.Contracts.Hello.V1; +using Google.Protobuf; + +namespace ArcForges.Web.App.Tests.Fixtures; + +/// The fixture documents and frames of the Account and Chat tests. +public static class ProbeFixtures +{ + /// The origin the fixtures are served from (the TypeScript fixture origin). + public static readonly Uri Origin = new("https://account.example.test/"); + + /// The CSRF token of the authenticated bootstrap. + public const string AuthenticatedCsrf = "cccccccccccccccccccccccc"; + + /// The CSRF token of the anonymous bootstrap. + public const string AnonymousCsrf = "aaaaaaaaaaaaaaaaaaaaaaaa"; + + /// The authenticated bootstrap document (the fixture of the TypeScript suite, in wire form). + public const string AuthenticatedJson = + "{\"csrfToken\":\"" + AuthenticatedCsrf + "\",\"authenticated\":true," + + "\"session\":{\"sessionId\":\"6d1d4c2a-62a0-4b86-9d3f-0c6a3d0b5c11\",\"expiresAt\":\"2026-10-03T08:00:00.000000Z\"," + + "\"idleExpiresAt\":\"2026-10-02T20:30:00.000000Z\",\"userId\":\"0f0e6a30-5d1c-4c7e-8b53-5b6b7f9a4a10\"," + + "\"deviceId\":\"8f2a1d77-1c1b-4b0a-9a55-2f1d0e5c7b21\",\"workspaceIds\":[\"3f1d3b1e-2a8c-4c44-a1c8-77a1e8f0b9a1\"," + + "\"a2b84f55-0c2d-4b0d-8e6a-5d6e44a6c7d3\"],\"recoveryGeneration\":\"18446744073709551615\",\"purpose\":\"authenticate\"}," + + "\"profile\":{\"displayName\":\"Ada Lovelace\",\"locale\":\"en-GB\",\"timezone\":\"Europe/London\"," + + "\"revision\":\"9007199254740993\"}}"; + + /// The anonymous bootstrap document. + public const string AnonymousJson = "{\"csrfToken\":\"" + AnonymousCsrf + "\",\"authenticated\":false}"; + + /// A logout receipt with the given effect. + public static byte[] ReceiptBody(string effect = "happened") => + Encoding.UTF8.GetBytes( + "{\"commandId\":\"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\"effect\":\"" + effect + "\"}"); + + /// The authenticated bootstrap as a JSON object that a test can change field by field. + public static JsonObject AuthenticatedNode() => JsonNode.Parse(AuthenticatedJson)!.AsObject(); + + /// The anonymous bootstrap as a JSON object that a test can change field by field. + public static JsonObject AnonymousNode() => JsonNode.Parse(AnonymousJson)!.AsObject(); + + /// The UTF-8 bytes of a JSON object. + public static byte[] Bytes(JsonObject node) => Encoding.UTF8.GetBytes(node.ToJsonString()); + + /// One gRPC-Web frame: a flag byte, a big-endian 32-bit length and the payload. + public static byte[] Frame(ReadOnlySpan payload, byte flag) + { + var frame = new byte[payload.Length + 5]; + frame[0] = flag; + System.Buffers.Binary.BinaryPrimitives.WriteUInt32BigEndian(frame.AsSpan(1, 4), checked((uint)payload.Length)); + payload.CopyTo(frame.AsSpan(5)); + return frame; + } + + /// A trailers frame (flag 0x80) carrying the given text. + public static byte[] Trailers(string text) => Frame(Encoding.UTF8.GetBytes(text), 0x80); + + /// A binary gRPC-Web hello answer: the SayHelloResponse data frame and an OK trailer. + public static byte[] HelloReply(string message = "Hello, ArcForges!") + { + var payload = new SayHelloResponse { Message = message }.ToByteArray(); + return Concat(Frame(payload, 0), Trailers("grpc-status: 0\r\n")); + } + + /// The bytes of the given parts, in order. + public static byte[] Concat(params byte[][] parts) + { + var total = 0; + foreach (var part in parts) + total += part.Length; + var bytes = new byte[total]; + var offset = 0; + foreach (var part in parts) + { + part.CopyTo(bytes, offset); + offset += part.Length; + } + return bytes; + } +} diff --git a/tests/ArcForges.Web.App.Tests/HelloProbeTests.cs b/tests/ArcForges.Web.App.Tests/HelloProbeTests.cs new file mode 100644 index 0000000..ad3dacb --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/HelloProbeTests.cs @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of tests/unit/app-hello.test.ts: the anonymous binary gRPC-Web greeting through the generated Contracts client over +// Grpc.Net.Client.Web. The server is a test-only double; every answer is a real gRPC-Web frame sequence. +using System.Net; +using System.Text; +using ArcForges.Contracts.Hello.V1; +using ArcForges.Web.App.Probe; +using ArcForges.Web.App.Tests.Fixtures; +using ArcForges.Web.App.Tests.TestDoubles; +using Grpc.Core; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class HelloProbeTests +{ + private const string GrpcWeb = "application/grpc-web+proto"; + + private static HelloProbe ProbeFor(HttpMessageHandler handler, TimeSpan? deadline = null) => + new(handler, new ProbeOrigin(ProbeFixtures.Origin), deadline ?? HelloProbe.DefaultDeadline); + + private static async Task FailureOf(Func call) + { + var failure = await Assert.ThrowsAsync(call); + return failure.Kind; + } + + private static HttpResponseMessage Answer(byte[] body, string contentType = GrpcWeb, HttpStatusCode status = HttpStatusCode.OK) + { + var response = new HttpResponseMessage(status) { Content = new ByteArrayContent(body) }; + response.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(contentType); + return response; + } + + [Fact] + public void TheDeadlineIsTenSecondsAndTheHelloPathIsTheSameOriginApiPath() + { + Assert.Equal(TimeSpan.FromSeconds(10), HelloProbe.DefaultDeadline); + Assert.Equal("/api/arcforges.hello.v1.HelloService/SayHello", HelloProbe.HelloApiPath); + } + + [Fact] + public async Task TheGreetingIsOneAnonymousBinaryGrpcWebCallToSameOriginApi() + { + var server = ScriptedServer.Always(() => Answer(ProbeFixtures.HelloReply("Hello, ArcForges 世界!"))); + var reply = await ProbeFor(server).SayHelloAsync("ArcForges 世界", CancellationToken.None); + + Assert.Equal("Hello, ArcForges 世界!", reply); + var seen = Assert.Single(server.Requests); + Assert.Equal($"{ProbeFixtures.Origin.Scheme}://{ProbeFixtures.Origin.Authority}{HelloProbe.HelloApiPath}", seen.Url.ToString()); + Assert.Equal(HttpMethod.Post, seen.Method); + Assert.False(seen.HasCookieHeader); + Assert.False(seen.HasAuthorizationHeader); + // The request frame: one uncompressed protobuf data frame carrying the name. + Assert.Equal((byte)0, seen.Body[0]); + var length = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(seen.Body.AsSpan(1, 4)); + Assert.Equal(seen.Body.Length - 5, (int)length); + Assert.Equal("ArcForges 世界", SayHelloRequest.Parser.ParseFrom(seen.Body.AsSpan(5).ToArray()).Name); + } + + [Fact] + public async Task EveryGrpcStatusMapsToAClosedFailureKindAndNoneIsASuccess() + { + var expected = new (StatusCode Code, FailureKind Kind)[] + { + (StatusCode.Cancelled, FailureKind.Unexpected), + (StatusCode.Unknown, FailureKind.Unexpected), + (StatusCode.InvalidArgument, FailureKind.Rejected), + (StatusCode.DeadlineExceeded, FailureKind.Timeout), + (StatusCode.NotFound, FailureKind.Rejected), + (StatusCode.AlreadyExists, FailureKind.Rejected), + (StatusCode.PermissionDenied, FailureKind.Forbidden), + (StatusCode.ResourceExhausted, FailureKind.Limit), + (StatusCode.FailedPrecondition, FailureKind.Rejected), + (StatusCode.Aborted, FailureKind.Unavailable), + (StatusCode.OutOfRange, FailureKind.Rejected), + (StatusCode.Unimplemented, FailureKind.Rejected), + (StatusCode.Internal, FailureKind.Unexpected), + (StatusCode.Unavailable, FailureKind.Unavailable), + (StatusCode.DataLoss, FailureKind.Unexpected), + (StatusCode.Unauthenticated, FailureKind.Unauthenticated), + }; + foreach (var (code, kind) in expected) + { + Assert.True(FailureMapping.FromGrpc(code) == kind, code.ToString()); + // The same status arriving as a real trailers-only gRPC-Web answer through the generated client. + var server = ScriptedServer.Always(() => Answer( + ProbeFixtures.Trailers($"grpc-status: {(int)code}\r\ngrpc-message: Server%20text\r\n"))); + Assert.True(await FailureOf(() => ProbeFor(server).SayHelloAsync("x", CancellationToken.None)) == kind, code.ToString()); + } + } + + [Fact] + public async Task AServerMessageIsNeverEchoedIntoTheFailure() + { + var server = ScriptedServer.Always(() => Answer( + ProbeFixtures.Trailers("grpc-status: 3\r\ngrpc-message: secret-token-123\r\n"))); + var failure = await Assert.ThrowsAsync(() => ProbeFor(server).SayHelloAsync("x", CancellationToken.None)); + Assert.Equal(FailureKind.Rejected, failure.Kind); + Assert.DoesNotContain("secret-token-123", failure.Message, StringComparison.Ordinal); + } + + [Fact] + public async Task HttpStatusesAndNonGrpcAnswersAreTypedFailuresNeverALocalGreeting() + { + var answers = new (string Name, Func Answer, FailureKind Kind)[] + { + ("404", () => Answer("Not found"u8.ToArray(), "text/plain", HttpStatusCode.NotFound), FailureKind.Rejected), + ("405", () => Answer("Method not allowed"u8.ToArray(), "text/plain", (HttpStatusCode)405), FailureKind.Rejected), + ("429", () => Answer("Slow down"u8.ToArray(), "text/plain", (HttpStatusCode)429), FailureKind.Limit), + ("500", () => Answer("Broken"u8.ToArray(), "text/plain", HttpStatusCode.InternalServerError), FailureKind.Unexpected), + ("503", () => Answer([], "text/plain", HttpStatusCode.ServiceUnavailable), FailureKind.Unavailable), + ("504", () => Answer([], "text/plain", HttpStatusCode.GatewayTimeout), FailureKind.Timeout), + ("html", () => Answer("Not an API"u8.ToArray(), "text/html"), FailureKind.Malformed), + ("valid frames under another content type", () => Answer(ProbeFixtures.HelloReply(), "application/json"), FailureKind.Malformed), + ("no content type", () => Raw(ProbeFixtures.HelloReply()), FailureKind.Malformed), + }; + foreach (var (name, answer, kind) in answers) + Assert.True( + await FailureOf(() => ProbeFor(ScriptedServer.Always(answer)).SayHelloAsync("ArcForges", CancellationToken.None)) == kind, + name); + + var network = new FailingTransport(new HttpRequestException("Failed to fetch")); + Assert.True(await FailureOf(() => ProbeFor(network).SayHelloAsync("ArcForges", CancellationToken.None)) == FailureKind.Unavailable); + } + + [Fact] + public async Task MalformedFramesAndStatusNeverResolve() + { + var data = ProbeFixtures.HelloReply("Hello, ArcForges!"); + var messageLength = 5 + (int)System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(1, 4)); + var messageFrame = data[..messageLength]; + var bodies = new (string Name, byte[] Body)[] + { + ("empty body", []), + ("truncated header", messageFrame[..3]), + ("truncated message", messageFrame[..(messageLength - 2)]), + ("no trailers", messageFrame), + ("trailers without a status", ProbeFixtures.Concat(messageFrame, ProbeFixtures.Trailers("x-other: 1\r\n"))), + ("unparsable status", ProbeFixtures.Concat(messageFrame, ProbeFixtures.Trailers("grpc-status: ok\r\n"))), + ( + "compressed frame without negotiated compression", + ProbeFixtures.Concat(new byte[] { 1 }.Concat(messageFrame.Skip(1)).ToArray(), ProbeFixtures.Trailers("grpc-status: 0\r\n"))), + ( + "frame length beyond the body", + ProbeFixtures.Concat([0, 0, 0, 1, 0, 1], ProbeFixtures.Trailers("grpc-status: 0\r\n"))), + ("trailers only, status ok, no message", ProbeFixtures.Trailers("grpc-status: 0\r\n")), + ("random bytes", Enumerable.Range(0, 64).Select(index => (byte)((index * 37 + 11) & 0xff)).ToArray()), + }; + foreach (var (name, body) in bodies) + Assert.True( + await FailureOf(() => ProbeFor(ScriptedServer.Always(() => Answer(body))).SayHelloAsync("ArcForges", CancellationToken.None)) == FailureKind.Malformed, + name); + } + + [Fact] + public async Task AReplyThatIsNotTheGreetingForThisNameIsMalformed() + { + var server = ScriptedServer.Always(() => Answer(ProbeFixtures.HelloReply("Hello, someone else!"))); + Assert.True(await FailureOf(() => ProbeFor(server).SayHelloAsync("ArcForges", CancellationToken.None)) == FailureKind.Malformed); + } + + [Fact] + public async Task TheUsersCancellationAbortsTheRequestAndIsReportedAsCancelled() + { + using var controller = new CancellationTokenSource(); + var transport = new NeverAnswers(); + var pending = ProbeFor(transport).SayHelloAsync("ArcForges", controller.Token); + await transport.Started.Task; + Assert.False(transport.Observed.IsCancellationRequested); + await controller.CancelAsync(); + Assert.True(await FailureOf(() => pending) == FailureKind.Cancelled); + Assert.True(transport.Observed.IsCancellationRequested); + } + + [Fact] + public async Task ACallThatHasAlreadyBeenCancelledNeverReachesASuccess() + { + using var controller = new CancellationTokenSource(); + await controller.CancelAsync(); + var server = ScriptedServer.Always(() => Answer(ProbeFixtures.HelloReply())); + Assert.True(await FailureOf(() => ProbeFor(server).SayHelloAsync("ArcForges", controller.Token)) == FailureKind.Cancelled); + } + + [Fact] + public async Task AServerThatNeverAnswersEndsAtTheDeadlineAsATimeout() + { + // The ten second deadline is the page's; the test shortens it to keep the suite quick and asserts the value above. + var transport = new NeverAnswers(); + var failure = await FailureOf(() => ProbeFor(transport, TimeSpan.FromMilliseconds(200)).SayHelloAsync("ArcForges", CancellationToken.None)); + Assert.True(failure == FailureKind.Timeout); + } + + private static HttpResponseMessage Raw(byte[] body) + { + var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(body) }; + return response; + } + + /// A test-only transport that fails before any answer. + private sealed class FailingTransport(Exception failure) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + Task.FromException(failure); + } + + /// A test-only transport that never answers and records the token it was given, as a stalled fetch does. + private sealed class NeverAnswers : HttpMessageHandler + { + public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + + public CancellationToken Observed { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Observed = cancellationToken; + Started.TrySetResult(); + await Task.Delay(Timeout.Infinite, cancellationToken); + return new HttpResponseMessage(HttpStatusCode.OK); + } + } +} diff --git a/tests/ArcForges.Web.App.Tests/RepositoryRoot.cs b/tests/ArcForges.Web.App.Tests/RepositoryRoot.cs new file mode 100644 index 0000000..f7cd00a --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/RepositoryRoot.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.App.Tests; + +/// Locates the repository root from the test output directory. +public static class RepositoryRoot +{ + /// Walks up from the test assembly until the repository marker files are found. + public static string Find() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "win.slnx")) + && File.Exists(Path.Combine(directory.FullName, "global.json"))) + return directory.FullName; + directory = directory.Parent; + } + throw new DirectoryNotFoundException("The repository root with win.slnx and global.json was not found."); + } +} diff --git a/tests/ArcForges.Web.App.Tests/SessionProbeTests.cs b/tests/ArcForges.Web.App.Tests/SessionProbeTests.cs new file mode 100644 index 0000000..54891cf --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/SessionProbeTests.cs @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Port of tests/unit/app-session.test.ts: the same-origin session probe (bootstrap, logout, the bounded body and the typed +// failures). The server is a test-only double (TestDoubles/ScriptedServer.cs); the documents are wire fixtures. +using System.Net; +using System.Text; +using ArcForges.Web.App.Probe; +using ArcForges.Web.App.Tests.Fixtures; +using ArcForges.Web.App.Tests.TestDoubles; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class SessionProbeTests +{ + private const string Json = "application/json"; + + private static SessionProbe ProbeFor(HttpMessageHandler handler) => + new(handler, new ProbeOrigin(ProbeFixtures.Origin)); + + private static CancellationToken Never => CancellationToken.None; + + private static async Task FailureOf(Func call) + { + var failure = await Assert.ThrowsAsync(call); + return failure.Kind; + } + + private static async Task ReadFailure(HttpMessageHandler handler) => + await FailureOf(() => ProbeFor(handler).ReadSessionAsync(Never)); + + [Fact] + public async Task BootstrapIsASameOriginCookieGetDrivenByTheGeneratedRouteCatalogue() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson))); + var session = await ProbeFor(server).ReadSessionAsync(Never); + + var seen = Assert.Single(server.Requests); + Assert.Equal($"{ProbeFixtures.Origin.Scheme}://{ProbeFixtures.Origin.Authority}/session/v1/bootstrap", seen.Url.ToString()); + Assert.Equal(HttpMethod.Get, seen.Method); + Assert.Null(seen.CsrfHeader); + Assert.False(seen.HasAuthorizationHeader); + Assert.Equal(Json, seen.AcceptHeader); + Assert.Equal( + new AuthenticatedSession( + ProbeFixtures.AuthenticatedCsrf, + "Ada Lovelace", + "en-GB", + "Europe/London", + "2026-10-03T08:00:00.000000Z", + "2026-10-02T20:30:00.000000Z", + 2, + "18446744073709551615"), + session); + } + + [Fact] + public async Task AnAnonymousAnswerCarriesOnlyTheCsrfToken() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson))); + var session = await ProbeFor(server).ReadSessionAsync(Never); + Assert.Equal(new AnonymousSession(ProbeFixtures.AnonymousCsrf), session); + } + + [Fact] + public async Task AnAuthenticatedSessionWithoutAProfileStillPresents() + { + var node = ProbeFixtures.AuthenticatedNode(); + node.Remove("profile"); + var server = ScriptedServer.Always(() => Responses.Json(ProbeFixtures.Bytes(node))); + + var session = Assert.IsType(await ProbeFor(server).ReadSessionAsync(Never)); + Assert.Null(session.DisplayName); + Assert.Null(session.Locale); + } + + [Fact] + public async Task ContradictoryOrInexactBootstrapAnswersAreMalformedNeverASession() + { + var withoutSession = ProbeFixtures.AuthenticatedNode(); + withoutSession.Remove("session"); + + var anonymousWithSession = ProbeFixtures.AnonymousNode(); + anonymousWithSession["session"] = ProbeFixtures.AuthenticatedNode()["session"]!.DeepClone(); + + var anonymousWithProfile = ProbeFixtures.AnonymousNode(); + anonymousWithProfile["profile"] = ProbeFixtures.AuthenticatedNode()["profile"]!.DeepClone(); + + var leadingZero = ProbeFixtures.AuthenticatedNode(); + leadingZero["session"]!["recoveryGeneration"] = "01"; + + var aboveUint64 = ProbeFixtures.AuthenticatedNode(); + aboveUint64["session"]!["recoveryGeneration"] = "18446744073709551616"; + + var cases = new (string Name, byte[] Body)[] + { + ("authenticated without a session", ProbeFixtures.Bytes(withoutSession)), + ("anonymous with a session", ProbeFixtures.Bytes(anonymousWithSession)), + ("anonymous with a profile", ProbeFixtures.Bytes(anonymousWithProfile)), + ("leading zero generation", ProbeFixtures.Bytes(leadingZero)), + ("generation above uint64", ProbeFixtures.Bytes(aboveUint64)), + }; + foreach (var (name, body) in cases) + Assert.True( + await ReadFailure(ScriptedServer.Always(() => Responses.Json(body))) == FailureKind.Malformed, + name); + } + + [Fact] + public async Task TheGeneratedCodecRefusesMalformedUnknownFieldAndWrongTypeDocuments() + { + var bodies = new[] + { + string.Empty, + "{", + "[]", + "null", + "{\"csrfToken\":\"x\"}", + "{\"csrfToken\":\"x\",\"authenticated\":\"yes\"}", + "{\"csrfToken\":\"x\",\"authenticated\":false,\"extra\":1}", + "{\"csrfToken\":\"x\",\"authenticated\":false,\"authenticated\":true}", + "{\"csrfToken\":\"" + new string('x', 20) + "\",\"authenticated\":false", + }; + foreach (var body in bodies) + Assert.True( + await ReadFailure(ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(body)))) == FailureKind.Malformed, + body); + } + + [Fact] + public async Task OnlyAJsonTwoHundredIsReadEveryOtherStatusIsATypedFailureWithAFixedMessage() + { + var statuses = new (HttpStatusCode Status, FailureKind Kind)[] + { + ((HttpStatusCode)301, FailureKind.Malformed), + (HttpStatusCode.BadRequest, FailureKind.Rejected), + (HttpStatusCode.Unauthorized, FailureKind.Unauthenticated), + (HttpStatusCode.Forbidden, FailureKind.Forbidden), + (HttpStatusCode.NotFound, FailureKind.Rejected), + (HttpStatusCode.RequestTimeout, FailureKind.Timeout), + (HttpStatusCode.RequestEntityTooLarge, FailureKind.Limit), + ((HttpStatusCode)429, FailureKind.Limit), + (HttpStatusCode.InternalServerError, FailureKind.Unexpected), + (HttpStatusCode.BadGateway, FailureKind.Unavailable), + (HttpStatusCode.ServiceUnavailable, FailureKind.Unavailable), + (HttpStatusCode.GatewayTimeout, FailureKind.Timeout), + }; + foreach (var (status, kind) in statuses) + { + // A body that would parse as a session proves the status is checked first. + var failure = await Assert.ThrowsAsync(() => + ProbeFor(ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson), status))) + .ReadSessionAsync(Never)); + Assert.True(failure.Kind == kind, status.ToString()); + Assert.DoesNotContain(ProbeFixtures.AuthenticatedCsrf, failure.Message, StringComparison.Ordinal); + } + + foreach (var contentType in new[] { "text/html", "application/x-ndjson", "application/jsonp" }) + Assert.True( + await ReadFailure(ScriptedServer.Always(() => Responses.Raw(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson), contentType))) == FailureKind.Malformed, + contentType); + Assert.True( + await ReadFailure(ScriptedServer.Always(() => Responses.Raw(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson), null))) == FailureKind.Malformed, + "no content type"); + + var parameters = await ProbeFor(ScriptedServer.Always(() => + Responses.Raw(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson), "application/json; charset=utf-8"))).ReadSessionAsync(Never); + Assert.IsType(parameters); + } + + [Fact] + public async Task TheBodyIsBoundedBeforeItIsParsed() + { + var exact = Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson); + var padded = new byte[16384 + 1]; + Array.Fill(padded, (byte)0x20); + exact.CopyTo(padded, 16384 + 1 - exact.Length); + // At the schema bound a document is still read: the padding is JSON whitespace. + var atBound = new byte[16384]; + Array.Fill(atBound, (byte)0x20); + exact.CopyTo(atBound, 16384 - exact.Length); + + Assert.IsType(await ProbeFor(ScriptedServer.Always(() => Responses.Json(atBound))).ReadSessionAsync(Never)); + Assert.True(await ReadFailure(ScriptedServer.Always(() => Responses.Json(padded))) == FailureKind.Malformed); + + // A declared length above the bound is refused without reading, and a stream above it is cut off after at most + // one chunk beyond the bound, for the session and the receipt alike. + foreach (var bound in new[] { 16384, 4096 }) + { + var endless = new CountingStream((byte)0x20, 1024); + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(endless), + }; + response.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(Json); + var handler = ScriptedServer.Always(() => response); + var failure = await FailureOfBound(handler, bound); + Assert.True(failure == FailureKind.Malformed, bound.ToString()); + Assert.True(endless.Disposed, bound.ToString()); + Assert.True(endless.BytesYielded > bound, bound.ToString()); + Assert.True(endless.BytesYielded <= bound + 4096, bound.ToString()); + } + + var declared = Responses.Json(exact); + declared.Content.Headers.Remove("Content-Length"); + declared.Content.Headers.TryAddWithoutValidation("Content-Length", "16385"); + Assert.True(await ReadFailure(ScriptedServer.Always(() => declared)) == FailureKind.Malformed); + + var malformedLength = Responses.Json(exact); + malformedLength.Content.Headers.Remove("Content-Length"); + malformedLength.Content.Headers.TryAddWithoutValidation("Content-Length", "12e3"); + Assert.True(await ReadFailure(ScriptedServer.Always(() => malformedLength)) == FailureKind.Malformed); + } + + [Fact] + public async Task TheStreamBoundIsExactOneByteOverItIsTheFirstByteRefused() + { + foreach (var bound in new[] { 16384, 4096 }) + { + var answer = new CountingStream((byte)0x20, 1); + var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(answer) }; + response.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(Json); + var handler = ScriptedServer.Always(() => response); + Assert.True(await FailureOfBound(handler, bound) == FailureKind.Malformed, bound.ToString()); + Assert.Equal(bound + 1, answer.Reads); + } + } + + [Fact] + public async Task ABodySplitAcrossChunksReadsAsOneDocumentAFailingStreamIsUnavailable() + { + var bytes = Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson); + var chunks = new[] { bytes[..7], bytes[7..90], bytes[90..] }; + var answer = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(new ChunkedStream(chunks)) }; + answer.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(Json); + Assert.IsType(await ProbeFor(ScriptedServer.Always(() => answer)).ReadSessionAsync(Never)); + + var broken = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(new ChunkedStream([bytes[..20]], new IOException("connection reset"))), + }; + broken.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(Json); + Assert.True(await ReadFailure(ScriptedServer.Always(() => broken)) == FailureKind.Unavailable); + } + + [Fact] + public async Task NetworkFailureIsUnavailableAndCancellationIsDistinctFromIt() + { + var failing = new DelegatingFailure(new HttpRequestException("Failed to fetch")); + Assert.True(await ReadFailure(failing) == FailureKind.Unavailable); + + using var controller = new CancellationTokenSource(); + var pending = ProbeFor(new NeverAnswers()).ReadSessionAsync(controller.Token); + await controller.CancelAsync(); + Assert.True(await FailureOf(() => pending) == FailureKind.Cancelled); + + using var already = new CancellationTokenSource(); + await already.CancelAsync(); + Assert.True(await FailureOf(() => ProbeFor(ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson)))) + .ReadSessionAsync(already.Token)) == FailureKind.Cancelled); + } + + [Fact] + public async Task LogoutSendsTheCsrfTokenAsTheOnlyCredentialHeaderWithNoBody() + { + var server = ScriptedServer.Always(() => Responses.Json(ProbeFixtures.ReceiptBody("happened"))); + await ProbeFor(server).EndSessionAsync(ProbeFixtures.AuthenticatedCsrf, Never); + + var seen = Assert.Single(server.Requests); + Assert.Equal($"{ProbeFixtures.Origin.Scheme}://{ProbeFixtures.Origin.Authority}/session/v1/logout", seen.Url.ToString()); + Assert.Equal(HttpMethod.Post, seen.Method); + // The literal wire name is pinned here, independent of the module constant. + Assert.Equal("X-AF-CSRF", SessionProbe.CsrfHeader, ignoreCase: false); + Assert.Equal(ProbeFixtures.AuthenticatedCsrf, seen.CsrfHeader); + Assert.False(seen.HasAuthorizationHeader); + Assert.Empty(seen.Body); + } + + [Fact] + public async Task LogoutReportsAnEndedSessionARefusedTokenAndAMalformedReceiptDistinctly() + { + static Task Run(HttpResponseMessage response) => + ProbeFor(ScriptedServer.Always(() => response)).EndSessionAsync("t", CancellationToken.None); + + Assert.True(await FailureOf(() => Run(Responses.Text("{}", HttpStatusCode.Unauthorized))) == FailureKind.Unauthenticated); + Assert.True(await FailureOf(() => Run(Responses.Text("{}", HttpStatusCode.Forbidden))) == FailureKind.Forbidden); + Assert.True(await FailureOf(() => Run(Responses.Text("{}", HttpStatusCode.BadRequest))) == FailureKind.Rejected); + Assert.True(await FailureOf(() => Run(Responses.Text("{}", HttpStatusCode.ServiceUnavailable))) == FailureKind.Unavailable); + Assert.True(await FailureOf(() => Run(Responses.Json(ProbeFixtures.ReceiptBody("didNotHappen")))) == FailureKind.Rejected); + Assert.True(await FailureOf(() => Run(Responses.Json(ProbeFixtures.ReceiptBody("unknown")))) == FailureKind.Unexpected); + Assert.True(await FailureOf(() => Run(Responses.Json(Encoding.UTF8.GetBytes("{}")))) == FailureKind.Malformed); + Assert.True(await FailureOf(() => Run(Responses.Json(Encoding.UTF8.GetBytes("{\"commandId\":\"x\"}")))) == FailureKind.Malformed); + Assert.True(await FailureOf(() => Run(Responses.Json(Enumerable.Repeat((byte)0x20, 4097).ToArray()))) == FailureKind.Malformed); + } + + private static async Task FailureOfBound(HttpMessageHandler handler, int bound) + { + // The session route has the 16 KiB bound; the logout route has the 4 KiB receipt bound. + if (bound == 16384) + return await ReadFailure(handler); + return await FailureOf(() => ProbeFor(handler).EndSessionAsync("t", Never)); + } + + /// A test-only transport that fails before any answer. + private sealed class DelegatingFailure(Exception failure) : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => + Task.FromException(failure); + } + + /// A test-only transport that never answers and ends only when the caller cancels, as a stalled fetch does. + private sealed class NeverAnswers : HttpMessageHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + await Task.Delay(Timeout.Infinite, cancellationToken); + return new HttpResponseMessage(HttpStatusCode.OK); + } + } +} diff --git a/tests/ArcForges.Web.App.Tests/TestDoubles/ScriptedServer.cs b/tests/ArcForges.Web.App.Tests/TestDoubles/ScriptedServer.cs new file mode 100644 index 0000000..b587c29 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/TestDoubles/ScriptedServer.cs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// TEST-ONLY DOUBLE. The same-origin server is an external input that these unit tests do not run; this double answers +// with the scripted responses the tests set up, and records each request as it was sent. It is never referenced by the +// application project and never part of a release graph. +using System.Net.Http.Headers; + +namespace ArcForges.Web.App.Tests.TestDoubles; + +/// What the test double observed of one request, captured before the transport disposed it. +public sealed record CapturedRequest( + Uri Url, + HttpMethod Method, + string? CsrfHeader, + bool HasCookieHeader, + bool HasAuthorizationHeader, + string? AcceptHeader, + byte[] Body); + +/// A scripted same-origin server: each request is answered by the given function, and cancellation reaches it. +public sealed class ScriptedServer : HttpMessageHandler +{ + private readonly Func> _answer; + + /// Creates the double with the answer for each request. + public ScriptedServer(Func> answer) + { + _answer = answer; + } + + /// Every request that reached the double, in order. + public List Requests { get; } = []; + + /// The number of requests that reached the double. + public int Count => Requests.Count; + + /// Answers every request with the same response factory. + public static ScriptedServer Always(Func answer) => + new((_, _) => Task.FromResult(answer())); + + /// Records the request as sent, then answers it. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + var body = request.Content is null ? [] : await request.Content.ReadAsByteArrayAsync(cancellationToken); + var captured = new CapturedRequest( + request.RequestUri ?? throw new InvalidOperationException("The request has no URL."), + request.Method, + request.Headers.TryGetValues("X-AF-CSRF", out var csrf) ? csrf.SingleOrDefault() : null, + request.Headers.Contains("Cookie"), + request.Headers.Contains("Authorization"), + request.Headers.Accept.Count == 0 ? null : request.Headers.Accept.ToString(), + body); + Requests.Add(captured); + return await _answer(captured, cancellationToken); + } +} + +/// +/// A test-only response stream that yields the given chunks, then either ends or fails. It stands for a connection that +/// delivers the body in pieces and may break. +/// +public sealed class ChunkedStream(IReadOnlyList chunks, Exception? failWith = null) : Stream +{ + private int _index; + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (_index < chunks.Count) + { + var chunk = chunks[_index++]; + var count = Math.Min(chunk.Length, buffer.Length); + chunk.AsSpan(0, count).CopyTo(buffer.Span); + return ValueTask.FromResult(count); + } + if (failWith is not null) + throw failWith; + return ValueTask.FromResult(0); + } +} + +/// +/// A test-only endless response stream: each read yields a fixed number of bytes and counts the reads, and disposing it +/// records that the transfer was cut off. +/// +public sealed class CountingStream(byte fill, int bytesPerRead) : Stream +{ + /// The number of reads made so far. + public long Reads { get; private set; } + + /// The total bytes yielded so far. + public long BytesYielded { get; private set; } + + /// True once the stream was disposed, which is how a cut-off transfer is observed. + public bool Disposed { get; private set; } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + Reads++; + var count = Math.Min(bytesPerRead, buffer.Length); + buffer.Span[..count].Fill(fill); + BytesYielded += count; + return ValueTask.FromResult(count); + } + + protected override void Dispose(bool disposing) + { + Disposed = true; + base.Dispose(disposing); + } +} + +/// Helpers that build the test-only response shapes. +public static class Responses +{ + /// A response with a JSON body and the given status. + public static HttpResponseMessage Json(byte[] body, System.Net.HttpStatusCode status = System.Net.HttpStatusCode.OK, string contentType = "application/json") + { + var response = new HttpResponseMessage(status) { Content = new ByteArrayContent(body) }; + response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + return response; + } + + /// A response with a text body and the given status and content type. + public static HttpResponseMessage Text(string body, System.Net.HttpStatusCode status = System.Net.HttpStatusCode.OK, string? contentType = null) + { + var response = new HttpResponseMessage(status) { Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(body)) }; + if (contentType is not null) + response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + return response; + } + + /// A response with the given bytes and content type, for shapes the test controls exactly. + public static HttpResponseMessage Raw(byte[] body, string? contentType, System.Net.HttpStatusCode status = System.Net.HttpStatusCode.OK) + { + var response = new HttpResponseMessage(status) { Content = new ByteArrayContent(body) }; + if (contentType is not null) + response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + return response; + } +} diff --git a/tests/ArcForges.Web.App.Tests/WasmProfilePolicyTests.cs b/tests/ArcForges.Web.App.Tests/WasmProfilePolicyTests.cs new file mode 100644 index 0000000..a6571a3 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/WasmProfilePolicyTests.cs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The App profile's policy rules (WEB.40 validation; P2-021 item 2): the exact script-src and style-src token sets, no inline +// style, no Virtualize, no JavaScript interop, and RunAOTCompilation off. The host page is read from the built source. +using System.Text.RegularExpressions; +using ArcForges.Web.Ui; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class WasmProfilePolicyTests +{ + private static readonly string Root = RepositoryRoot.Find(); + + private static string AppSource(string relative) => File.ReadAllText(Path.Combine(Root, "src", "ArcForges.Web.App", relative)); + + [Fact] + public void TheAppPolicyHasExactlyTheWasmScriptTokensAndSelfOnlyStyles() + { + var policy = WasmContentSecurityPolicy.FromHostPages([AppSource("wwwroot/index.html")]); + var directives = WasmContentSecurityPolicy.ParseDirectives(policy); + + // The host page has no inline script, so the only script tokens are self and the WebAssembly compile token. + Assert.Equal(new[] { "'self'", "'wasm-unsafe-eval'" }, directives["script-src"]); + Assert.Equal(new[] { "'self'" }, directives["style-src"]); + Assert.Equal(new[] { "'self'" }, directives["default-src"]); + Assert.Equal(new[] { "'self'" }, directives["connect-src"]); + Assert.DoesNotContain("'unsafe-inline'", policy, StringComparison.Ordinal); + Assert.DoesNotContain("'unsafe-eval'", policy, StringComparison.Ordinal); + Assert.True(policy.Length < WasmContentSecurityPolicy.HeaderLineBudget); + } + + [Fact] + public void ARequiredInlineScriptAddsItsHashAndNothingElseToScriptSources() + { + const string page = ""; + var directives = WasmContentSecurityPolicy.ParseDirectives(WasmContentSecurityPolicy.FromHostPages([page])); + var sources = directives["script-src"]; + Assert.Equal("'self'", sources[0]); + Assert.Equal("'wasm-unsafe-eval'", sources[1]); + var hash = Assert.Single(sources.Skip(2)); + Assert.Matches("^'sha256-[A-Za-z0-9+/]{43}='$", hash); + } + + [Fact] + public void AnExternalOrUnquotedScriptSourceIsRefused() + { + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages(["", RegexOptions.Singleline | RegexOptions.IgnoreCase)) + Assert.True(string.IsNullOrWhiteSpace(script.Groups["body"].Value), "The host page has an inline script body."); + } + + [Fact] + public void NoRazorComponentEmitsAnInlineStyleOrUsesVirtualize() + { + var pages = Directory.EnumerateFiles(Path.Combine(Root, "src", "ArcForges.Web.App"), "*.razor", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(Path.Combine(Root, "src", "ArcForges.Web.Ui"), "*.razor", SearchOption.AllDirectories)); + foreach (var file in pages) + { + var source = File.ReadAllText(file); + Assert.True(!Regex.IsMatch(source, "\\sstyle\\s*=", RegexOptions.IgnoreCase), "Inline style attribute: " + file); + Assert.DoesNotContain("Virtualize", source, StringComparison.Ordinal); + } + } + + [Fact] + public void TheAppUsesNoJavaScriptInteropBecauseNoAuditedPointIsNeeded() + { + var sources = Directory.EnumerateFiles(Path.Combine(Root, "src", "ArcForges.Web.App"), "*.cs", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(Path.Combine(Root, "src", "ArcForges.Web.App"), "*.razor", SearchOption.AllDirectories)); + foreach (var file in sources) + { + var source = File.ReadAllText(file); + Assert.DoesNotContain("IJSRuntime", source, StringComparison.Ordinal); + Assert.DoesNotContain("IJSInProcessRuntime", source, StringComparison.Ordinal); + } + } + + [Fact] + public void TheProfileIsStandaloneWebAssemblyWithAheadOfTimeCompilationOff() + { + var project = File.ReadAllText(Path.Combine(Root, "src", "ArcForges.Web.App", "ArcForges.Web.App.csproj")); + Assert.Contains("Sdk=\"Microsoft.NET.Sdk.BlazorWebAssembly\"", project, StringComparison.Ordinal); + Assert.Contains("false", project, StringComparison.Ordinal); + Assert.DoesNotContain("Microsoft.AspNetCore.Components.Server", project, StringComparison.Ordinal); + } +} diff --git a/tests/ArcForges.Web.Operations.Tests/OperationsProfileTests.cs b/tests/ArcForges.Web.Operations.Tests/OperationsProfileTests.cs new file mode 100644 index 0000000..0ac0c61 --- /dev/null +++ b/tests/ArcForges.Web.Operations.Tests/OperationsProfileTests.cs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The Operations profile skeleton (WEB.40): the shell route renders, the policy is the same exact token set as the App +// profiles, and the separate-origin boundary shares nothing. No operator feature is tested here (OPS.05 and OPS.11). +using ArcForges.Web.Ui; +using Bunit; +using Xunit; + +namespace ArcForges.Web.Operations.Tests; + +public sealed class OperationsProfileTests +{ + private static readonly string Root = RepositoryRoot.Find(); + + [Fact] + public void TheShellRouteRendersTheSeparateOriginSkeletonAndNoOperatorFeature() + { + using var context = new BunitContext(); + var cut = context.Render(); + Assert.Contains("Operations console", cut.Markup, StringComparison.Ordinal); + Assert.Empty(cut.FindAll("form")); + Assert.Empty(cut.FindAll("button")); + } + + [Fact] + public void ThePolicyHasTheSameExactTokenSetAsTheAppProfiles() + { + var html = File.ReadAllText(Path.Combine(Root, "src", "ArcForges.Web.Operations", "wwwroot", "index.html")); + var directives = WasmContentSecurityPolicy.ParseDirectives(WasmContentSecurityPolicy.FromHostPages([html])); + Assert.Equal(new[] { "'self'", "'wasm-unsafe-eval'" }, directives["script-src"]); + Assert.Equal(new[] { "'self'" }, directives["style-src"]); + var policy = WasmContentSecurityPolicy.FromHostPages([html]); + Assert.DoesNotContain("'unsafe-inline'", policy, StringComparison.Ordinal); + Assert.DoesNotContain("'unsafe-eval'", policy, StringComparison.Ordinal); + } + + [Fact] + public void TheSeparateOriginBoundarySharesNoCookieOrSessionWithAnotherProfile() + { + Assert.Empty(ProfileBoundary.SharedCookieOrigins); + Assert.Equal("own-origin", ProfileBoundary.PolicyScope); + } + + [Fact] + public void TheOperationsProfileIsStandaloneWebAssemblyWithAheadOfTimeCompilationOff() + { + var project = File.ReadAllText(Path.Combine(Root, "src", "ArcForges.Web.Operations", "ArcForges.Web.Operations.csproj")); + Assert.Contains("Sdk=\"Microsoft.NET.Sdk.BlazorWebAssembly\"", project, StringComparison.Ordinal); + Assert.Contains("false", project, StringComparison.Ordinal); + } +} diff --git a/tests/ArcForges.Web.Operations.Tests/RepositoryRoot.cs b/tests/ArcForges.Web.Operations.Tests/RepositoryRoot.cs new file mode 100644 index 0000000..a089b5c --- /dev/null +++ b/tests/ArcForges.Web.Operations.Tests/RepositoryRoot.cs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Operations.Tests; + +/// Locates the repository root from the test output directory. +public static class RepositoryRoot +{ + /// Walks up from the test assembly until the repository marker files are found. + public static string Find() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "win.slnx")) + && File.Exists(Path.Combine(directory.FullName, "global.json"))) + return directory.FullName; + directory = directory.Parent; + } + throw new DirectoryNotFoundException("The repository root with win.slnx and global.json was not found."); + } +} diff --git a/tests/ArcForges.Web.Ui.Tests/WasmContentSecurityPolicyTests.cs b/tests/ArcForges.Web.Ui.Tests/WasmContentSecurityPolicyTests.cs new file mode 100644 index 0000000..fe9727d --- /dev/null +++ b/tests/ArcForges.Web.Ui.Tests/WasmContentSecurityPolicyTests.cs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The shared WebAssembly profile policy (WEB.40; P2-021 item 2): positive and negative fixtures for the exact token set. +using ArcForges.Web.Ui; +using Xunit; + +namespace ArcForges.Web.Ui.Tests; + +public sealed class WasmContentSecurityPolicyTests +{ + private const string HostPage = "
"; + + [Fact] + public void ScriptSourcesAreExactlySelfAndWasmUnsafeEvalWithoutHashesForAPageWithoutInlineScript() + { + var directives = WasmContentSecurityPolicy.ParseDirectives(WasmContentSecurityPolicy.FromHostPages([HostPage])); + Assert.Equal(new[] { "'self'", "'wasm-unsafe-eval'" }, directives["script-src"]); + } + + [Fact] + public void StyleSourcesAreSelfOnlyAndTheDefaultIsSelf() + { + var directives = WasmContentSecurityPolicy.ParseDirectives(WasmContentSecurityPolicy.FromHostPages([HostPage])); + Assert.Equal(new[] { "'self'" }, directives["style-src"]); + Assert.Equal(new[] { "'self'" }, directives["default-src"]); + Assert.Equal(new[] { "'none'" }, directives["object-src"]); + } + + [Fact] + public void NoUnsafeTokenAppearsInAnyDirective() + { + var policy = WasmContentSecurityPolicy.FromHostPages([HostPage, ""]); + Assert.DoesNotContain("'unsafe-inline'", policy, StringComparison.Ordinal); + Assert.DoesNotContain("'unsafe-eval'", policy, StringComparison.Ordinal); + } + + [Fact] + public void AnInlineScriptAddsExactlyItsSha256HashToScriptSources() + { + var page = ""; + var script = WasmContentSecurityPolicy.ParseDirectives(WasmContentSecurityPolicy.FromHostPages([page]))["script-src"]; + Assert.Equal(3, script.Count); + Assert.Equal("'self'", script[0]); + Assert.Equal("'wasm-unsafe-eval'", script[1]); + Assert.Equal( + "'sha256-" + Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes("window.start = 1;"))) + "'", + script[2]); + } + + [Fact] + public void AnAbsoluteExternalScriptIsRefused() + { + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + } + + [Fact] + public void AnUnquotedOrUnterminatedScriptSourceIsRefused() + { + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages([""])); + Assert.Throws(() => WasmContentSecurityPolicy.FromHostPages(["`); + await reseal(dir); + expectRefusal( + verify(dir, source), + "Public Site bytes differ from the regenerated Site: hello/index.html", + ); +}); + +test("a resealed policy loosened to unsafe-inline is refused by the regenerated headers", async () => { + const dir = await copyCandidate(baseline, "resealed-headers"); + const headers = path.join(dir, "assets/_headers"); + await writeFile( + headers, + (await readFile(headers, "utf8")).replace( + "script-src 'self'", + "script-src 'self' 'unsafe-inline'", + ), + ); + await reseal(dir); + expectRefusal( + verify(dir, source), + "Public Site bytes differ from the regenerated Site: _headers", + ); +}); + +test("a resealed wrangler change that enables workers.dev is refused", async () => { + const dir = await copyCandidate(baseline, "resealed-wrangler"); + const config = JSON.parse(await readFile(path.join(dir, "wrangler.json"), "utf8")); + config.workers_dev = true; + await writeFile(path.join(dir, "wrangler.json"), `${JSON.stringify(config, null, 2)}\n`); + await reseal(dir); + expectRefusal(verify(dir, source), "workers.dev must stay disabled"); +}); + +test("a resealed Worker replacement is refused by the pinned digest", async () => { + const dir = await copyCandidate(baseline, "resealed-worker"); + await writeFile( + path.join(dir, "worker/index.js"), + "export default { fetch: () => fetch('https://example.invalid') };\n", + ); + await reseal(dir); + expectRefusal(verify(dir, source), "Worker script changed from the reviewed source"); +}); + +test("a resealed licence change is refused against the repository", async () => { + const dir = await copyCandidate(baseline, "resealed-licence"); + await writeFile(path.join(dir, "assets/license.txt"), "Not the AGPL.\n"); + await reseal(dir); + expectRefusal(verify(dir, source), "Full AGPL legal text changed"); +}); + +test("a resealed identity for another source is refused", async () => { + const dir = await copyCandidate(baseline, "resealed-identity"); + const info = JSON.parse(await readFile(path.join(dir, "assets/__build-info.json"), "utf8")); + info.build.sourceCommit = "c".repeat(40); + await writeFile(path.join(dir, "assets/__build-info.json"), `${JSON.stringify(info, null, 2)}\n`); + await reseal(dir); + expectRefusal(verify(dir, source), "The build identity names another source commit"); +}); + +// The bundle tests consume the publish of ArcForges.Web.App made by the local proof (artifacts/publish/app). Without it, +// the tests are skipped rather than run against a synthetic bundle. +const publish = path.join(root, "artifacts/publish/app"); +const havePublish = await access(path.join(publish, "wwwroot/index.html")).then( + () => true, + () => false, +); + +async function buildBundle(out: string): Promise<{ status: number; output: string; name: string }> { + const result = tool(["profiles", "bundle", "--publish", publish, "--out", out]); + const name = + (await listFiles(out)).find((file) => /^web-profiles-[0-9a-f]{64}\.tar$/u.test(file)) ?? ""; + return { ...result, name }; +} + +test( + "the profile bundle keeps its name pattern, layout and reviewed headers, and verifies", + { skip: !havePublish }, + async () => { + const out = candidateDirectory("bundle"); + const first = await buildBundle(out); + assert.equal(first.status, 0, first.output); + assert.match(first.name, /^web-profiles-[0-9a-f]{64}\.tar$/u); + const archive = path.join(out, first.name); + const digest = first.name.slice("web-profiles-".length, -".tar".length); + const verified = tool(["profiles", "verify", "--bundle", archive, "--expected-digest", digest]); + assert.equal(verified.status, 0, verified.output); + const repeated = await buildBundle(candidateDirectory("bundle-repeat")); + assert.equal(repeated.name, first.name, "Bundle builds differ"); + }, +); + +test( + "a bundle with a changed byte is refused by its digest and by its content hash", + { skip: !havePublish }, + async () => { + const out = candidateDirectory("bundle-tamper"); + const built = await buildBundle(out); + const archive = path.join(out, built.name); + const bytes = await readFile(archive); + const flipped = Buffer.from(bytes); + // The first content byte of the last entry (its header is one block before it, and the archive ends with two zero + // blocks and the padding of the last file), so the header stays valid and only the content changes. + const contentByte = bytes.length - 1024 - 512; + flipped[contentByte] = (flipped[contentByte] ?? 0) ^ 0x01; + const tampered = path.join(out, "tampered.tar"); + await writeFile(tampered, flipped); + const digest = built.name.slice("web-profiles-".length, -".tar".length); + expectRefusal( + tool(["profiles", "verify", "--bundle", tampered, "--expected-digest", digest]), + "Bundle digest mismatch", + ); + expectRefusal(tool(["profiles", "verify", "--bundle", tampered]), "Content of"); + }, +); + +test( + "a truncated bundle is refused by the strict archive reader", + { skip: !havePublish }, + async () => { + const out = candidateDirectory("bundle-truncated"); + const built = await buildBundle(out); + const bytes = await readFile(path.join(out, built.name)); + const truncated = path.join(out, "truncated.tar"); + await writeFile(truncated, bytes.subarray(0, bytes.length - 1024)); + const result = tool(["profiles", "verify", "--bundle", truncated]); + assert.notEqual(result.status, 0, result.output); + assert.match(result.output, /terminator|truncated|Not a bundle archive|Data after/u); + }, +); + +test("the static Site candidate exists in the seal with its required pages", async () => { + for (const page of [ + "assets/index.html", + "assets/hello/index.html", + "assets/cloud-hello/index.html", + "assets/404.html", + ]) + await stat(path.join(baseline, page)); + const sbom = JSON.parse(await readFile(path.join(baseline, "sbom.cdx.json"), "utf8")); + assert.equal(sbom.bomFormat, "CycloneDX"); + assert(sbom.components.length > 0, "The build SBOM lists no packages"); + const runtime = JSON.parse(await readFile(path.join(baseline, "runtime-sbom.cdx.json"), "utf8")); + assert.deepEqual(runtime.components, [], "The static Site must ship no runtime dependency"); +}); + +test("the seal lists every member and no member is a link or a source map", async () => { + const manifest = JSON.parse(await readFile(path.join(baseline, "manifest.json"), "utf8")); + assert.deepEqual( + Object.keys(manifest.files).sort(), + (await listFiles(baseline)).filter((file) => file !== "manifest.json"), + ); + assert( + Object.keys(manifest.files).every((file) => !file.endsWith(".map")), + "A source map is published", + ); +}); diff --git a/tooling/candidate.ts b/tooling/candidate.ts new file mode 100644 index 0000000..4ff26d5 --- /dev/null +++ b/tooling/candidate.ts @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Node glue of the C# candidate. The C# tool (tools/ArcForges.Web.Tooling) builds, seals and verifies the public candidate +// and the Blazor profile bundle. Node produces the two inputs only Node can produce: the Worker JavaScript emitted from +// worker/index.ts by type stripping, and the build identity derived from git and the reviewed version sources. Before any +// deployment Node also re-derives the identity from the checkout, so a candidate with a foreign identity cannot ship. +// node tooling/candidate.ts worker +// node tooling/candidate.ts identity +// node tooling/candidate.ts verify [candidate-directory] +import assert from "node:assert/strict"; +import { readFile, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expectedIdentity, verifyIdentity } from "./build-identity.ts"; +import { candidate, root, run, save, workerScript } from "./project.ts"; + +export const toolLibrary = join( + root, + "tools/ArcForges.Web.Tooling/bin/Release/net10.0/ArcForges.Web.Tooling.dll", +); + +export interface SealedManifest { + version: string; + source: string; + dirty: boolean; + files: Record; +} + +/** Runs one command of the C# tool and returns its standard output. The tool is built by `dotnet build` beforehand. */ +export function tool(args: string[]): string { + return run("dotnet", [toolLibrary, ...args]); +} + +/** + * Verifies a sealed candidate before anything is uploaded: the C# verifier checks the seal, the members, the wrangler + * configuration, the pinned Worker, the regenerated Site and the reviewed inputs; then the identity is re-derived here. + */ +export async function verifyCandidate( + directory: string = candidate, + expectedSource: string | undefined = process.env.GITHUB_SHA, +): Promise { + const args = ["candidate", "verify", "--dir", directory, "--repo", root]; + if (expectedSource) args.push("--expected-source", expectedSource); + process.stdout.write(tool(args)); + const manifest = JSON.parse( + await readFile(join(directory, "manifest.json"), "utf8"), + ) as SealedManifest; + const identity = JSON.parse(await readFile(join(directory, "assets/__build-info.json"), "utf8")); + verifyIdentity(identity, manifest.version); + assert.equal( + identity.build.dirty, + manifest.dirty, + "The identity dirty flag differs from the seal", + ); + return manifest; +} + +async function main() { + const [command, first, second] = process.argv.slice(2); + switch (command) { + case "worker": { + assert(first, "Use: worker "); + await writeFile(first, await workerScript()); + break; + } + case "identity": { + assert(first && second, "Use: identity "); + await save(second, expectedIdentity(first)); + break; + } + case "verify": { + const manifest = await verifyCandidate(first ? resolve(first) : candidate); + console.log(`Candidate verified: ${manifest.version} ${manifest.source}`); + break; + } + default: + throw new Error("Use worker, identity or verify."); + } +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main(); diff --git a/tooling/cloudflare.ts b/tooling/cloudflare.ts index fc1da1d..60f3ace 100644 --- a/tooling/cloudflare.ts +++ b/tooling/cloudflare.ts @@ -4,7 +4,8 @@ import { readFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { setTimeout as delay } from "node:timers/promises"; -import { candidate, digest, json, npm, root, run, save, verify } from "./project.ts"; +import { verifyCandidate } from "./candidate.ts"; +import { candidate, digest, json, npm, root, run, save } from "./project.ts"; const statePath = join(root, "artifacts/deployment.json"); type Identity = { source: string; version: string }; @@ -83,8 +84,23 @@ export async function waitForDelivery( } throw new Error(`Public assets did not converge; no redeployment was attempted. ${lastError}`); } +// Local dry run: the same sealed-candidate verification as the deployment, then wrangler's own dry run, which bundles +// the Worker and lists the assets without any upload. It needs no credentials and changes nothing remote. +async function dryRun() { + await verifyCandidate(); + const output = run(process.execPath, [ + join(root, "node_modules/wrangler/bin/wrangler.js"), + "deploy", + "--dry-run", + "--config", + join(candidate, "wrangler.json"), + ]); + process.stdout.write(output); + console.log("Dry run only: nothing was uploaded."); +} + async function deploy() { - const manifest = await verify(); + const manifest = await verifyCandidate(); assert(!manifest.dirty, "Commit the source before deployment"); assert.equal( process.env.GITHUB_REF, @@ -158,7 +174,7 @@ async function deploy() { } async function smoke() { assert.notEqual(process.env.CI, "true", "Live browser tests are local opt-in only."); - const manifest = await verify(); + const manifest = await verifyCandidate(); const state = (await json(statePath)) as Deployment; assert.equal(state.source, manifest.source); assert.equal(state.version, manifest.version); @@ -228,5 +244,6 @@ async function smoke() { if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { if (process.argv[2] === "deploy") await deploy(); else if (process.argv[2] === "smoke") await smoke(); - else throw new Error("Use deploy or smoke."); + else if (process.argv[2] === "dry-run") await dryRun(); + else throw new Error("Use deploy, smoke or dry-run."); } diff --git a/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs b/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs new file mode 100644 index 0000000..24590b7 --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.RegularExpressions; +using System.Text.Json.Nodes; +using ArcForges.Web.Site; + +namespace ArcForges.Web.Tooling.Candidate; + +/// The inputs of one candidate build. Every value is explicit; nothing is read from the environment or the clock. +public sealed record CandidateOptions +{ + /// The repository root that holds the reviewed inputs (LICENSE, NOTICE, lock files, the policy, wrangler.json). + public required string Repository { get; init; } + + /// The 40-hex lower-case source commit of the build. + public required string SourceRef { get; init; } + + /// The release version: 0.1.0-local, or 0.1.0-ci.RUN.ATTEMPT on CI. + public required string Version { get; init; } + + /// True when the source tree had uncommitted changes. A dirty candidate is never deployed. + public bool Dirty { get; init; } + + /// The build identity document (assets/__build-info.json), produced by the Node identity emitter. + public required byte[] Identity { get; init; } + + /// The emitted Worker script (worker/index.js), produced by the Node emitter from worker/index.ts. + public required byte[] Worker { get; init; } +} + +/// Builds the sealed public candidate in memory, then writes it. Two builds of the same inputs are the same bytes. +public static partial class CandidateBuilder +{ + /// Builds every member of the candidate, including the sealed manifest, keyed by candidate path. + public static async Task> BuildAsync(CandidateOptions options, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(options); + if (!VersionPattern().IsMatch(options.Version)) + throw new ArgumentException("Invalid candidate version.", nameof(options)); + if (!SourcePattern().IsMatch(options.SourceRef)) + throw new ArgumentException("The source ref must be a 40-character lower-case commit id.", nameof(options)); + CandidateCore.RequireWorker(options.Worker); + RequireIdentity(options.Identity, options.SourceRef, options.Version, options.Dirty); + + var site = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = options.SourceRef }, cancellationToken) + .ConfigureAwait(false); + var repository = options.Repository; + var members = new SortedDictionary(StringComparer.Ordinal); + void Add(string path, byte[] bytes) + { + if (members.ContainsKey(path)) + throw new InvalidOperationException("Duplicate candidate member: " + path); + members[path] = bytes; + } + + foreach (var file in site.Files) + Add(CandidateCore.AssetsPrefix + file.Path, file.Content); + Add(CandidateCore.AssetsPrefix + "license.txt", CandidateCore.Licence(repository)); + Add(CandidateCore.AssetsPrefix + "third-party-notices.txt", CandidateCore.Notices(repository)); + Add(CandidateCore.AssetsPrefix + "source-provenance.txt", CandidateCore.SourceNotice(repository)); + Add(CandidateCore.AssetsPrefix + "__build.json", CandidateCore.BuildJson(options.Version, options.SourceRef)); + Add(CandidateCore.AssetsPrefix + "__build-info.json", options.Identity); + Add(CandidateCore.WranglerPath, CandidateCore.Json(CandidateCore.CandidateWrangler(repository))); + Add(CandidateCore.WorkerPath, options.Worker); + Add(CandidateCore.SbomPath, CandidateCore.BuildSbom(repository)); + Add(CandidateCore.RuntimeSbomPath, CandidateCore.RuntimeSbom()); + Add( + CandidateCore.ReceiptPath, + CandidateCore.Receipt(options.SourceRef, options.Version, CandidateCore.PolicySha256(repository), members)); + + var listed = new JsonObject(); + foreach (var (path, bytes) in members.OrderBy(pair => pair.Key, StringComparer.Ordinal)) + listed[path] = CandidateCore.Sha256(bytes); + var manifest = new JsonObject + { + ["schema"] = CandidateCore.ManifestSchema, + ["version"] = options.Version, + ["source"] = options.SourceRef, + ["dirty"] = options.Dirty, + ["files"] = listed, + }; + Add(CandidateCore.ManifestPath, CandidateCore.Json(manifest)); + return members; + } + + /// Writes the built members to a directory that must not exist yet (fail closed on a stale candidate). + public static void Write(string directory, IReadOnlyDictionary members) + { + var root = Path.GetFullPath(directory); + if (Directory.Exists(root) || File.Exists(root)) + throw new InvalidOperationException("Refusing to write into an existing path: " + root); + foreach (var (path, bytes) in members) + { + var target = Path.GetFullPath(Path.Combine(root, path.Replace('/', Path.DirectorySeparatorChar))); + if (!target.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.Ordinal)) + throw new InvalidOperationException("Candidate path escapes the output directory: " + path); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.WriteAllBytes(target, bytes); + } + } + + /// The identity must name this source, this version and this dirty state; a mismatch is refused before sealing. + internal static void RequireIdentity(byte[] identity, string sourceRef, string version, bool dirty) + { + var document = JsonNode.Parse(identity)?.AsObject() + ?? throw new InvalidOperationException("The build identity must be a JSON object."); + var build = document["build"]?.AsObject() + ?? throw new InvalidOperationException("The build identity has no build section."); + if (build["sourceCommit"]?.GetValue() != sourceRef) + throw new InvalidOperationException("The build identity names another source commit."); + if (build["dirty"]?.GetValue() != dirty) + throw new InvalidOperationException("The build identity names another dirty state."); + if (document["artifact"]?["version"]?.GetValue() != version) + throw new InvalidOperationException("The build identity names another version."); + } + + [GeneratedRegex(@"^0\.1\.0-(?:local|ci\.[1-9]\d*\.[1-9]\d*)$", RegexOptions.CultureInvariant)] + internal static partial Regex VersionPattern(); + + [GeneratedRegex(@"^[a-f0-9]{40}$", RegexOptions.CultureInvariant)] + internal static partial Regex SourcePattern(); +} diff --git a/tools/ArcForges.Web.Tooling/Candidate/CandidateCore.cs b/tools/ArcForges.Web.Tooling/Candidate/CandidateCore.cs new file mode 100644 index 0000000..74502ee --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Candidate/CandidateCore.cs @@ -0,0 +1,291 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace ArcForges.Web.Tooling.Candidate; + +/// +/// The rules shared by the candidate build and its verifier: the reviewed inputs, the pinned Worker, the legal and notice +/// files, the build SBOM and the sealed manifest. Every value here is derived from the repository or the Site source; no +/// timestamp or machine path is an input, so two builds of the same inputs are the same bytes. +/// +internal static class CandidateCore +{ + /// The manifest schema, the same as the React candidate's. + public const int ManifestSchema = 1; + + /// The sealed manifest path (not listed in its own file map). + public const string ManifestPath = "manifest.json"; + + /// The provenance receipt path (listed in the manifest, but its own members exclude it). + public const string ReceiptPath = "provenance/receipt.json"; + + /// The emitted canonical-host Worker (the JavaScript form of worker/index.ts). + public const string WorkerPath = "worker/index.js"; + + /// The candidate wrangler configuration. + public const string WranglerPath = "wrangler.json"; + + /// The build SBOM and the runtime SBOM of the public Site. + public const string SbomPath = "sbom.cdx.json"; + + /// The runtime SBOM path. + public const string RuntimeSbomPath = "runtime-sbom.cdx.json"; + + /// The static public assets are published under this prefix (the wrangler assets directory). + public const string AssetsPrefix = "assets/"; + + /// The full AGPL text, published as the licence file. + public const string LicencePath = "LICENSE"; + + /// The source provenance notice, published beside the licence. + public const string SourceNoticePath = "eng/provenance/NOTICE.txt"; + + /// The reviewed dependency policy, bound into the receipt. + public const string PolicyPath = "eng/policy/dependency-policy.json"; + + /// The Tailwind preflight licence text: the public stylesheet derives from Tailwind preflight. + public const string TailwindLicencePath = "third-party/tailwind.LICENSE.txt"; + + /// The per-project lock files of the Site closure, read for the build SBOM. + public static readonly string[] SiteLockPaths = + [ + "src/ArcForges.Web.Site/packages.lock.json", + "src/ArcForges.Web.Ui/packages.lock.json", + ]; + + /// + /// The SHA-256 of the Worker JavaScript emitted from worker/index.ts by Node type stripping (LF-normalised). The Node + /// emitter (tooling/candidate.ts) and this pin must agree; a changed Worker source needs a reviewed change to this value. + /// + public const string WorkerSha256 = "5020b1504e965aa428bb3bcca2bd270bfd5e4adb53dbe8fb0d3338bca401d088"; + + /// The public pages every candidate must publish (the React candidate's required pages). + public static readonly string[] RequiredPages = ["index.html", "hello/index.html", "cloud-hello/index.html", "404.html"]; + + /// The files that are not generated by the Site and that every candidate publishes beside it. + public static readonly string[] IdentityAndLegalFiles = + [ + "license.txt", + "third-party-notices.txt", + "source-provenance.txt", + "__build.json", + "__build-info.json", + ]; + + /// The members every candidate must hold, as candidate paths. + public static IReadOnlyList RequiredMembers { get; } = + [ + .. RequiredPages.Select(page => AssetsPrefix + page), + AssetsPrefix + "_headers", + .. IdentityAndLegalFiles.Select(file => AssetsPrefix + file), + SbomPath, + RuntimeSbomPath, + WranglerPath, + WorkerPath, + ReceiptPath, + ]; + + private static readonly UTF8Encoding StrictUtf8 = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + private static readonly JsonSerializerOptions Indented = new() { WriteIndented = true }; + + /// The lower-case hexadecimal SHA-256 of the bytes. + public static string Sha256(ReadOnlySpan bytes) => Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + + /// Decodes strict UTF-8 and normalises CRLF to LF, the bytes a browser or Node text reader sees. + public static byte[] Lf(ReadOnlySpan bytes) => + Encoding.UTF8.GetBytes(StrictUtf8.GetString(bytes).Replace("\r\n", "\n", StringComparison.Ordinal)); + + /// Serialises a JSON document with two-space indentation and one trailing line feed. + public static byte[] Json(JsonNode node) => + Encoding.UTF8.GetBytes(JsonSerializer.Serialize(node, Indented) + "\n"); + + /// Reads one repository file. The path is a fixed constant of this tool, never user input. + public static byte[] ReadRepo(string repository, string relativePath) + { + var path = Path.Combine(repository, relativePath.Replace('/', Path.DirectorySeparatorChar)); + if (!File.Exists(path)) + throw new InvalidOperationException("Missing reviewed repository input: " + relativePath); + return File.ReadAllBytes(path); + } + + /// Reads every regular file under a directory as relative '/' paths. Links and reparse points are refused. + public static SortedDictionary ReadTree(string directory) + { + var files = new SortedDictionary(StringComparer.Ordinal); + var root = Path.GetFullPath(directory); + void Visit(string current) + { + foreach (var entry in Directory.EnumerateFileSystemEntries(current)) + { + var attributes = File.GetAttributes(entry); + if ((attributes & FileAttributes.ReparsePoint) != 0) + throw new InvalidOperationException("Links are not allowed in a candidate: " + entry); + if ((attributes & FileAttributes.Directory) != 0) + { + Visit(entry); + continue; + } + var relative = Path.GetRelativePath(root, entry).Replace(Path.DirectorySeparatorChar, '/'); + files[relative] = File.ReadAllBytes(entry); + } + } + Visit(root); + return files; + } + + /// + /// The published third-party notices. The static Site ships only Tailwind preflight-derived CSS from third-party + /// material, so the notice text covers exactly that licence; a change to the shipped third-party content changes it. + /// + public static byte[] Notices(string repository) + { + var tailwind = Lf(ReadRepo(repository, TailwindLicencePath)); + var text = new StringBuilder(); + text.Append("ArcForges Web third-party notices\n"); + text.Append("Generated for the C# static Site candidate. The public Site publishes no JavaScript and no runtime\n"); + text.Append("dependency. Its stylesheet derives from Tailwind preflight (MIT); every other published file is first-party.\n"); + text.Append("\n===== Tailwind CSS preflight (MIT) =====\n"); + text.Append(Encoding.UTF8.GetString(tailwind)); + return Encoding.UTF8.GetBytes(text.ToString()); + } + + /// The published licence file: the full AGPL text with LF line endings. + public static byte[] Licence(string repository) => Lf(ReadRepo(repository, LicencePath)); + + /// The published source provenance notice with LF line endings. + public static byte[] SourceNotice(string repository) => Lf(ReadRepo(repository, SourceNoticePath)); + + /// The build identity: the candidate's __build.json, which the deploy identity poll reads. + public static byte[] BuildJson(string version, string source) => + Json(new JsonObject { ["version"] = version, ["source"] = source }); + + /// + /// The build SBOM of the public Site: every non-project NuGet package of the Site and Ui locks, with its exact + /// version, its content hash and the licence the reviewed closure records for it. A package with no reviewed licence + /// fails the build. + /// + public static byte[] BuildSbom(string repository) + { + var closure = ReviewedLicences(repository); + var components = new SortedDictionary(StringComparer.Ordinal); + foreach (var lockPath in SiteLockPaths) + { + var document = JsonNode.Parse(ReadRepo(repository, lockPath))?.AsObject() + ?? throw new InvalidOperationException("Invalid lock file: " + lockPath); + var frameworks = document["dependencies"]?.AsObject() + ?? throw new InvalidOperationException("A lock file without dependencies: " + lockPath); + foreach (var (_, dependencies) in frameworks) + { + foreach (var (name, value) in dependencies!.AsObject()) + { + var entry = value!.AsObject(); + if (entry["type"]?.GetValue() == "Project") + continue; + var version = entry["resolved"]?.GetValue() + ?? throw new InvalidOperationException("A package without a resolved version: " + name); + var contentHash = entry["contentHash"]?.GetValue() + ?? throw new InvalidOperationException("A package without a content hash: " + name); + var purl = $"pkg:nuget/{name}@{version}"; + if (components.ContainsKey(purl)) + continue; + var licenceKey = (name + "/" + version).ToLowerInvariant(); + if (!closure.TryGetValue(licenceKey, out var licence)) + throw new InvalidOperationException("No reviewed licence for " + purl); + components[purl] = new JsonObject + { + ["type"] = "library", + ["bom-ref"] = purl, + ["name"] = name, + ["version"] = version, + ["purl"] = purl, + ["licenses"] = new JsonArray(new JsonObject { ["license"] = new JsonObject { ["id"] = licence } }), + ["hashes"] = new JsonArray(new JsonObject + { + ["alg"] = "SHA-512", + ["content"] = Convert.ToHexString(Convert.FromBase64String(contentHash)).ToLowerInvariant(), + }), + }; + } + } + } + var bom = new JsonObject + { + ["bomFormat"] = "CycloneDX", + ["specVersion"] = "1.5", + ["version"] = 1, + ["metadata"] = new JsonObject + { + ["component"] = new JsonObject { ["type"] = "application", ["name"] = "arcforges-web-site", ["bom-ref"] = "arcforges-web-site" }, + }, + ["components"] = new JsonArray(components.Values.Select(component => (JsonNode)component).ToArray()), + }; + return Json(bom); + } + + /// The runtime SBOM: the static Site ships no runtime dependency, so its component list is empty. + public static byte[] RuntimeSbom() => + Json(new JsonObject + { + ["bomFormat"] = "CycloneDX", + ["specVersion"] = "1.5", + ["version"] = 1, + ["components"] = new JsonArray(), + }); + + /// The candidate wrangler configuration: the reviewed root configuration with the candidate's assets directory. + public static JsonObject CandidateWrangler(string repository) + { + var root = JsonNode.Parse(ReadRepo(repository, WranglerPath))?.AsObject() + ?? throw new InvalidOperationException("Invalid wrangler.json"); + root.Remove("$schema"); + var assets = root["assets"]?.AsObject() + ?? throw new InvalidOperationException("wrangler.json has no assets section"); + assets["directory"] = "./assets"; + return root; + } + + /// The provenance receipt: the source, the version, the reviewed policy digest and every member's digest. + public static byte[] Receipt(string source, string version, string policySha256, IReadOnlyDictionary members) + { + var listed = new JsonObject(); + foreach (var (path, bytes) in members.OrderBy(pair => pair.Key, StringComparer.Ordinal)) + if (path != ManifestPath && path != ReceiptPath) + listed[path] = Sha256(bytes); + return Json(new JsonObject + { + ["schemaVersion"] = 1, + ["repository"] = "Web", + ["source"] = source, + ["version"] = version, + ["policySha256"] = policySha256, + ["members"] = listed, + }); + } + + /// The reviewed dependency policy digest, LF-normalised, that the receipt binds. + public static string PolicySha256(string repository) => Sha256(Lf(ReadRepo(repository, PolicyPath))); + + /// The Worker digest check: the emitted script must be exactly the pinned bytes. + public static void RequireWorker(ReadOnlySpan bytes) + { + if (Sha256(bytes) != WorkerSha256) + throw new InvalidOperationException("Worker script changed from the reviewed source (worker/index.ts)."); + } + + /// The licence key to SPDX licence map of the reviewed NuGet closure. + private static Dictionary ReviewedLicences(string repository) + { + var policy = JsonNode.Parse(ReadRepo(repository, PolicyPath))?.AsObject() + ?? throw new InvalidOperationException("Invalid dependency policy"); + var closure = policy["nugetClosure"]?.AsObject() + ?? throw new InvalidOperationException("The dependency policy has no nugetClosure"); + var result = new Dictionary(StringComparer.Ordinal); + foreach (var (key, value) in closure) + result[key.ToLowerInvariant()] = value!["licence"]?.GetValue() + ?? throw new InvalidOperationException("No licence for " + key); + return result; + } +} diff --git a/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs b/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs new file mode 100644 index 0000000..7180f89 --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text; +using System.Text.RegularExpressions; +using System.Text.Json.Nodes; +using ArcForges.Web.Site; + +namespace ArcForges.Web.Tooling.Candidate; + +/// +/// Verifies a sealed public candidate before anything is uploaded. It checks the seal, the exact member set, the wrangler +/// configuration, the pinned Worker, the identity, the public Site (by regenerating it from the sealed source and comparing +/// every byte), the static-page graph (no script without a hash, no event handlers, no frames, every stylesheet present), +/// and, when the repository is given, the licence, notice, SBOM and provenance receipt against the reviewed inputs. +/// +public static partial class CandidateVerifier +{ + private static readonly string[] ManifestKeys = ["dirty", "files", "schema", "source", "version"]; + private static readonly string[] WranglerKeys = ["assets", "compatibility_date", "main", "name", "no_bundle", "preview_urls", "workers_dev"]; + private static readonly string[] ForbiddenAssetPatterns = ["/.env", "/.dev.vars", "/node_modules", "/server", "/.vite", "/__spa-fallback"]; + + /// Verifies the candidate at . Throws at the first failed rule. + /// The verified manifest's version, source and member count. + public static async Task VerifyAsync( + string directory, + string? repository, + string? expectedSource, + CancellationToken cancellationToken = default) + { + var tree = CandidateCore.ReadTree(directory); + if (!tree.TryGetValue(CandidateCore.ManifestPath, out var manifestBytes)) + throw Fail("The candidate has no manifest."); + var manifest = JsonNode.Parse(manifestBytes)?.AsObject() ?? throw Fail("The manifest is not a JSON object."); + Require(manifest.Select(pair => pair.Key).OrderBy(key => key, StringComparer.Ordinal).SequenceEqual(ManifestKeys), + "The manifest keys changed."); + Require(manifest["schema"]?.GetValue() == CandidateCore.ManifestSchema, "Unsupported manifest schema."); + var version = manifest["version"]?.GetValue() ?? throw Fail("The manifest has no version."); + var source = manifest["source"]?.GetValue() ?? throw Fail("The manifest has no source."); + var dirty = manifest["dirty"]?.GetValue() ?? throw Fail("The manifest has no dirty flag."); + Require(CandidateBuilder.VersionPattern().IsMatch(version), "Invalid candidate version."); + Require(CandidateBuilder.SourcePattern().IsMatch(source), "Invalid source revision."); + if (expectedSource is not null) + Require(source == expectedSource, "Candidate source does not match this run."); + + var listed = manifest["files"]?.AsObject() ?? throw Fail("The manifest has no file map."); + var listedPaths = listed.Select(pair => pair.Key).OrderBy(path => path, StringComparer.Ordinal).ToArray(); + var actualPaths = tree.Keys.Where(path => path != CandidateCore.ManifestPath).ToArray(); + Require(listedPaths.SequenceEqual(actualPaths), "Candidate file set changed."); + foreach (var (path, hash) in listed) + Require(CandidateCore.Sha256(tree[path]) == hash?.GetValue(), "Candidate changed: " + path); + + foreach (var path in actualPaths.Where(path => path.StartsWith(CandidateCore.AssetsPrefix, StringComparison.Ordinal))) + { + Require(!path.EndsWith(".map", StringComparison.Ordinal), "Source map in public assets: " + path); + foreach (var pattern in ForbiddenAssetPatterns) + Require(!("/" + path).Contains(pattern, StringComparison.Ordinal), "Private or build file in public assets: " + path); + } + foreach (var required in CandidateCore.RequiredMembers) + Require(tree.ContainsKey(required), "Missing required file: " + required); + + VerifyWrangler(tree, repository); + CandidateCore.RequireWorker(tree[CandidateCore.WorkerPath]); + VerifyIdentity(tree, source, version, dirty); + VerifyReceipt(tree, source, version, repository); + await VerifySiteAsync(tree, source, cancellationToken).ConfigureAwait(false); + VerifyStaticGraph(tree); + if (repository is not null) + VerifyReviewedInputs(tree, repository); + return new CandidateSummary(version, source, dirty, tree.Count - 1); + } + + private static void VerifyWrangler(SortedDictionary tree, string? repository) + { + var config = JsonNode.Parse(tree[CandidateCore.WranglerPath])?.AsObject() ?? throw Fail("Invalid wrangler.json."); + Require(config.Select(pair => pair.Key).OrderBy(key => key, StringComparer.Ordinal).SequenceEqual(WranglerKeys), + "Deployment configuration keys changed."); + Require(config["name"]?.GetValue() == "arcforges-web", "Deployment name changed."); + Require(config["main"]?.GetValue() == "./worker/index.js", "Deployment entry changed."); + Require(config["no_bundle"]?.GetValue() == true, "Deployment must not bundle."); + Require(config["preview_urls"]?.GetValue() == false, "Preview URLs must stay disabled."); + Require(config["workers_dev"]?.GetValue() == false, "workers.dev must stay disabled."); + var expectedAssets = JsonNode.Parse(""" + {"directory":"./assets","binding":"ASSETS","run_worker_first":true,"html_handling":"auto-trailing-slash","not_found_handling":"404-page"} + """); + Require(JsonNode.DeepEquals(config["assets"], expectedAssets), "Deployment asset routing changed."); + if (repository is not null) + Require(JsonNode.DeepEquals(config, CandidateCore.CandidateWrangler(repository)), "Deployment configuration changed."); + } + + private static void VerifyIdentity(SortedDictionary tree, string source, string version, bool dirty) + { + var identity = JsonNode.Parse(tree[CandidateCore.AssetsPrefix + "__build.json"])?.AsObject() + ?? throw Fail("Invalid __build.json."); + Require(identity["source"]?.GetValue() == source && identity["version"]?.GetValue() == version, + "The build identity does not match the manifest."); + var info = JsonNode.Parse(tree[CandidateCore.AssetsPrefix + "__build-info.json"])?.AsObject() + ?? throw Fail("Invalid __build-info.json."); + CandidateBuilder.RequireIdentity(tree[CandidateCore.AssetsPrefix + "__build-info.json"], source, version, dirty); + Require(info["build"]?["sourceCommit"]?.GetValue() == source, "The build info names another source commit."); + } + + private static void VerifyReceipt(SortedDictionary tree, string source, string version, string? repository) + { + var receipt = JsonNode.Parse(tree[CandidateCore.ReceiptPath])?.AsObject() ?? throw Fail("Invalid receipt."); + Require(receipt["schemaVersion"]?.GetValue() == 1 && receipt["repository"]?.GetValue() == "Web", + "Unsupported receipt."); + Require(receipt["source"]?.GetValue() == source && receipt["version"]?.GetValue() == version, + "The receipt does not match the manifest."); + var members = new JsonObject(); + foreach (var (path, bytes) in tree.Where(pair => pair.Key != CandidateCore.ManifestPath && pair.Key != CandidateCore.ReceiptPath)) + members[path] = CandidateCore.Sha256(bytes); + Require(JsonNode.DeepEquals(receipt["members"], members), "The receipt members differ from the sealed files."); + if (repository is not null) + Require(receipt["policySha256"]?.GetValue() == CandidateCore.PolicySha256(repository), + "The receipt binds another dependency policy."); + } + + private static async Task VerifySiteAsync(SortedDictionary tree, string source, CancellationToken cancellationToken) + { + // The public Site is regenerated from the sealed source and compared byte for byte, so a changed page, header, + // policy or stylesheet cannot ship under a valid seal. + var site = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = source }, cancellationToken).ConfigureAwait(false); + var expected = site.Files.Select(file => CandidateCore.AssetsPrefix + file.Path).ToHashSet(StringComparer.Ordinal); + foreach (var identity in CandidateCore.IdentityAndLegalFiles) + expected.Add(CandidateCore.AssetsPrefix + identity); + var actual = tree.Keys.Where(path => path.StartsWith(CandidateCore.AssetsPrefix, StringComparison.Ordinal)).ToHashSet(StringComparer.Ordinal); + Require(expected.SetEquals(actual), "The public assets differ from the reviewed Site inventory."); + foreach (var file in site.Files) + Require(tree[CandidateCore.AssetsPrefix + file.Path].SequenceEqual(file.Content), + "Public Site bytes differ from the regenerated Site: " + file.Path); + } + + private static void VerifyStaticGraph(SortedDictionary tree) + { + // The public pages are static: no external script, no event-handler attribute, no frame or embedded object, and + // every inline script is covered by the page's policy hash. Each stylesheet must be a published asset. + var headers = Encoding.UTF8.GetString(tree[CandidateCore.AssetsPrefix + "_headers"]); + var policyLine = headers.Split('\n').FirstOrDefault(line => line.TrimStart().StartsWith("Content-Security-Policy:", StringComparison.Ordinal)) + ?? throw Fail("The headers carry no Content-Security-Policy."); + var policy = policyLine.Trim()["Content-Security-Policy:".Length..].Trim(); + Require(policy.Contains("script-src 'self'", StringComparison.Ordinal), "The policy has no script-src 'self'."); + foreach (var forbidden in new[] { "unsafe-inline", "unsafe-eval", "wasm-unsafe-eval" }) + Require(!policy.Contains(forbidden, StringComparison.Ordinal), "The public policy allows " + forbidden + "."); + var hashes = policy.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Where(token => token.StartsWith("'sha256-", StringComparison.Ordinal)) + .Select(token => token.Trim('\'')["sha256-".Length..]) + .ToHashSet(StringComparer.Ordinal); + + foreach (var page in CandidateCore.RequiredPages) + { + var path = CandidateCore.AssetsPrefix + page; + var html = Encoding.UTF8.GetString(tree[path]); + Require(!ExternalScript().IsMatch(html), "External script on a public page: " + page); + Require(!EventAttribute().IsMatch(html), "Event-handler attribute on a public page: " + page); + Require(!FrameElement().IsMatch(html), "Frame or embedded object on a public page: " + page); + foreach (Match inline in InlineScript().Matches(html)) + { + var body = inline.Groups["body"].Value; + if (body.Length == 0) + continue; + var digest = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(body))); + Require(hashes.Contains(digest), "Inline script without a policy hash on " + page); + } + var stylesheets = 0; + foreach (Match tag in LinkTag().Matches(html)) + { + if (!StylesheetRel().IsMatch(tag.Value)) + continue; + stylesheets++; + var href = HrefAttribute().Match(tag.Value); + Require(href.Success, "Stylesheet without an href on " + page); + Require(tree.ContainsKey(CandidateCore.AssetsPrefix + href.Groups["href"].Value.TrimStart('/')), + "Stylesheet not published: " + href.Groups["href"].Value); + } + Require(stylesheets == 1, "Each public page links exactly one published stylesheet: " + page); + } + } + + private static void VerifyReviewedInputs(SortedDictionary tree, string repository) + { + Require(tree[CandidateCore.AssetsPrefix + "license.txt"].SequenceEqual(CandidateCore.Licence(repository)), + "Full AGPL legal text changed."); + Require(tree[CandidateCore.AssetsPrefix + "third-party-notices.txt"].SequenceEqual(CandidateCore.Notices(repository)), + "Required third-party notices changed."); + Require(tree[CandidateCore.AssetsPrefix + "source-provenance.txt"].SequenceEqual(CandidateCore.SourceNotice(repository)), + "Source notice changed."); + Require(tree[CandidateCore.SbomPath].SequenceEqual(CandidateCore.BuildSbom(repository)), + "Build SBOM identities or edges changed."); + Require(tree[CandidateCore.RuntimeSbomPath].SequenceEqual(CandidateCore.RuntimeSbom()), + "Runtime SBOM changed."); + } + + private static InvalidOperationException Fail(string message) => new(message); + + private static void Require(bool condition, string message) + { + if (!condition) + throw Fail(message); + } + + [GeneratedRegex(@"]*\bsrc\s*=", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex ExternalScript(); + + [GeneratedRegex(@"\son[a-z]+\s*=", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex EventAttribute(); + + [GeneratedRegex(@"<(?:iframe|object|embed|frame|applet)\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex FrameElement(); + + [GeneratedRegex(@"]*>(?[\s\S]*?)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex InlineScript(); + + [GeneratedRegex(@"]*>", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex LinkTag(); + + [GeneratedRegex(@"\brel\s*=\s*""[^""]*\bstylesheet\b[^""]*""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex StylesheetRel(); + + [GeneratedRegex(@"\bhref\s*=\s*""(?[^""]+)""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex HrefAttribute(); +} + +/// The verified facts of a candidate. +public sealed record CandidateSummary(string Version, string Source, bool Dirty, int Members); diff --git a/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs b/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs new file mode 100644 index 0000000..1afeba4 --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using ArcForges.Web.Tooling.Candidate; +using ArcForges.Web.Ui; + +namespace ArcForges.Web.Tooling.Profiles; + +/// The result of one bundle build: the archive name is the digest of the archive bytes. +public sealed record ProfileBundleResult(string Name, string Digest, byte[] Archive, int Members); + +/// The facts of a verified bundle. +public sealed record ProfileBundleSummary(string Name, string Digest, int Members); + +/// +/// The Web profile bundle that the Cloud proof deployment consumes: the Blazor WebAssembly standalone application +/// (ArcForges.Web.App, published once and serving the Account and Chat routes) as one deterministic, digest-named archive. +/// The archive keeps the React bundle's name pattern (web-profiles-<sha256>.tar), its first entry +/// (manifest.json), its root _headers with one Content-Security-Policy per profile path, and its +/// <profile>/index.html pages. Blazor's framework files are root-relative under the page's base href="/", +/// so the published tree is served at its own root paths and assets/ is empty in this bundle. +/// +public static partial class ProfileBundle +{ + /// The manifest schema of the bundle. + public const int Schema = 1; + + /// The first entry of the archive. + public const string ManifestPath = "manifest.json"; + + /// The Cloudflare headers file at the archive root. + public const string HeadersPath = "_headers"; + + /// The application shell. It is the source of every profile page and is not served at the root. + public const string ShellPath = "index.html"; + + /// The profile paths. The one published application serves both routes. + public static readonly string[] Profiles = ["account", "chat"]; + + /// The Cloudflare header line budget each profile policy must fit within. + public const int HeaderLineBudget = 1800; + + /// The bundle file name for a digest. + public static string BundleName(string digest) => "web-profiles-" + digest + ".tar"; + + /// Builds the bundle from a publish directory (the output of dotnet publish of ArcForges.Web.App). + public static ProfileBundleResult Build(string publishDirectory) + { + var publish = Path.GetFullPath(publishDirectory); + var wwwroot = Path.Combine(publish, "wwwroot"); + if (!Directory.Exists(wwwroot)) + throw new InvalidOperationException("The publish output has no wwwroot directory."); + var tree = CandidateCore.ReadTree(wwwroot); + if (!tree.TryGetValue(ShellPath, out var shell)) + throw new InvalidOperationException("The publish output has no application shell."); + VerifyStaticGraph(publish, tree); + + var page = CandidateCore.Lf(shell); + var pageText = Encoding.UTF8.GetString(page); + var served = new SortedDictionary(StringComparer.Ordinal); + foreach (var (path, bytes) in tree) + { + if (path == ShellPath) + continue; + if (path == HeadersPath || path == ManifestPath || Profiles.Any(profile => path.StartsWith(profile + "/", StringComparison.Ordinal))) + throw new InvalidOperationException("The publish output uses a reserved bundle path: " + path); + served[path] = bytes; + } + var policies = new SortedDictionary(StringComparer.Ordinal); + foreach (var profile in Profiles) + { + served[profile + "/index.html"] = page; + policies[profile] = WasmContentSecurityPolicy.FromHostPages([pageText]); + } + served[HeadersPath] = Encoding.UTF8.GetBytes(HeadersText(policies)); + + var buildDigest = BuildDigest(tree); + var profileRows = new JsonObject(); + foreach (var profile in Profiles) + profileRows[profile] = new JsonObject + { + ["page"] = profile + "/index.html", + ["buildDigest"] = buildDigest, + ["csp"] = policies[profile], + }; + var fileRows = new JsonObject(); + foreach (var (path, bytes) in served) + fileRows[path] = new JsonObject { ["sha256"] = CandidateCore.Sha256(bytes), ["bytes"] = bytes.Length }; + var manifest = new JsonObject + { + ["schema"] = Schema, + ["profiles"] = profileRows, + ["files"] = fileRows, + }; + + var entries = new List { new(ManifestPath, CandidateCore.Json(manifest)) }; + entries.AddRange(served.Select(pair => new TarArchive.Entry(pair.Key, pair.Value))); + var archive = TarArchive.Write(entries); + var digest = CandidateCore.Sha256(archive); + return new ProfileBundleResult(BundleName(digest), digest, archive, entries.Count); + } + + /// + /// Writes the bundle into a directory. Earlier bundles of this name pattern are removed first, so the directory holds + /// exactly one bundle afterwards. + /// + public static void Write(string outputDirectory, ProfileBundleResult bundle) + { + var directory = Path.GetFullPath(outputDirectory); + Directory.CreateDirectory(directory); + foreach (var name in Directory.EnumerateFiles(directory)) + if (BundleFilePattern().IsMatch(Path.GetFileName(name))) + File.Delete(name); + File.WriteAllBytes(Path.Combine(directory, bundle.Name), bundle.Archive); + } + + /// + /// Verifies a bundle archive: its name, its strict tar layout, its manifest and every member's digest, the profile + /// pages, the policy of each profile path against its page, and the exact headers file. + /// + public static ProfileBundleSummary Verify(byte[] archive, string? expectedDigest = null) + { + ArgumentNullException.ThrowIfNull(archive); + var digest = CandidateCore.Sha256(archive); + if (expectedDigest is not null) + Require(digest == expectedDigest, "Bundle digest mismatch."); + var entries = TarArchive.Read(archive); + Require(entries.Count > 0 && entries[0].Path == ManifestPath, "The manifest must be the first entry."); + var manifest = JsonNode.Parse(entries[0].Bytes)?.AsObject() ?? throw new InvalidOperationException("Invalid bundle manifest."); + Require(manifest["schema"]?.GetValue() == Schema, "Unsupported bundle schema."); + var profileRows = manifest["profiles"]?.AsObject() ?? throw new InvalidOperationException("The manifest has no profiles."); + Require(profileRows.Select(pair => pair.Key).OrderBy(name => name, StringComparer.Ordinal).SequenceEqual(Profiles.OrderBy(name => name, StringComparer.Ordinal)), + "The bundle does not hold exactly the reviewed profiles."); + var fileRows = manifest["files"]?.AsObject() ?? throw new InvalidOperationException("The manifest has no files."); + var rest = entries.Skip(1).ToArray(); + Require(rest.Select(entry => entry.Path).SequenceEqual(fileRows.Select(pair => pair.Key)), + "The archive does not hold exactly the manifest's files in order."); + foreach (var entry in rest) + { + var row = fileRows[entry.Path]?.AsObject() ?? throw new InvalidOperationException("Unlisted file " + entry.Path); + Require(row["bytes"]?.GetValue() == entry.Bytes.Length, "Size of " + entry.Path); + Require(row["sha256"]?.GetValue() == CandidateCore.Sha256(entry.Bytes), "Content of " + entry.Path); + } + Require(rest.All(entry => entry.Path != ShellPath), "The application shell must not be served at the root."); + + var members = rest.ToDictionary(entry => entry.Path, entry => entry.Bytes, StringComparer.Ordinal); + var policies = new SortedDictionary(StringComparer.Ordinal); + foreach (var profile in Profiles) + { + var pagePath = profile + "/index.html"; + Require(members.TryGetValue(pagePath, out var page), "Missing profile page " + pagePath); + var pageText = Encoding.UTF8.GetString(page!); + Require(pageText.Contains("
() == pagePath, "The profile row names another page."); + Require(row["csp"]?.GetValue() == policy, "The profile policy differs from its page."); + policies[profile] = policy; + } + Require(members.TryGetValue(HeadersPath, out var headers), "Missing _headers."); + Require(Encoding.UTF8.GetString(headers!) == HeadersText(policies), "The headers file differs from the reviewed rules."); + return new ProfileBundleSummary(BundleName(digest), digest, entries.Count); + } + + /// The exact headers file: the security headers, one policy per profile path, and the assets cache rule. + public static string HeadersText(IReadOnlyDictionary policies) + { + var lines = new List + { + "/*", + " X-Content-Type-Options: nosniff", + " Referrer-Policy: no-referrer", + " Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()", + " X-Robots-Tag: noindex, nofollow", + }; + foreach (var profile in Profiles) + { + Require(policies.TryGetValue(profile, out var policy), "No policy for " + profile); + Require(policy!.Length < HeaderLineBudget, "The " + profile + " policy exceeds the header line budget."); + lines.Add("/" + profile + "/*"); + lines.Add(" Content-Security-Policy: " + policy); + lines.Add(" X-Frame-Options: DENY"); + lines.Add(" Cache-Control: public, no-cache, no-transform"); + } + lines.Add("/assets/*"); + lines.Add(" ! Cache-Control"); + lines.Add(" Cache-Control: public, max-age=31536000, immutable, no-transform"); + return string.Join('\n', lines) + "\n"; + } + + /// + /// The browser-graph check of the Blazor output: the publish's static web assets manifest names every served file + /// with the integrity of its original resource. Each file must decode (Brotli or gzip variants included) to that + /// integrity, and every published file must be named by the manifest. A changed, missing or unlisted file fails. + /// + private static void VerifyStaticGraph(string publish, SortedDictionary tree) + { + var manifests = Directory.GetFiles(publish, "*.staticwebassets.endpoints.json", SearchOption.TopDirectoryOnly); + Require(manifests.Length == 1, "The publish output needs exactly one static web assets manifest."); + var document = JsonNode.Parse(File.ReadAllBytes(manifests[0]))?.AsObject() + ?? throw new InvalidOperationException("Invalid static web assets manifest."); + Require(document["Version"]?.GetValue() == 1, "Unsupported static web assets manifest version."); + var endpoints = document["Endpoints"]?.AsArray() ?? throw new InvalidOperationException("No endpoints in the manifest."); + var covered = new HashSet(StringComparer.Ordinal); + foreach (var node in endpoints) + { + var endpoint = node?.AsObject() ?? throw new InvalidOperationException("Invalid endpoint."); + var assetFile = endpoint["AssetFile"]?.GetValue() ?? throw new InvalidOperationException("An endpoint has no asset file."); + Require(tree.TryGetValue(assetFile, out var stored), "The manifest names a missing file: " + assetFile); + covered.Add(assetFile); + var integrity = endpoint["EndpointProperties"]?.AsArray() + .Select(property => property?.AsObject()) + .FirstOrDefault(property => property?["Name"]?.GetValue() == "integrity")?["Value"]?.GetValue() + ?? throw new InvalidOperationException("An endpoint has no integrity: " + assetFile); + // An uncompressed file must carry the integrity of its own original bytes. A compressed variant carries either + // the original resource's integrity or the integrity of its own compressed bytes (the ETag form the publish + // records); both are bound to the served bytes, and the original is always recovered by decoding. + var plain = assetFile.EndsWith(".br", StringComparison.Ordinal) ? Decompress(stored!, brotli: true) + : assetFile.EndsWith(".gz", StringComparison.Ordinal) ? Decompress(stored!, brotli: false) + : stored!; + var original = "sha256-" + Convert.ToBase64String(SHA256.HashData(plain)); + var own = "sha256-" + Convert.ToBase64String(SHA256.HashData(stored!)); + if (plain == stored) + Require(integrity == original, "Served bytes differ from the build's integrity: " + assetFile); + else + Require(integrity == original || integrity == own, "Served bytes differ from the build's integrity: " + assetFile); + } + Require(covered.SetEquals(tree.Keys), "A published file is not named by the build's static web assets manifest."); + } + + private static byte[] Decompress(byte[] stored, bool brotli) + { + using var source = new MemoryStream(stored); + using var output = new MemoryStream(); + Stream reader = brotli ? new BrotliStream(source, CompressionMode.Decompress) : new GZipStream(source, CompressionMode.Decompress); + using (reader) + reader.CopyTo(output); + return output.ToArray(); + } + + private static string BuildDigest(SortedDictionary tree) + { + var text = new StringBuilder(); + foreach (var (path, bytes) in tree) + text.Append(path).Append('\n').Append(CandidateCore.Sha256(bytes)).Append('\n'); + return CandidateCore.Sha256(Encoding.UTF8.GetBytes(text.ToString())); + } + + private static void Require(bool condition, string message) + { + if (!condition) + throw new InvalidOperationException(message); + } + + [GeneratedRegex(@"^web-profiles-[0-9a-f]{64}\.tar$", RegexOptions.CultureInvariant)] + private static partial Regex BundleFilePattern(); +} diff --git a/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs b/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs new file mode 100644 index 0000000..e8f4a9b --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text; + +namespace ArcForges.Web.Tooling.Profiles; + +/// +/// The deterministic archive of the profile bundle: plain POSIX ustar regular files in a fixed order, zero times and +/// owners, no compression. Equal content is the same bytes, so the bundle's name is the digest of its own archive. The +/// reader accepts exactly what the writer writes and refuses anything else. +/// +internal static class TarArchive +{ + private const int Block = 512; + private const int NameLength = 100; + + /// One regular file of the archive. + public sealed record Entry(string Path, byte[] Bytes); + + /// Writes the entries in the given order and returns the archive bytes. + public static byte[] Write(IReadOnlyList entries) + { + var seen = new HashSet(StringComparer.Ordinal); + using var output = new MemoryStream(); + foreach (var entry in entries) + { + if (!seen.Add(entry.Path)) + throw new InvalidOperationException("Duplicate entry " + entry.Path); + output.Write(Header(entry.Path, entry.Bytes.LongLength)); + output.Write(entry.Bytes); + var padding = (Block - entry.Bytes.Length % Block) % Block; + output.Write(new byte[padding]); + } + output.Write(new byte[Block * 2]); + return output.ToArray(); + } + + /// Reads exactly what writes. Any other header, size or padding is refused. + public static IReadOnlyList Read(byte[] archive) + { + ArgumentNullException.ThrowIfNull(archive); + if (archive.Length % Block != 0 || archive.Length < Block * 2) + throw new InvalidOperationException("Not a bundle archive."); + var entries = new List(); + var offset = 0; + while (true) + { + if (offset + Block > archive.Length) + throw new InvalidOperationException("The archive ends without its terminator."); + var head = archive.AsSpan(offset, Block); + if (head.IndexOfAnyExcept((byte)0) < 0) + { + if (offset + Block * 2 != archive.Length) + throw new InvalidOperationException("Data after the archive terminator."); + if (archive.AsSpan(offset + Block, Block).IndexOfAnyExcept((byte)0) >= 0) + throw new InvalidOperationException("The archive terminator is not zero."); + return entries; + } + var end = head[..NameLength].IndexOf((byte)0); + var nameBytes = end < 0 ? head[..NameLength] : head[..end]; + var path = Encoding.ASCII.GetString(nameBytes); + var sizeField = Encoding.ASCII.GetString(head.Slice(124, 11)).TrimEnd('\0'); + if (!IsOctal(sizeField)) + throw new InvalidOperationException("Bad entry size."); + var size = Convert.ToInt64(sizeField, 8); + if (size < 0 || size > int.MaxValue) + throw new InvalidOperationException("Bad entry size."); + var expected = Header(path, size); + if (!head.SequenceEqual(expected.AsSpan())) + throw new InvalidOperationException("Entry " + path + " is not a plain regular file header."); + var start = offset + Block; + if (start + size > archive.Length) + throw new InvalidOperationException("Entry " + path + " is truncated."); + var bytes = archive.AsSpan(start, (int)size).ToArray(); + var padded = (int)((size + Block - 1) / Block * Block); + if (start + padded > archive.Length) + throw new InvalidOperationException("Entry " + path + " has no padding."); + for (var index = start + (int)size; index < start + padded; index++) + if (archive[index] != 0) + throw new InvalidOperationException("Entry " + path + " has non-zero padding."); + entries.Add(new Entry(path, bytes)); + offset = start + padded; + } + } + + private static bool IsOctal(string text) => text.Length > 0 && text.All(character => character is >= '0' and <= '7'); + + private static byte[] Header(string path, long size) + { + if (!System.Text.RegularExpressions.Regex.IsMatch(path, @"^[A-Za-z0-9_][A-Za-z0-9._/-]*$", System.Text.RegularExpressions.RegexOptions.CultureInvariant) + || path.Contains("..", StringComparison.Ordinal) || path.Contains("//", StringComparison.Ordinal) || path.EndsWith('/')) + throw new InvalidOperationException("Bad archive path: " + path); + var name = Encoding.ASCII.GetBytes(path); + if (name.Length > NameLength) + throw new InvalidOperationException("Path too long for a plain tar header: " + path); + var head = new byte[Block]; + name.CopyTo(head, 0); + WriteOctal(head, 100, 8, 0x1A4); // 0644 + WriteOctal(head, 108, 8, 0); + WriteOctal(head, 116, 8, 0); + WriteOctal(head, 124, 12, size); + WriteOctal(head, 136, 12, 0); + for (var index = 148; index < 156; index++) + head[index] = 0x20; + head[156] = (byte)'0'; + Encoding.ASCII.GetBytes("ustar\0").CopyTo(head, 257); + Encoding.ASCII.GetBytes("00").CopyTo(head, 263); + long sum = 0; + foreach (var value in head) + sum += value; + Encoding.ASCII.GetBytes(Convert.ToString(sum, 8).PadLeft(6, '0') + "\0 ").CopyTo(head, 148); + return head; + } + + private static void WriteOctal(byte[] head, int offset, int length, long value) + { + var text = Convert.ToString(value, 8); + if (text.Length >= length) + throw new InvalidOperationException("Value does not fit its tar field."); + Encoding.ASCII.GetBytes(text.PadLeft(length - 1, '0') + "\0").CopyTo(head, offset); + } +} diff --git a/tools/ArcForges.Web.Tooling/Program.cs b/tools/ArcForges.Web.Tooling/Program.cs index 20c0b7b..f96e5b8 100644 --- a/tools/ArcForges.Web.Tooling/Program.cs +++ b/tools/ArcForges.Web.Tooling/Program.cs @@ -1,49 +1,69 @@ // SPDX-License-Identifier: AGPL-3.0-only using ArcForges.Web.Site; +using ArcForges.Web.Tooling.Candidate; +using ArcForges.Web.Tooling.Profiles; namespace ArcForges.Web.Tooling; /// -/// Build tooling entry point. The TypeScript tooling ports here in WEB.40 step 2; only the ported commands run, and every -/// other command is refused with exit code 2. +/// Build tooling entry point of the C# Web. The TypeScript tooling ports here in steps; every command not listed below is +/// refused with exit code 2. Exit code 1 is a failed verification, 2 a usage error. /// public static class Program { private const string SiteBuildUsage = "usage: site build --out [--source-ref <40-hex lower-case commit>]"; + private const string CandidateBuildUsage = + "usage: candidate build --out --repo --source-ref <40-hex> --version --identity --worker [--dirty true|false]"; + private const string CandidateVerifyUsage = "usage: candidate verify --dir [--repo ] [--expected-source <40-hex>]"; + private const string ProfilesBundleUsage = "usage: profiles bundle --publish --out "; + private const string ProfilesVerifyUsage = "usage: profiles verify --bundle [--expected-digest ]"; - /// Runs a ported command. Ported: site build, which writes the static public Site to a new directory. + /// Runs a ported command: site build, candidate build|verify and profiles bundle|verify. public static async Task Main(string[] args) { - if (args is ["site", "build", .. var options]) - return await SiteBuildAsync(options).ConfigureAwait(false); + try + { + if (args is ["site", "build", .. var site]) + return await SiteBuildAsync(site).ConfigureAwait(false); + if (args is ["candidate", "build", .. var build]) + return await CandidateBuildAsync(build).ConfigureAwait(false); + if (args is ["candidate", "verify", .. var verify]) + return await CandidateVerifyAsync(verify).ConfigureAwait(false); + if (args is ["profiles", "bundle", .. var bundle]) + return ProfilesBundle(bundle); + if (args is ["profiles", "verify", .. var check]) + return ProfilesVerify(check); + } + catch (InvalidOperationException error) + { + Console.Error.WriteLine("Verification failed: " + error.Message); + return 1; + } + catch (ArgumentException error) + { + Console.Error.WriteLine(error.Message); + return 2; + } - Console.Error.WriteLine("ArcForges.Web.Tooling: unknown or unported command. Ported commands: site build."); + Console.Error.WriteLine( + "ArcForges.Web.Tooling: unknown or unported command. Ported commands: site build, candidate build, candidate verify, profiles bundle, profiles verify."); return 2; } /// Builds the static Site and writes every file to a directory that must not exist yet. private static async Task SiteBuildAsync(string[] options) { - string? outDirectory = null; - string? sourceRef = null; - for (var index = 0; index < options.Length; index++) + if (!TryParse(options, ["--out", "--source-ref"], out var values)) { - if (options[index] == "--out" && index + 1 < options.Length && outDirectory is null) - outDirectory = options[++index]; - else if (options[index] == "--source-ref" && index + 1 < options.Length && sourceRef is null) - sourceRef = options[++index]; - else - { - Console.Error.WriteLine(SiteBuildUsage); - return 2; - } + Console.Error.WriteLine(SiteBuildUsage); + return 2; } - - if (string.IsNullOrWhiteSpace(outDirectory)) + if (!values.TryGetValue("--out", out var outDirectory) || string.IsNullOrWhiteSpace(outDirectory)) { Console.Error.WriteLine(SiteBuildUsage); return 2; } + values.TryGetValue("--source-ref", out var sourceRef); // Fail closed: a stale file from an earlier build must never be published by a later one. var root = Path.GetFullPath(outDirectory); @@ -53,17 +73,7 @@ private static async Task SiteBuildAsync(string[] options) return 2; } - SiteOutput output; - try - { - output = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = sourceRef }).ConfigureAwait(false); - } - catch (ArgumentException error) - { - Console.Error.WriteLine(error.Message); - return 2; - } - + var output = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = sourceRef }).ConfigureAwait(false); Directory.CreateDirectory(root); foreach (var file in output.Files) { @@ -77,4 +87,104 @@ private static async Task SiteBuildAsync(string[] options) Console.WriteLine($"Wrote {output.Files.Count} Site files to {root}"); return 0; } + + /// Builds the sealed public candidate (the C# Site, its identity, legal files, Worker, SBOMs and receipt). + private static async Task CandidateBuildAsync(string[] options) + { + if (!TryParse(options, ["--out", "--repo", "--source-ref", "--version", "--identity", "--worker", "--dirty"], out var values) + || !Has(values, "--out", "--repo", "--source-ref", "--version", "--identity", "--worker")) + { + Console.Error.WriteLine(CandidateBuildUsage); + return 2; + } + var dirty = false; + if (values.TryGetValue("--dirty", out var dirtyText)) + { + if (dirtyText is not ("true" or "false")) + { + Console.Error.WriteLine(CandidateBuildUsage); + return 2; + } + dirty = dirtyText == "true"; + } + var root = Path.GetFullPath(values["--out"]); + if (Directory.Exists(root) || File.Exists(root)) + throw new ArgumentException("Refusing to write into an existing path: " + root); + + var members = await CandidateBuilder.BuildAsync(new CandidateOptions + { + Repository = Path.GetFullPath(values["--repo"]), + SourceRef = values["--source-ref"], + Version = values["--version"], + Dirty = dirty, + Identity = await File.ReadAllBytesAsync(values["--identity"]).ConfigureAwait(false), + Worker = await File.ReadAllBytesAsync(values["--worker"]).ConfigureAwait(false), + }).ConfigureAwait(false); + CandidateBuilder.Write(root, members); + Console.WriteLine($"Wrote candidate {values["--version"]} {values["--source-ref"]} ({members.Count} members) to {root}"); + return 0; + } + + /// Verifies a sealed candidate. The repository, when given, binds the licence, notices, SBOM and policy receipt. + private static async Task CandidateVerifyAsync(string[] options) + { + if (!TryParse(options, ["--dir", "--repo", "--expected-source"], out var values) || !Has(values, "--dir")) + { + Console.Error.WriteLine(CandidateVerifyUsage); + return 2; + } + values.TryGetValue("--repo", out var repository); + values.TryGetValue("--expected-source", out var expectedSource); + if (expectedSource is not null && !System.Text.RegularExpressions.Regex.IsMatch(expectedSource, "^[a-f0-9]{40}$")) + throw new ArgumentException("The expected source must be a 40-character lower-case commit id."); + var summary = await CandidateVerifier.VerifyAsync( + Path.GetFullPath(values["--dir"]), + repository is null ? null : Path.GetFullPath(repository), + expectedSource).ConfigureAwait(false); + Console.WriteLine($"Candidate verified: {summary.Version} {summary.Source} ({summary.Members} members{(summary.Dirty ? ", local changes; not deployable" : "")})"); + return 0; + } + + /// Builds the Web profile bundle from a publish of ArcForges.Web.App and writes it to the output directory. + private static int ProfilesBundle(string[] options) + { + if (!TryParse(options, ["--publish", "--out"], out var values) || !Has(values, "--publish", "--out")) + { + Console.Error.WriteLine(ProfilesBundleUsage); + return 2; + } + var bundle = ProfileBundle.Build(values["--publish"]); + ProfileBundle.Write(values["--out"], bundle); + Console.WriteLine($"Profile bundle {bundle.Name}: {bundle.Members} members, {bundle.Archive.Length} bytes"); + return 0; + } + + /// Verifies a profile bundle archive against its own name, layout, manifest and reviewed rules. + private static int ProfilesVerify(string[] options) + { + if (!TryParse(options, ["--bundle", "--expected-digest"], out var values) || !Has(values, "--bundle")) + { + Console.Error.WriteLine(ProfilesVerifyUsage); + return 2; + } + values.TryGetValue("--expected-digest", out var expectedDigest); + var summary = ProfileBundle.Verify(File.ReadAllBytes(values["--bundle"]), expectedDigest); + Console.WriteLine($"Profile bundle verified: {summary.Name} ({summary.Members} members)"); + return 0; + } + + /// Parses --name value pairs. Each name must be allowed once; a bare value or a repeated name fails. + private static bool TryParse(string[] args, string[] allowed, out Dictionary values) + { + values = new Dictionary(StringComparer.Ordinal); + for (var index = 0; index < args.Length; index += 2) + { + if (index + 1 >= args.Length || !allowed.Contains(args[index], StringComparer.Ordinal) || values.ContainsKey(args[index])) + return false; + values[args[index]] = args[index + 1]; + } + return true; + } + + private static bool Has(Dictionary values, params string[] names) => names.All(values.ContainsKey); } From f95a6550ec1820183b6a0a18ec2029adbf552a3d Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 22:01:48 +0100 Subject: [PATCH 11/38] [WEB.40] Deploy the C# candidate in CI, with successor receipt web-40-admission-r7 (U3b, part 2) - .github/workflows/ci.yml: the candidate job publishes ArcForges.Web.App, emits the Worker and identity (tooling/candidate.ts), builds the sealed C# candidate twice with a byte comparison, verifies it, and builds and verifies the profile bundle. The React candidate and profile builds leave the deploy path. The deploy job restores and builds the C# verifier and ships the same sealed candidate under the unchanged main-push guards. - eng/policy/dependency-reviews/web-40-admission-r7.json supersedes r6. Its reviewer field is w-deku-20261008-rev-web-40. Only the ci.yml input digest changed; no package, lock, class or project file changed. - eng/policy/dependency-policy.json binds r7 and the rebound ci.yml digest. Validation: tooling/dependency-policy.ts passed (351 dependencies, 39 inputs); dependency-policy tests 15/15; provenance source tests 35/35; architecture audit 0 findings; naming check pass; prettier and biome clean on changed TypeScript. Not run to completion: the C# candidate negative tests and the workflow itself (hosted CI is authoritative). Co-Authored-By: Claude Haiku 5.5 --- .github/workflows/ci.yml | 59 +- eng/policy/dependency-policy.json | 12 +- .../web-40-admission-r7.json | 995 ++++++++++++++++++ 3 files changed, 1052 insertions(+), 14 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r7.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 106d64d..e8e5b77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -227,7 +227,7 @@ jobs: category: /language:${{ matrix.language }} candidate: - name: Build static candidate + name: Build and verify the C# static candidate and profile bundle needs: [source, quality] runs-on: ubuntu-latest timeout-minutes: 20 @@ -244,9 +244,38 @@ jobs: node-version-file: .node-version cache: npm - run: npm ci --ignore-scripts - - run: npm run build - - name: Build, measure and gate the Account and Chat production profiles - run: npm run build:profiles + - name: Set up the pinned .NET SDK + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: "10.0.401" + # WEB.40 deploy switch: the candidate is the C# static Site (tools/ArcForges.Web.Tooling, candidate build), sealed and + # verified before upload. The Worker JavaScript and the build identity are emitted by Node from the reviewed sources, + # because only Node can type-strip worker/index.ts and derive the identity from git. + - name: Restore the locked tooling and the Blazor profile application + run: | + set -euo pipefail + dotnet restore tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj --locked-mode + dotnet build tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj --no-restore -c Release + dotnet restore src/ArcForges.Web.App/ArcForges.Web.App.csproj --locked-mode + dotnet publish src/ArcForges.Web.App/ArcForges.Web.App.csproj --no-restore -c Release -o artifacts/publish/app + - name: Emit the Worker and the build identity from the reviewed sources + run: | + set -euo pipefail + mkdir -p artifacts/inputs + node tooling/candidate.ts worker artifacts/inputs/index.js + node tooling/candidate.ts identity "0.1.0-ci.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}" artifacts/inputs/build-info.json + - name: Build the sealed candidate twice and compare the bytes + run: | + set -euo pipefail + tool=tools/ArcForges.Web.Tooling/bin/Release/net10.0/ArcForges.Web.Tooling.dll + version="0.1.0-ci.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}" + for out in artifacts/candidate artifacts/candidate-repeat; do + dotnet "$tool" candidate build --out "$out" --repo "$GITHUB_WORKSPACE" --source-ref "$GITHUB_SHA" --version "$version" --identity artifacts/inputs/build-info.json --worker artifacts/inputs/index.js --dirty false + done + diff -r artifacts/candidate artifacts/candidate-repeat + rm -rf artifacts/candidate-repeat + - name: Verify the sealed candidate before upload + run: node tooling/candidate.ts verify artifacts/candidate - id: upload uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: @@ -254,8 +283,13 @@ jobs: path: artifacts/candidate if-no-files-found: error retention-days: 30 - # CLOUD.71: the profile bundle written by build:profiles above (one deterministic, digest-named archive of the - # two profile builds), carried to the deployment job unchanged. It stays outside the sealed candidate directory. + # CLOUD.71: the profile bundle (one deterministic, digest-named archive of the Blazor application's publish), carried + # to the deployment job unchanged. It stays outside the sealed candidate directory. + - name: Build the Account and Chat profile bundle + run: | + set -euo pipefail + dotnet tools/ArcForges.Web.Tooling/bin/Release/net10.0/ArcForges.Web.Tooling.dll profiles bundle --publish artifacts/publish/app --out artifacts/profiles + dotnet tools/ArcForges.Web.Tooling/bin/Release/net10.0/ArcForges.Web.Tooling.dll profiles verify --bundle "$(ls artifacts/profiles/web-profiles-*.tar)" - id: upload-profiles if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 @@ -289,8 +323,8 @@ jobs: test "$CANDIDATE" = success if [ "$EVENT" = pull_request ]; then test "$REVIEW" = success; else test "$REVIEW" = skipped; fi - # Deployment step, outside validation (brief section 10). It still ships the sealed React candidate from the candidate - # job; the C# Site and profile outputs are built, tested and retained above and are not yet promoted by this job. + # Deployment step, outside validation (brief section 10). It ships the same sealed C# candidate that the candidate job + # built and verified, after verifying it again here; the Cloudflare upload guards are unchanged (main push only). deploy: name: Deploy Cloudflare if: >- @@ -315,6 +349,15 @@ jobs: node-version-file: .node-version cache: npm - run: npm ci --ignore-scripts + - name: Set up the pinned .NET SDK + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: "10.0.401" + - name: Build the candidate verifier + run: | + set -euo pipefail + dotnet restore tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj --locked-mode + dotnet build tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj --no-restore -c Release - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: artifact-ids: ${{ needs.candidate.outputs.artifact-id }} diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index ab0ed17..6c2eb1e 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -9,7 +9,7 @@ "packages/ui/package.json" ], "inputs": { - ".github/workflows/ci.yml": "8cba7e21b837bc6fded92b69ec0ce0b8e0dc47dd68ad49201e292086b933ee92", + ".github/workflows/ci.yml": "ff594b36bcec33e499a091695639db163cf86494ecce2a1c434c5e1a64a7b5b2", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", @@ -53,9 +53,9 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-08", - "rationale": "WEB.40 successor of web-40-admission-r5 for the C# CI rewrite (unit U3). .github/workflows/ci.yml gains a csharp job on Windows and Linux (locked restore, build, test of the xUnit and bUnit suites and the policy suite, publish of the Account and Chat and Operations profiles, and a static Site build that is compared byte for byte with a repeat build), retains the Site and profile outputs of the Linux leg, adds csharp to the CodeQL matrix with a manual locked build, and requires the csharp job in verify. The browser test project is not restored or built in CI. The main-push deploy job is unchanged and still ships the sealed React candidate. No package, lock, class or closure change; every other input digest is rebound unchanged from r5.", + "rationale": "WEB.40 successor of web-40-admission-r6 for the deploy switch (unit U3b). .github/workflows/ci.yml: the candidate job publishes the Blazor WebAssembly application, emits the Worker and build identity through tooling/candidate.ts, builds the sealed C# candidate twice with a byte comparison, verifies it, and builds the Account and Chat profile bundle with the C# tool; the deploy job restores and builds the C# verifier and then ships the same sealed candidate under the unchanged main-push guards. The static Site candidate is the C# Site output. No package, lock, class, closure or project file changes; every other input digest is unchanged from r6.", "inputs": { - ".github/workflows/ci.yml": "8cba7e21b837bc6fded92b69ec0ce0b8e0dc47dd68ad49201e292086b933ee92", + ".github/workflows/ci.yml": "ff594b36bcec33e499a091695639db163cf86494ecce2a1c434c5e1a64a7b5b2", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", @@ -104,9 +104,9 @@ "sbom", "framework-posture" ], - "runtimeAssessment": "The workflow runs the locked restore, build and test of the C# projects, the Blazor WebAssembly publish of the Account, Chat and Operations profiles and the static Site build. No browser run, live service, deployment or macOS job is added.", + "runtimeAssessment": "The workflow runs the locked restore and build of the candidate verifier and the Blazor WebAssembly publish, the sealed candidate build twice with a byte comparison and its verification, the profile bundle build and verification, and the deploy-time verification before the existing Cloudflare upload. No browser run, live service, hosted device or macOS job is added.", "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", - "previous": "eng/policy/dependency-reviews/web-40-admission-r5.json", + "previous": "eng/policy/dependency-reviews/web-40-admission-r6.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -2243,7 +2243,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r6.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r7.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", diff --git a/eng/policy/dependency-reviews/web-40-admission-r7.json b/eng/policy/dependency-reviews/web-40-admission-r7.json new file mode 100644 index 0000000..487d9c4 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r7.json @@ -0,0 +1,995 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-08", + "rationale": "WEB.40 successor of web-40-admission-r6 for the deploy switch (unit U3b). .github/workflows/ci.yml: the candidate job publishes the Blazor WebAssembly application, emits the Worker and build identity through tooling/candidate.ts, builds the sealed C# candidate twice with a byte comparison, verifies it, and builds the Account and Chat profile bundle with the C# tool; the deploy job restores and builds the C# verifier and then ships the same sealed candidate under the unchanged main-push guards. The static Site candidate is the C# Site output. No package, lock, class, closure or project file changes; every other input digest is unchanged from r6.", + "inputs": { + ".github/workflows/ci.yml": "ff594b36bcec33e499a091695639db163cf86494ecce2a1c434c5e1a64a7b5b2", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "89f8b63f55195b4e2a4acab4d3fda62cef5746cea75164a2fa8bd6928fff2991", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "b9ac8f3f8b997294fa229042e5cbb4451900ad4849c1847fb648be5689191457", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "91076f8a58e052f5e4afc90ac543a2e6b492def2caf37ba2cc8116706e3a791d", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "ba9ed72f6455e68fcc7026995263eb8199f9b85df70991c49e42fd79842d76a3", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "aa12e838198e5e764c1e8552044a27d22275b797566bc128dd2c6d9e314bc28a", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "38ef63915a4befa1c79f77a0ee31e2d8e54adb7be4fc67c4c6903c74deb99ef0", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The workflow runs the locked restore and build of the candidate verifier and the Blazor WebAssembly publish, the sealed candidate build twice with a byte comparison and its verification, the profile bundle build and verification, and the deploy-time verification before the existing Cloudflare upload. No browser run, live service, hosted device or macOS job is added.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r6.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@adobe/css-tools@4.5.0": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "@arcforges/api-client@1.0.0-ci.287.1": "sha512-Nzr25VLIi8nSRQcO46gs2QobNp8yiw51Tf+5th4ZpB19Q6DoweWVs0CYSDhbtAAH8uAIC1IzLiKywEsoD48Geg==", + "@arcforges/proto@1.0.0-ci.287.1": "sha512-lJNdv4ReiG1KF54tziiJpkHgDHS96XsO504no07Ly7cnXCqqK9O0d5o6AaufIn08K7J7C5IR+Hpbq2ZFCrL8+w==", + "@asamuzakjp/css-color@7.0.0": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", + "lru-cache@11.5.2": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "@asamuzakjp/dom-selector@9.2.0": "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw==", + "@axe-core/playwright@4.13.0": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "@babel/code-frame@7.29.7": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "@babel/compat-data@7.29.7": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "@babel/core@7.29.7": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "semver@6.3.1": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "@babel/generator@7.29.8": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "@babel/helper-compilation-targets@7.29.7": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "@babel/helper-globals@7.29.7": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "@babel/helper-module-imports@7.29.7": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "@babel/helper-module-transforms@7.29.7": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "@babel/helper-string-parser@7.29.7": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "@babel/helper-validator-identifier@7.29.7": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "@babel/helper-validator-option@7.29.7": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "@babel/helpers@7.29.7": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "@babel/parser@7.29.8": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "@babel/runtime@7.29.7": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "@babel/template@7.29.7": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "@babel/traverse@7.29.8": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "@babel/types@7.29.8": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "@biomejs/biome@2.5.14": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", + "@biomejs/cli-darwin-arm64@2.5.14": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", + "@biomejs/cli-darwin-x64@2.5.14": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", + "@biomejs/cli-linux-arm64@2.5.14": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", + "@biomejs/cli-linux-arm64-musl@2.5.14": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", + "@biomejs/cli-linux-x64@2.5.14": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", + "@biomejs/cli-linux-x64-musl@2.5.14": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", + "@biomejs/cli-win32-arm64@2.5.14": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", + "@biomejs/cli-win32-x64@2.5.14": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", + "@bramus/specificity@2.4.2": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "@bufbuild/protobuf@2.15.0": "sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==", + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@connectrpc/connect@2.2.0": "sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==", + "@connectrpc/connect-web@2.2.0": "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@csstools/color-helpers@6.1.1": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "@csstools/css-calc@3.4.0": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "@csstools/css-color-parser@4.2.3": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "@csstools/css-parser-algorithms@4.0.0": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "@csstools/css-syntax-patches-for-csstree@1.1.14": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "@csstools/css-tokenizer@4.0.1": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@exodus/bytes@1.15.1": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/gen-mapping@0.3.13": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "@jridgewell/remapping@2.3.5": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@jridgewell/trace-mapping@0.3.31": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "@oxc-project/types@0.149.0": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "@playwright/test@1.63.0": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@react-router/dev@8.4.0": "sha512-GKznpn6h8c52xGXwtsypD5ZwNiWFSeqoKPdyGq7SfG+T9rFjGO8F0i/R/yECfZqHXaNz1NVmpODuBamBy9FfMg==", + "@react-router/node@8.4.0": "sha512-1teZHtk7XDm1pteQ2Plgtvtjjup5C5sIwFlOMYVIyoOH7X3bId3Qb//kUdbydX6NRIxcGK28UnfZlz4SrD/mkg==", + "@remix-run/node-fetch-server@0.14.1": "sha512-pKenF5ysIN5lDCnCyvoUxDhKP0tfnGagvGbZh01xxHG6DJXSIXWVC9NpLSDGjxcxd1CKdCOKuZFleXBF3pChIg==", + "@remix-run/route-pattern@0.22.1": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "@rolldown/binding-android-arm-eabi@1.2.8": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "@rolldown/binding-android-arm64@1.2.8": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "@rolldown/binding-darwin-arm64@1.2.8": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "@rolldown/binding-darwin-x64@1.2.8": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "@rolldown/binding-freebsd-x64@1.2.8": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "@rolldown/binding-linux-arm-gnueabihf@1.2.8": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "@rolldown/binding-linux-arm64-gnu@1.2.8": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "@rolldown/binding-linux-arm64-musl@1.2.8": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "@rolldown/binding-linux-ppc64-gnu@1.2.8": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "@rolldown/binding-linux-s390x-gnu@1.2.8": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "@rolldown/binding-linux-x64-gnu@1.2.8": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "@rolldown/binding-linux-x64-musl@1.2.8": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "@rolldown/binding-openharmony-arm64@1.2.8": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "@rolldown/binding-win32-arm64-msvc@1.2.8": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "@rolldown/binding-win32-x64-msvc@1.2.8": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "@rolldown/pluginutils@1.0.1": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@tailwindcss/node@4.3.3": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "@tailwindcss/oxide@4.3.3": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "@tailwindcss/oxide-android-arm64@4.3.3": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "@tailwindcss/oxide-darwin-arm64@4.3.3": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "@tailwindcss/oxide-darwin-x64@4.3.3": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "@tailwindcss/oxide-freebsd-x64@4.3.3": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "@tailwindcss/oxide-linux-x64-musl@4.3.3": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "@tailwindcss/oxide-wasm32-wasi@4.3.3": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "@tailwindcss/vite@4.3.3": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "@testing-library/dom@10.4.2": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "@testing-library/jest-dom@7.0.1": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dom-accessibility-api@0.6.3": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "@testing-library/react@16.3.3": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "@types/aria-query@5.0.4": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "@types/chai@5.2.3": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "@types/deep-eql@4.0.2": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "@types/estree@1.0.9": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@types/react@19.3.0": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "@types/react-dom@19.3.0": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "@vitest/mocker@5.0.1": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "magic-string@1.4.1": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "@vitest/spy@5.0.1": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "ansi-regex@5.0.1": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "ansi-styles@5.2.0": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "aria-query@5.3.0": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "assertion-error@2.0.1": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "axe-core@4.13.0": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "babel-dead-code-elimination@1.0.12": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "baseline-browser-mapping@2.11.24": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "bidi-js@1.1.0": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "browserslist@4.29.0": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "caniuse-lite@1.0.30001810": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "chai@6.2.2": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "chokidar@5.0.0": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "confbox@0.3.1": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "convert-source-map@2.0.0": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "cookie-es@3.1.1": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "css-tree@3.2.1": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "css.escape@1.5.1": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "csstype@3.2.3": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "data-urls@7.0.0": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "whatwg-url@16.0.1": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "debug@4.4.3": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "decimal.js@10.6.0": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dedent@1.7.2": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dequal@2.0.3": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dom-accessibility-api@0.5.16": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "electron-to-chromium@1.5.429": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==", + "enhanced-resolve@5.25.1": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "entities@8.1.0": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "es-module-lexer@2.3.2": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "escalade@3.2.0": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "estree-walker@3.0.3": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "exit-hook@5.1.0": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "expect-type@1.4.0": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "exsolve@1.1.1": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "fdir@6.5.0": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "gensync@1.0.0-beta.2": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "graceful-fs@4.2.11": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "html-encoding-sniffer@6.0.0": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "indent-string@4.0.0": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "is-potential-custom-element-name@1.0.1": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "isbot@5.2.2": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "jiti@2.7.0": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "js-tokens@4.0.0": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "jsdom@30.1.0": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "jsesc@3.1.0": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "json5@2.2.3": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "lightningcss@1.32.0": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "lightningcss-android-arm64@1.32.0": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "lightningcss-darwin-arm64@1.32.0": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "lightningcss-darwin-x64@1.32.0": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "lightningcss-freebsd-x64@1.32.0": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "lightningcss-linux-arm-gnueabihf@1.32.0": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "lightningcss-linux-arm64-gnu@1.32.0": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "lightningcss-linux-arm64-musl@1.32.0": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "lightningcss-linux-x64-gnu@1.32.0": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "lightningcss-linux-x64-musl@1.32.0": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "lightningcss-win32-arm64-msvc@1.32.0": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "lightningcss-win32-x64-msvc@1.32.0": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "lodash@4.18.1": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "lru-cache@5.1.1": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "lz-string@1.5.0": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "magic-string@0.30.21": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "mdn-data@2.27.1": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "min-indent@1.0.1": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "ms@2.1.3": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "nanoid@3.3.19": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node-releases@2.0.55": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "obug@2.2.1": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "p-map@7.0.8": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "parse5@8.0.1": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "picocolors@1.1.1": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "picomatch@4.0.7": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "pkg-types@2.3.3": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "playwright@1.63.0": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "playwright-core@1.63.0": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "postcss@8.5.28": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "prettier@3.9.8": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "pretty-format@27.5.1": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "punycode@2.3.1": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "react@19.3.0": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "react-dom@19.3.0": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "react-is@17.0.2": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "react-refresh@0.18.0": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "react-router@8.4.0": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "readdirp@5.1.1": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "redent@3.0.0": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "require-from-string@2.0.2": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "rolldown@1.2.8": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "saxes@6.0.0": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "scheduler@0.28.0": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "siginfo@2.0.0": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "source-map-js@1.2.2": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "stackback@0.0.2": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "std-env@4.2.0": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "strip-indent@3.0.0": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tailwindcss@4.3.3": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "tapable@2.3.3": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "tinybench@6.1.4": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "tinyexec@1.3.0": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "tinyglobby@0.2.17": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "tldts@7.4.13": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", + "tldts-core@7.4.13": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", + "tough-cookie@6.0.2": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "tr46@6.0.0": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici@8.10.2": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "update-browserslist-db@1.3.3": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "valibot@1.5.0": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "vite@8.3.0": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "lightningcss@1.33.0": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "lightningcss-android-arm64@1.33.0": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "lightningcss-darwin-arm64@1.33.0": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "lightningcss-darwin-x64@1.33.0": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "lightningcss-freebsd-x64@1.33.0": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "lightningcss-linux-arm-gnueabihf@1.33.0": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "lightningcss-linux-arm64-gnu@1.33.0": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "lightningcss-linux-arm64-musl@1.33.0": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "lightningcss-linux-x64-gnu@1.33.0": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "lightningcss-linux-x64-musl@1.33.0": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "lightningcss-win32-arm64-msvc@1.33.0": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "lightningcss-win32-x64-msvc@1.33.0": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "vitest@5.0.1": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "w3c-xmlserializer@5.0.0": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "webidl-conversions@8.0.1": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "whatwg-mimetype@5.0.0": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "whatwg-url@17.1.1": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "why-is-node-running@2.3.0": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "xml-name-validator@5.0.0": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "xmlchars@2.2.0": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "yallist@3.1.1": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r6.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} From 80900a1430c0fbb483e4a220e20980e3c18728b3 Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 23:38:36 +0100 Subject: [PATCH 12/38] [WEB.40] Port the GOV.11 policy suite to C# with refusal and accepted fixtures (U4) Re-implements the GOV.11 Node/TypeScript policy as the C# successor tests/ArcForges.Web.Policy.Tests, with one xUnit refusal per rule family and sub-rule and accepted forms that must stay admitted. - Architecture: workspace (one solution, lock per project, one npm lock, manifest inventory), exact pins (SDK, Node, npm, central versions, PackageDownload), obsolete Blazor targets (Blazor Server forbidden), portable references, production and install commands, SDK-to-UI licence separation, wire-source rules, private and server imports, desktop DOM prohibition, computed and unresolved imports and the release route graph (Architecture/*.cs, ArchitecturePolicy.cs). - Licence boundary over eng/policy/licence-boundary.json (Licence/). - Forbidden-term scanner: NamingScannerTests verifies the published naming authority identity and runs the published scanner under python -I on a Git fixture for the current terms and every forbidden term. - Fixtures: Fixtures/PolicyBaseline.cs (passing repository) and Fixtures/PolicyCases.cs (refusals and accepted forms). - RepositoryPolicyTests runs the audit over the real tracked and nonignored inputs; it must report zero findings. - ci.yml csharp job: setup-node from .node-version and npm ci --ignore-scripts, so the naming authority is in node_modules for the test. - docs/development.md: factual C# policy suite paragraph. Admission: successor receipt eng/policy/dependency-reviews/web-40-admission-r8.json supersedes r7 (reviewer w-deku-20261008-rev-web-40); only the ci.yml digest changed. eng/policy/dependency-policy.json binds r8 and the rebound digest. ADP-07 supporting files (write scope, recorded per commit): .github/workflows/ci.yml docs/development.md eng/policy/dependency-policy.json eng/policy/dependency-reviews/web-40-admission-r8.json eng/provenance/files.json (12 first-party paths added) tests/ArcForges.Web.Policy.Tests/Architecture/*.cs tests/ArcForges.Web.Policy.Tests/Fixtures/*.cs tests/ArcForges.Web.Policy.Tests/Licence/LicencePolicy.cs tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs tests/ArcForges.Web.Policy.Tests/RepositoryPolicyTests.cs Validation (local, Windows, .NET 10.0.401 locked): dotnet restore --locked-mode, build and test of the Policy project: 97/97 passed (includes RepositoryPolicyTests with zero findings). Validation (local, WSL2 Debian copy, SDK 10.0.400 with the pin rewritten in the copy, locked restore): restore, build and test 97/97 passed. node --test tooling/dependency-policy.test.ts: 43/43 (and dependency policy: 351 dependencies, 39 inputs). Provenance tests 43/43, vitest unit 139/139, biome lint and tsc clean. Not run locally: tooling/project.ts policy after its Node-version assertion. The local toolchain is Node 24.20.0 against the 24.21.0 pin, so that assertion fails locally and the later steps did not run; hosted CI is authoritative. Co-Authored-By: Claude Haiku 5.5 --- .github/workflows/ci.yml | 7 + docs/development.md | 2 + eng/policy/dependency-policy.json | 10 +- .../web-40-admission-r8.json | 995 ++++++++++++++++++ eng/provenance/files.json | 12 + .../Architecture/ArchitecturePolicy.CSharp.cs | 82 ++ .../ArchitecturePolicy.MSBuild.cs | 193 ++++ .../ArchitecturePolicy.TypeScript.cs | 196 ++++ .../Architecture/ArchitecturePolicy.cs | 332 ++++++ .../Architecture/PolicyCore.cs | 253 +++++ .../ArchitecturePolicyTests.cs | 72 ++ .../Fixtures/PolicyBaseline.cs | 121 +++ .../Fixtures/PolicyCases.cs | 145 +++ .../Licence/LicencePolicy.cs | 274 +++++ .../NamingScannerTests.cs | 127 +++ .../RepositoryPolicyTests.cs | 25 + 16 files changed, 2841 insertions(+), 5 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r8.json create mode 100644 tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.CSharp.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.MSBuild.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.TypeScript.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Architecture/PolicyCore.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/Licence/LicencePolicy.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs create mode 100644 tests/ArcForges.Web.Policy.Tests/RepositoryPolicyTests.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8e5b77..6860e5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,6 +103,13 @@ jobs: uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 with: dotnet-version: "10.0.401" + - name: Set up the pinned Node toolchain for the published naming authority + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .node-version + package-manager-cache: false + - name: Install the locked npm closure that carries the published naming authority + run: npm ci --ignore-scripts - name: Restore with the locked NuGet closure run: | set -euo pipefail diff --git a/docs/development.md b/docs/development.md index 7ab78ef..f93fbb9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -16,6 +16,8 @@ Install the exact Node version in `.node-version`, which includes npm 11.19.0. ` | `npm run preview` | Run the sealed assets and redirect Worker using local Wrangler | | `npm run test:e2e` | Optional local browser checks using existing installations; never CI | +The C# policy suite runs in the `csharp` CI job and locally with `dotnet test tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj -c Release` after `npm ci --ignore-scripts` (the forbidden-term scanner reads the published naming authority from `node_modules`). It is the C# successor of the GOV.11 policy: the architecture rules (workspace and central packages, exact pins, obsolete Blazor targets, portable references, production and install commands, private and server imports, generated wire types only, the desktop DOM prohibition and the release route graph), the licence boundary over `eng/policy/licence-boundary.json`, the forbidden-term scanner and the NuGet closure admission. Every rule has a passing and a failing fixture in `tests/ArcForges.Web.Policy.Tests/Fixtures`. The TypeScript source rules in `eng/policy/architecture.ts` keep running in `npm run policy` until the React and TypeScript removal unit retires them. + Hooks check whitespace only. They never repeat builds/tests at commit or push. Git enables `extensions.worktreeConfig` so the hook path does not alter the main checkout's configuration. The `.githooks` shell files only dispatch the whitespace check. On Windows they run through Git for Windows. The protected `main` branch requires both `Verify` and the separate GitHub `CodeQL` scanning result. A successful CodeQL analysis job alone does not imply that it found no blocking security alerts. diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 6c2eb1e..75d70a0 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -9,7 +9,7 @@ "packages/ui/package.json" ], "inputs": { - ".github/workflows/ci.yml": "ff594b36bcec33e499a091695639db163cf86494ecce2a1c434c5e1a64a7b5b2", + ".github/workflows/ci.yml": "f8d07aef5fc955a429c1de0bf2b94f209100f47f62e1fd500f76f35f42787eea", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", @@ -53,9 +53,9 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-08", - "rationale": "WEB.40 successor of web-40-admission-r6 for the deploy switch (unit U3b). .github/workflows/ci.yml: the candidate job publishes the Blazor WebAssembly application, emits the Worker and build identity through tooling/candidate.ts, builds the sealed C# candidate twice with a byte comparison, verifies it, and builds the Account and Chat profile bundle with the C# tool; the deploy job restores and builds the C# verifier and then ships the same sealed candidate under the unchanged main-push guards. The static Site candidate is the C# Site output. No package, lock, class, closure or project file changes; every other input digest is unchanged from r6.", + "rationale": "WEB.40 successor of web-40-admission-r7 for the U4 policy port (unit U4). .github/workflows/ci.yml: the csharp job installs the pinned Node toolchain and the locked npm closure (npm ci --ignore-scripts) so the C# policy suite can read the published naming authority from node_modules and run its scanner under python -I. No package, lock, class, closure, project or props file changes; every other input digest is unchanged from r7.", "inputs": { - ".github/workflows/ci.yml": "ff594b36bcec33e499a091695639db163cf86494ecce2a1c434c5e1a64a7b5b2", + ".github/workflows/ci.yml": "f8d07aef5fc955a429c1de0bf2b94f209100f47f62e1fd500f76f35f42787eea", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", @@ -106,7 +106,7 @@ ], "runtimeAssessment": "The workflow runs the locked restore and build of the candidate verifier and the Blazor WebAssembly publish, the sealed candidate build twice with a byte comparison and its verification, the profile bundle build and verification, and the deploy-time verification before the existing Cloudflare upload. No browser run, live service, hosted device or macOS job is added.", "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", - "previous": "eng/policy/dependency-reviews/web-40-admission-r6.json", + "previous": "eng/policy/dependency-reviews/web-40-admission-r7.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -2243,7 +2243,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r7.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r8.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", diff --git a/eng/policy/dependency-reviews/web-40-admission-r8.json b/eng/policy/dependency-reviews/web-40-admission-r8.json new file mode 100644 index 0000000..018b487 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r8.json @@ -0,0 +1,995 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-08", + "rationale": "WEB.40 successor of web-40-admission-r7 for the U4 policy port (unit U4). .github/workflows/ci.yml: the csharp job installs the pinned Node toolchain and the locked npm closure (npm ci --ignore-scripts) so the C# policy suite can read the published naming authority from node_modules and run its scanner under python -I. No package, lock, class, closure, project or props file changes; every other input digest is unchanged from r7.", + "inputs": { + ".github/workflows/ci.yml": "f8d07aef5fc955a429c1de0bf2b94f209100f47f62e1fd500f76f35f42787eea", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "ArcForges.Web.esproj": "94cf503447c0a773dbce60ca9a5bcbfdfa8d44eea315a7d37f19081cc5619835", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "apps/app/package.json": "0634d5208be3edd24ff0f2f70f230d3069b16810ba0317295f9eb79d35bdc4b5", + "apps/site/package.json": "68f599c70359599806f4b9475187607a0d5a059d09bc3975f0ee03db70d0f38d", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "5b21acc8087f3216924c46d4c23ff11dd5ffbcf52ea1f55005ac807d4809d052", + "package.json": "bdb0154fe07aac403dabdf5bdda7c39b4c70a3605d85d5a3fcd43b0cbe4eff8e", + "packages/ui/package.json": "d20240b20d4f2ff524d36d502b6f28b42f25c0ff54b91c8e416e8a49266342d6", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "89f8b63f55195b4e2a4acab4d3fda62cef5746cea75164a2fa8bd6928fff2991", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "b9ac8f3f8b997294fa229042e5cbb4451900ad4849c1847fb648be5689191457", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "9a1e6fcb95f8296493c8eac07c78cfd03136708196cd7a539bbb6e832a3b6a98", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "68299086748a4c73f2e9576606bed51f9a5c31879eea72af48df38128cb44067", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "91076f8a58e052f5e4afc90ac543a2e6b492def2caf37ba2cc8116706e3a791d", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "ba9ed72f6455e68fcc7026995263eb8199f9b85df70991c49e42fd79842d76a3", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "f6d230ccd616da3f2445e37a8ecaae3bf97b52efdbf46555ff60bf8ebcbe0c53", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "0a32f1d0cbfe0f4e7c79749b1c87139d2cdc7f8295a9784942143276ccf3ddd2", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "3f771663e09c5920ebaa927d74e1cb3c5d4803d3f6d1cf753933867ee103f467", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "aa12e838198e5e764c1e8552044a27d22275b797566bc128dd2c6d9e314bc28a", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "38ef63915a4befa1c79f77a0ee31e2d8e54adb7be4fc67c4c6903c74deb99ef0", + "wrangler.json": "1cf89efa8f3739354f68722ce27632dd38117a7d1504de176b5f8d257a9a0a36" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The workflow runs the locked restore and build of the candidate verifier and the Blazor WebAssembly publish, the sealed candidate build twice with a byte comparison and its verification, the profile bundle build and verification, and the deploy-time verification before the existing Cloudflare upload. No browser run, live service, hosted device or macOS job is added.", + "maintenance": "Each NuGet coordinate is exact and content-hashed. The Contracts packages move only by a reviewed successor that re-verifies the CON.07 or later candidate identity from the publication record. The browser-wasm packs must stay at the SDK bundled runtime version (10.0.12 for SDK 10.0.401); any change needs a new reviewed receipt that names this file as its predecessor. Historical receipts remain immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r7.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@adobe/css-tools@4.5.0": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "@arcforges/api-client@1.0.0-ci.287.1": "sha512-Nzr25VLIi8nSRQcO46gs2QobNp8yiw51Tf+5th4ZpB19Q6DoweWVs0CYSDhbtAAH8uAIC1IzLiKywEsoD48Geg==", + "@arcforges/proto@1.0.0-ci.287.1": "sha512-lJNdv4ReiG1KF54tziiJpkHgDHS96XsO504no07Ly7cnXCqqK9O0d5o6AaufIn08K7J7C5IR+Hpbq2ZFCrL8+w==", + "@asamuzakjp/css-color@7.0.0": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", + "lru-cache@11.5.2": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "@asamuzakjp/dom-selector@9.2.0": "sha512-qKvLetaSd/OTWif9JT0eu1SHBg3H7Nvp9SoMwMxqT5tmYyrxea/P1A5pIfGbnIJlOtm3yVeqw+ORxDOkjN1aEw==", + "@axe-core/playwright@4.13.0": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==", + "@babel/code-frame@7.29.7": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "@babel/compat-data@7.29.7": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "@babel/core@7.29.7": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "semver@6.3.1": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "@babel/generator@7.29.8": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "@babel/helper-compilation-targets@7.29.7": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "@babel/helper-globals@7.29.7": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "@babel/helper-module-imports@7.29.7": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "@babel/helper-module-transforms@7.29.7": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "@babel/helper-string-parser@7.29.7": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "@babel/helper-validator-identifier@7.29.7": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "@babel/helper-validator-option@7.29.7": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "@babel/helpers@7.29.7": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "@babel/parser@7.29.8": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "@babel/runtime@7.29.7": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "@babel/template@7.29.7": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "@babel/traverse@7.29.8": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "@babel/types@7.29.8": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "@biomejs/biome@2.5.14": "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw==", + "@biomejs/cli-darwin-arm64@2.5.14": "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ==", + "@biomejs/cli-darwin-x64@2.5.14": "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA==", + "@biomejs/cli-linux-arm64@2.5.14": "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q==", + "@biomejs/cli-linux-arm64-musl@2.5.14": "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ==", + "@biomejs/cli-linux-x64@2.5.14": "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA==", + "@biomejs/cli-linux-x64-musl@2.5.14": "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg==", + "@biomejs/cli-win32-arm64@2.5.14": "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg==", + "@biomejs/cli-win32-x64@2.5.14": "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ==", + "@bramus/specificity@2.4.2": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "@bufbuild/protobuf@2.15.0": "sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==", + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@connectrpc/connect@2.2.0": "sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==", + "@connectrpc/connect-web@2.2.0": "sha512-uuBUcBTYfb/r1o98LzCCZk1H90yx0vmZ5m21dpw/q4EUwsKwkCDGI8H0aEdjcZld+Q2jJ5A5ADvPqfb6lzQ/sg==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@csstools/color-helpers@6.1.1": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "@csstools/css-calc@3.4.0": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", + "@csstools/css-color-parser@4.2.3": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", + "@csstools/css-parser-algorithms@4.0.0": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "@csstools/css-syntax-patches-for-csstree@1.1.14": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", + "@csstools/css-tokenizer@4.0.1": "sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@exodus/bytes@1.15.1": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/gen-mapping@0.3.13": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "@jridgewell/remapping@2.3.5": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@jridgewell/trace-mapping@0.3.31": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "@oxc-project/types@0.149.0": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "@playwright/test@1.63.0": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@react-router/dev@8.4.0": "sha512-GKznpn6h8c52xGXwtsypD5ZwNiWFSeqoKPdyGq7SfG+T9rFjGO8F0i/R/yECfZqHXaNz1NVmpODuBamBy9FfMg==", + "@react-router/node@8.4.0": "sha512-1teZHtk7XDm1pteQ2Plgtvtjjup5C5sIwFlOMYVIyoOH7X3bId3Qb//kUdbydX6NRIxcGK28UnfZlz4SrD/mkg==", + "@remix-run/node-fetch-server@0.14.1": "sha512-pKenF5ysIN5lDCnCyvoUxDhKP0tfnGagvGbZh01xxHG6DJXSIXWVC9NpLSDGjxcxd1CKdCOKuZFleXBF3pChIg==", + "@remix-run/route-pattern@0.22.1": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==", + "@rolldown/binding-android-arm-eabi@1.2.8": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "@rolldown/binding-android-arm64@1.2.8": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "@rolldown/binding-darwin-arm64@1.2.8": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "@rolldown/binding-darwin-x64@1.2.8": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "@rolldown/binding-freebsd-x64@1.2.8": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "@rolldown/binding-linux-arm-gnueabihf@1.2.8": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "@rolldown/binding-linux-arm64-gnu@1.2.8": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "@rolldown/binding-linux-arm64-musl@1.2.8": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "@rolldown/binding-linux-ppc64-gnu@1.2.8": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "@rolldown/binding-linux-s390x-gnu@1.2.8": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "@rolldown/binding-linux-x64-gnu@1.2.8": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "@rolldown/binding-linux-x64-musl@1.2.8": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "@rolldown/binding-openharmony-arm64@1.2.8": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "@rolldown/binding-win32-arm64-msvc@1.2.8": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "@rolldown/binding-win32-x64-msvc@1.2.8": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "@rolldown/pluginutils@1.0.1": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@tailwindcss/node@4.3.3": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "@tailwindcss/oxide@4.3.3": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "@tailwindcss/oxide-android-arm64@4.3.3": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "@tailwindcss/oxide-darwin-arm64@4.3.3": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "@tailwindcss/oxide-darwin-x64@4.3.3": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "@tailwindcss/oxide-freebsd-x64@4.3.3": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "@tailwindcss/oxide-linux-arm64-gnu@4.3.3": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "@tailwindcss/oxide-linux-arm64-musl@4.3.3": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "@tailwindcss/oxide-linux-x64-gnu@4.3.3": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "@tailwindcss/oxide-linux-x64-musl@4.3.3": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "@tailwindcss/oxide-wasm32-wasi@4.3.3": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "@tailwindcss/oxide-win32-arm64-msvc@4.3.3": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "@tailwindcss/oxide-win32-x64-msvc@4.3.3": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "@tailwindcss/vite@4.3.3": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "@testing-library/dom@10.4.2": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==", + "@testing-library/jest-dom@7.0.1": "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw==", + "dom-accessibility-api@0.6.3": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "@testing-library/react@16.3.3": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "@types/aria-query@5.0.4": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "@types/chai@5.2.3": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "@types/deep-eql@4.0.2": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "@types/estree@1.0.9": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@types/react@19.3.0": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "@types/react-dom@19.3.0": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "@vitest/mocker@5.0.1": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", + "magic-string@1.4.1": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "@vitest/spy@5.0.1": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", + "ansi-regex@5.0.1": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "ansi-styles@5.2.0": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "aria-query@5.3.0": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "assertion-error@2.0.1": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "axe-core@4.13.0": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "babel-dead-code-elimination@1.0.12": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==", + "baseline-browser-mapping@2.11.24": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "bidi-js@1.1.0": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "browserslist@4.29.0": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==", + "caniuse-lite@1.0.30001810": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "chai@6.2.2": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "chokidar@5.0.0": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "confbox@0.3.1": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "convert-source-map@2.0.0": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "cookie-es@3.1.1": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "css-tree@3.2.1": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "css.escape@1.5.1": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "csstype@3.2.3": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "data-urls@7.0.0": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "whatwg-url@16.0.1": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "debug@4.4.3": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "decimal.js@10.6.0": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dedent@1.7.2": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dequal@2.0.3": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dom-accessibility-api@0.5.16": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "electron-to-chromium@1.5.429": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==", + "enhanced-resolve@5.25.1": "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w==", + "entities@8.1.0": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "es-module-lexer@2.3.2": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "escalade@3.2.0": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "estree-walker@3.0.3": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "exit-hook@5.1.0": "sha512-INjr2xyxHo7bhAqf5ong++GZPPnpcuBcaXUKt03yf7Fie9yWD7FapL4teOU0+awQazGs5ucBh7xWs/AD+6nhog==", + "expect-type@1.4.0": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "exsolve@1.1.1": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "fdir@6.5.0": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "gensync@1.0.0-beta.2": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "graceful-fs@4.2.11": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "html-encoding-sniffer@6.0.0": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "indent-string@4.0.0": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "is-potential-custom-element-name@1.0.1": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "isbot@5.2.2": "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w==", + "jiti@2.7.0": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "js-tokens@4.0.0": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "jsdom@30.1.0": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", + "jsesc@3.1.0": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "json5@2.2.3": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "lightningcss@1.32.0": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "lightningcss-android-arm64@1.32.0": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "lightningcss-darwin-arm64@1.32.0": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "lightningcss-darwin-x64@1.32.0": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "lightningcss-freebsd-x64@1.32.0": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "lightningcss-linux-arm-gnueabihf@1.32.0": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "lightningcss-linux-arm64-gnu@1.32.0": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "lightningcss-linux-arm64-musl@1.32.0": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "lightningcss-linux-x64-gnu@1.32.0": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "lightningcss-linux-x64-musl@1.32.0": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "lightningcss-win32-arm64-msvc@1.32.0": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "lightningcss-win32-x64-msvc@1.32.0": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "lodash@4.18.1": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "lru-cache@5.1.1": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "lz-string@1.5.0": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "magic-string@0.30.21": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "mdn-data@2.27.1": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "min-indent@1.0.1": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "ms@2.1.3": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "nanoid@3.3.19": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node-releases@2.0.55": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "obug@2.2.1": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "p-map@7.0.8": "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==", + "parse5@8.0.1": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "picocolors@1.1.1": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "picomatch@4.0.7": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "pkg-types@2.3.3": "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w==", + "playwright@1.63.0": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "playwright-core@1.63.0": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "postcss@8.5.28": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "prettier@3.9.8": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "pretty-format@27.5.1": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "punycode@2.3.1": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "react@19.3.0": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "react-dom@19.3.0": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "react-is@17.0.2": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "react-refresh@0.18.0": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "react-router@8.4.0": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==", + "readdirp@5.1.1": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "redent@3.0.0": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "require-from-string@2.0.2": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "rolldown@1.2.8": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "saxes@6.0.0": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "scheduler@0.28.0": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "siginfo@2.0.0": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "source-map-js@1.2.2": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "stackback@0.0.2": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "std-env@4.2.0": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "strip-indent@3.0.0": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tailwindcss@4.3.3": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "tapable@2.3.3": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "tinybench@6.1.4": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "tinyexec@1.3.0": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "tinyglobby@0.2.17": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "tldts@7.4.13": "sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==", + "tldts-core@7.4.13": "sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==", + "tough-cookie@6.0.2": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "tr46@6.0.0": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici@8.10.2": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "update-browserslist-db@1.3.3": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "valibot@1.5.0": "sha512-nil6AkP2TChWL43Z5uJ6GTxX01CUA+g8LWUM+N/rB9NBbkUMaUsi9PUNzlUPgoKASgmx9f7eGOYpJ04/fSa6FQ==", + "vite@8.3.0": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "lightningcss@1.33.0": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "lightningcss-android-arm64@1.33.0": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "lightningcss-darwin-arm64@1.33.0": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "lightningcss-darwin-x64@1.33.0": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "lightningcss-freebsd-x64@1.33.0": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "lightningcss-linux-arm-gnueabihf@1.33.0": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "lightningcss-linux-arm64-gnu@1.33.0": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "lightningcss-linux-arm64-musl@1.33.0": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "lightningcss-linux-x64-gnu@1.33.0": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "lightningcss-linux-x64-musl@1.33.0": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "lightningcss-win32-arm64-msvc@1.33.0": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "lightningcss-win32-x64-msvc@1.33.0": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "vitest@5.0.1": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", + "w3c-xmlserializer@5.0.0": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "webidl-conversions@8.0.1": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "whatwg-mimetype@5.0.0": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "whatwg-url@17.1.1": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", + "why-is-node-running@2.3.0": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "xml-name-validator@5.0.0": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "xmlchars@2.2.0": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "yallist@3.1.1": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r7.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/eng/provenance/files.json b/eng/provenance/files.json index b7918e3..acfee06 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -27,9 +27,21 @@ "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", + "eng/policy/dependency-reviews/web-40-admission-r8.json", "NuGet.config", "README.md", "SECURITY.md", + "tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs", + "tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.CSharp.cs", + "tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.MSBuild.cs", + "tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.TypeScript.cs", + "tests/ArcForges.Web.Policy.Tests/Architecture/PolicyCore.cs", + "tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs", + "tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs", + "tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs", + "tests/ArcForges.Web.Policy.Tests/Licence/LicencePolicy.cs", + "tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs", + "tests/ArcForges.Web.Policy.Tests/RepositoryPolicyTests.cs", "THIRD_PARTY_NOTICES.md", "apps/app/README.md", "apps/app/app/app.css", diff --git a/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.CSharp.cs b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.CSharp.cs new file mode 100644 index 0000000..dacf4c6 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.CSharp.cs @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.RegularExpressions; + +namespace ArcForges.Web.Policy.Tests.Architecture; + +/// +/// The C# source rules: private namespaces, generated wire types only, the two admitted JSON and HTTP surfaces, the +/// desktop DOM prohibition and the release fixture routes. A file is release code when it is under src/. +/// +public static partial class ArchitecturePolicy +{ + private static readonly Regex CSharpUsing = new( + @"^\s*(?:global\s+)?using\s+(?:static\s+)?(?:[\w.]+\s*=\s*)?([\w.]+)\s*;", + RegexOptions.Multiline | RegexOptions.CultureInvariant); + private static readonly Regex RazorUsing = new(@"@using\s+(?:static\s+)?([\w.]+)", RegexOptions.CultureInvariant); + private static readonly Regex WireDeclaration = new( + @"\b(?:class|record|struct|interface)\s+(\w*(?:Request|Response|Dto|Message))\b", RegexOptions.CultureInvariant); + private static readonly Regex JsonApi = new( + @"\bSystem\.Text\.Json\b|\bNewtonsoft\.Json\b|\bJsonSerializer\b|\bJsonNode\b|\bJsonObject\b|\bJsonArray\b|\bJsonDocument\b|\bJsonElement\b|\bUtf8JsonWriter\b", + RegexOptions.CultureInvariant); + private static readonly Regex HttpApi = new( + @"\bHttpClient\b|\bHttpRequestMessage\b|\bHttpResponseMessage\b|\bHttpMethod\b|\b(?:FromJsonAsync|AsJsonAsync|ReadFromJsonAsync|GetStringAsync|GetByteArrayAsync|GetStreamAsync)\b|(?", RegexOptions.CultureInvariant); + + /// The one admitted JSON codec: the generated Contracts JSON context decodes the same-origin documents. + private static readonly string[] JsonAdmissions = ["src/ArcForges.Web.App/Probe/WireJson.cs"]; + + /// + /// The admitted HTTP surfaces: the BrowserSession HTTP exception calls (SessionProbe), the generated gRPC-Web + /// handler of the anonymous greeting (HelloProbe) and the cloud-hello server connection check (ServerConnection). + /// + private static readonly string[] HttpAdmissions = + [ + "src/ArcForges.Web.App/Probe/SessionProbe.cs", + "src/ArcForges.Web.App/Probe/HelloProbe.cs", + "src/ArcForges.Web.Ui/ServerConnection.cs", + ]; + + private static void AuditCSharpSources(PolicyContext context) + { + foreach (var file in context.Find(name => name.EndsWith(".cs", StringComparison.Ordinal) + || name.EndsWith(".razor", StringComparison.Ordinal))) + { + var text = context.Text(file) ?? string.Empty; + var razor = file.EndsWith(".razor", StringComparison.Ordinal); + var code = razor ? RazorComment.Replace(text, " ") : PolicyText.StripCode(text, templates: false); + + foreach (Match use in (razor ? RazorUsing : CSharpUsing).Matches(code)) + if (PrivateNamespace.IsMatch(use.Groups[1].Value)) + context.Report("private-import", file, "Private or server namespace: " + use.Groups[1].Value); + + if (DesktopPath.IsMatch(file) && DesktopApi.IsMatch(code)) + context.Report("desktop-dom", file, "Desktop graphs must not use the DOM or JavaScript interop."); + + if (!file.StartsWith("src/", StringComparison.Ordinal)) + continue; + + foreach (Match declaration in WireDeclaration.Matches(code)) + context.Report("wire-source", file, "Wire shapes must come from the published generated package: " + declaration.Groups[1].Value); + if (!JsonAdmissions.Contains(file, StringComparer.Ordinal) && JsonApi.IsMatch(code)) + context.Report("wire-source", file, "Production wire serialization uses the generated Contracts codecs; JSON needs an explicit admission."); + if (!HttpAdmissions.Contains(file, StringComparer.Ordinal) && HttpApi.IsMatch(code)) + context.Report("wire-source", file, "Business network calls must use the published generated transport."); + if (BlazorServerCode.IsMatch(code)) + context.Report("obsolete-target", file, "Blazor Server render modes and circuits are forbidden in release code."); + foreach (Match route in PageRoute.Matches(code)) + if (FixtureRouteSegment.IsMatch(route.Groups[1].Value)) + context.Report("release-fixture", file, "Fixture or debug route in the release route graph: " + route.Groups[1].Value); + } + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.MSBuild.cs b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.MSBuild.cs new file mode 100644 index 0000000..8ce24bb --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.MSBuild.cs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.RegularExpressions; + +namespace ArcForges.Web.Policy.Tests.Architecture; + +/// +/// The MSBuild rules: obsolete Blazor targets, portable references, production and install commands, restore sources, +/// imports, the Apache-to-AGPL project graph and the release route graph. +/// +public static partial class ArchitecturePolicy +{ + /// Paths that name test, fixture or double code. Such files must never be in the release route graph. + internal static readonly Regex FixturePath = new( + @"(?:^|/)(?:tests?|__tests__|fixtures?|test-?helpers|testdoubles)(?:/|\.)|\.(?:test|spec)\.|(?:^|/)[^/]*(?:Fixtures?|TestDoubles?)\.(?:cs|razor)$", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex ProjectReferenceInclude = new( + "]*?Include=[\"']([^\"']+)[\"']", RegexOptions.CultureInvariant); + private static readonly Regex NonLiteralReference = new(@"[$@%*?;]", RegexOptions.CultureInvariant); + private static readonly Regex BlazorServerTarget = new( + @"AddInteractiveServer|InteractiveServer|Microsoft\.AspNetCore\.Components\.Server\b|Microsoft\.AspNetCore\.Components\.WebAssembly\.Server\b|AddRazorComponents|MapRazorComponents|\bCircuit\b", + RegexOptions.CultureInvariant); + private static readonly Regex DevelopmentServer = new( + "]*?\\bCommand=\"([^\"]*)\"", RegexOptions.CultureInvariant); + private static readonly Regex ExecForbidden = new( + @"\b(?:npm\s+(?:install|i|exec)|npx\b|dotnet\s+(?:restore|add)|(?:vite|react-router|wrangler)\s+(?:dev|serve)|--hmr)\b|\bnpm\s+ci\b(?![^""]*--ignore-scripts)", + RegexOptions.CultureInvariant); + private static readonly Regex DevelopmentBuildCommand = new( + @"(?:\b(?:install|ci|dev|serve)\b|--hmr)", RegexOptions.CultureInvariant); + private static readonly Regex NpmInstallSwitch = new( + "\\s*(?!false\\s*<)[^<]+", RegexOptions.CultureInvariant); + private static readonly Regex BuildCommandElement = new("[^<]*", RegexOptions.CultureInvariant); + private static readonly Regex LockRestoreDisabled = new( + "<(?:RestorePackagesWithLockFile|RestoreLockedMode)>\\s*false\\s*<", RegexOptions.CultureInvariant); + private static readonly Regex AlternateRestoreSource = new( + "<(?:RestoreSources|RestoreAdditionalProjectSources)\\b", RegexOptions.CultureInvariant); + private static readonly Regex UsingInclude = new("]*?Include=\"([^\"]+)\"", RegexOptions.CultureInvariant); + private static readonly Regex LicenceBoundaryValue = new( + "]*)?>([^<]*)", RegexOptions.CultureInvariant); + private static readonly Regex ReleaseItem = new( + "<(?:Compile|Content|None|EmbeddedResource)\\s[^>]*?Include=\"([^\"]+)\"", RegexOptions.CultureInvariant); + private static readonly Regex PrivateNamespace = new( + @"^ArcForges(?:\.[\w]+)*\.(?:Private|Server|LocalRpc|Internal|Operator|AIInternal|Storage)(?:\.|$)|^ArcForges\.Contracts\.(?:Internal|Storage|AIInternal)(?:\.|$)", + RegexOptions.CultureInvariant); + + private static readonly string[] ReleaseRoots = ["src/"]; + + private static void AuditMsBuildProjects(PolicyContext context) + { + var projects = context.Find(name => name.EndsWith(".csproj", StringComparison.Ordinal) + || name.EndsWith(".esproj", StringComparison.Ordinal) + || name.EndsWith(".props", StringComparison.Ordinal) + || name.EndsWith(".targets", StringComparison.Ordinal)); + var targets = context.Find(name => name.EndsWith(".slnx", StringComparison.Ordinal) + || name.EndsWith(".csproj", StringComparison.Ordinal) + || name.EndsWith(".esproj", StringComparison.Ordinal) + || name.EndsWith(".props", StringComparison.Ordinal) + || name.EndsWith(".targets", StringComparison.Ordinal)); + + foreach (var file in targets) + { + var xml = PolicyText.StripXmlComments(context.Text(file) ?? string.Empty); + if (BlazorServerTarget.IsMatch(xml)) + context.Report("obsolete-target", file, "Blazor Server render modes and circuits are forbidden; Blazor WebAssembly standalone is the only Blazor target."); + if (DevelopmentServer.IsMatch(xml)) + context.Report("production-command", file, "A development server (SpaProxy) is not allowed in a production project."); + if (LockRestoreDisabled.IsMatch(xml)) + context.Report("workspace", file, "Implicit restore is refused: restore uses the locked NuGet closure."); + if (AlternateRestoreSource.IsMatch(xml)) + context.Report("workspace", file, "Alternate restore sources are not allowed; the nuget.org source is the only one."); + } + + foreach (var file in projects) + { + var xml = PolicyText.StripXmlComments(context.Text(file) ?? string.Empty); + if (file.EndsWith(".esproj", StringComparison.Ordinal) || file.EndsWith(".props", StringComparison.Ordinal) || file.EndsWith(".targets", StringComparison.Ordinal)) + { + if (NpmInstallSwitch.IsMatch(xml)) + context.Report("production-command", file, "Implicit IDE npm install is not allowed."); + } + foreach (var build in BuildCommandElement.Matches(xml).Select(match => match.Value)) + if (DevelopmentBuildCommand.IsMatch(build)) + context.Report("production-command", file, "The build command installs or starts a development server."); + foreach (Match exec in ExecCommand.Matches(xml)) + if (ExecForbidden.IsMatch(exec.Groups[1].Value)) + context.Report("production-command", file, "An Exec command installs, restores or starts a development server implicitly."); + foreach (Match use in UsingInclude.Matches(xml)) + if (PrivateNamespace.IsMatch(use.Groups[1].Value)) + context.Report("private-import", file, "Private or server namespace: " + use.Groups[1].Value); + foreach (Match package in Regex.Matches(xml, "]*?(?:Include|Update)=\"([^\"]+)\"", RegexOptions.CultureInvariant)) + if (PrivateNamespace.IsMatch(package.Groups[1].Value)) + context.Report("private-import", file, "Private or server package: " + package.Groups[1].Value); + if (!file.EndsWith(".csproj", StringComparison.Ordinal) && !file.EndsWith(".props", StringComparison.Ordinal) && !file.EndsWith(".targets", StringComparison.Ordinal)) + continue; + foreach (Match reference in ProjectReferenceInclude.Matches(xml)) + { + var include = reference.Groups[1].Value; + if (NonLiteralReference.IsMatch(include)) + { + context.Report("computed-import", file, "Nonliteral project reference: " + include); + continue; + } + if (include.EndsWith(".esproj", StringComparison.OrdinalIgnoreCase)) + { + context.Report("portable-reference", file, "Managed graph references esproj: " + include); + continue; + } + var target = ProjectTarget(file, include); + if (!context.Sources.ContainsKey(target) && !target.EndsWith(".esproj", StringComparison.Ordinal)) + context.Report("unresolved-import", file, "Project reference does not resolve: " + include); + } + } + + AuditLicenceBoundaryGraph(context, projects); + } + + /// An Apache-declared project must never reach a project of another boundary through project references. + private static void AuditLicenceBoundaryGraph(PolicyContext context, IReadOnlyList projects) + { + var boundaries = new Dictionary(StringComparer.Ordinal); + var edges = new Dictionary>(StringComparer.Ordinal); + foreach (var project in projects.Where(name => name.EndsWith(".csproj", StringComparison.Ordinal))) + { + var xml = PolicyText.StripXmlComments(context.Text(project) ?? string.Empty); + boundaries[project] = LicenceBoundaryValue.Match(xml).Groups[1].Value; + edges[project] = ProjectReferenceInclude.Matches(xml) + .Select(match => match.Groups[1].Value) + .Where(include => !NonLiteralReference.IsMatch(include) && !include.EndsWith(".esproj", StringComparison.OrdinalIgnoreCase)) + .Select(include => ProjectTarget(project, include)) + .ToList(); + } + foreach (var (project, boundary) in boundaries) + { + if (boundary != "Apache") + continue; + var seen = new HashSet(StringComparer.Ordinal); + var pending = new Stack(edges[project]); + while (pending.Count > 0) + { + var current = pending.Pop(); + if (!seen.Add(current) || current == project) + continue; + if (boundaries.TryGetValue(current, out var reached) && reached != "Apache") + context.Report("sdk-ui", project, "Apache project reaches " + current + " with boundary " + reached); + if (edges.TryGetValue(current, out var next)) + foreach (var target in next) + pending.Push(target); + } + } + } + + /// + /// The release route graph: every project reachable from a source project through project references must be + /// production code, and no file of a reached project may be a test, fixture or double. + /// + private static void AuditReleaseGraph(PolicyContext context) + { + var roots = context.Find(name => name.EndsWith(".csproj", StringComparison.Ordinal) + && ReleaseRoots.Any(prefix => name.StartsWith(prefix, StringComparison.Ordinal))); + var seen = new HashSet(StringComparer.Ordinal); + var pending = new Stack(roots); + while (pending.Count > 0) + { + var project = pending.Pop(); + if (!seen.Add(project)) + continue; + if (FixturePath.IsMatch(project)) + context.Report("release-fixture", project, "Test or fixture project reachable from the release route graph."); + var directory = PolicyText.DirectoryOf(project) + "/"; + var xml = PolicyText.StripXmlComments(context.Text(project) ?? string.Empty); + foreach (Match item in ReleaseItem.Matches(xml)) + if (item.Groups[1].Value.Contains("..", StringComparison.Ordinal) || FixturePath.IsMatch(item.Groups[1].Value)) + context.Report("release-fixture", project, "Release item reaches outside production code: " + item.Groups[1].Value); + foreach (var file in context.Find(name => name.StartsWith(directory, StringComparison.Ordinal) + && (name.EndsWith(".cs", StringComparison.Ordinal) || name.EndsWith(".razor", StringComparison.Ordinal)))) + if (FixturePath.IsMatch(file)) + context.Report("release-fixture", file, "Fixture or test helper in the release route graph."); + foreach (Match reference in ProjectReferenceInclude.Matches(xml)) + { + var include = reference.Groups[1].Value; + if (NonLiteralReference.IsMatch(include) || include.EndsWith(".esproj", StringComparison.OrdinalIgnoreCase)) + continue; + var target = ProjectTarget(project, include); + if (context.Sources.ContainsKey(target)) + pending.Push(target); + } + } + } + + private static string ProjectTarget(string project, string include) => + PolicyText.Join(PolicyText.DirectoryOf(project), include.Replace('\\', '/')); +} diff --git a/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.TypeScript.cs b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.TypeScript.cs new file mode 100644 index 0000000..7cb180c --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.TypeScript.cs @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace ArcForges.Web.Policy.Tests.Architecture; + +/// +/// The TypeScript source rules that remain until the React and TypeScript removal unit. They are the lexical port of +/// the Babel-based GOV.11 rules: specifiers and their resolution, generated wire types only, the fetch and JSON +/// prohibitions, the desktop DOM prohibition and the release route graph from the production roots. Only files +/// reachable from a production root are reported. +/// +public static partial class ArchitecturePolicy +{ + private static readonly string[] TypeScriptExtensions = [".ts", ".tsx", ".js", ".mjs", ".cjs", ".cts", ".mts"]; + private static readonly string[] AssetExtensions = [".css", ".svg", ".png", ".woff", ".woff2"]; + private static readonly string[] ResolutionSuffixes = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx"]; + + private static readonly Regex TsProductionRoot = new( + @"^(?:apps/[^/]+/app|packages/[^/]+/src|worker)/", RegexOptions.CultureInvariant); + private static readonly Regex TsStaticSpecifier = new( + @"\b(?:import|export)\b[^;'""`]*?\bfrom\s*(['""])([^'""\r\n]+)\1", RegexOptions.CultureInvariant); + private static readonly Regex TsBareImport = new( + @"(? Edges { get; } = []; + public List Findings { get; } = []; + } + + private static void AuditTypeScriptSources(PolicyContext context) + { + var workspaces = WorkspaceDirectories(context); + var parsed = new Dictionary(StringComparer.Ordinal); + foreach (var file in context.Find(name => TypeScriptExtensions.Any(extension => name.EndsWith(extension, StringComparison.Ordinal)))) + parsed[file] = ParseTypeScript(context, file, workspaces); + + var reachable = new HashSet(StringComparer.Ordinal); + var pending = new Stack(parsed.Keys.Where(file => TsProductionRoot.IsMatch(file))); + while (pending.Count > 0) + { + var file = pending.Pop(); + if (!reachable.Add(file) || !parsed.TryGetValue(file, out var info)) + continue; + foreach (var edge in info.Edges) + pending.Push(edge); + } + + foreach (var file in reachable.Order(StringComparer.Ordinal)) + { + if (FixturePath.IsMatch(file)) + context.Report("release-fixture", file, "Fixture or test helper reachable from the release route graph."); + if (parsed.TryGetValue(file, out var info)) + foreach (var finding in info.Findings) + context.Report(finding.Rule, finding.File, finding.Detail); + } + } + + private static Dictionary WorkspaceDirectories(PolicyContext context) + { + var workspaces = new Dictionary(StringComparer.Ordinal); + foreach (var manifest in context.Find(name => PolicyText.BaseName(name) == "package.json")) + { + var name = JsonString(PolicyText.ParseObject(context.Text(manifest)), "name"); + if (name.Length > 0) + workspaces[name] = PolicyText.DirectoryOf(manifest); + } + return workspaces; + } + + private static TypeScriptFile ParseTypeScript(PolicyContext context, string file, IReadOnlyDictionary workspaces) + { + var info = new TypeScriptFile(); + var code = PolicyText.StripCode(context.Text(file) ?? string.Empty, templates: true); + + foreach (Match match in TsStaticSpecifier.Matches(code)) + Specifier(context, info, file, match.Groups[2].Value, workspaces); + foreach (Match match in TsBareImport.Matches(code)) + Specifier(context, info, file, match.Groups[2].Value, workspaces); + foreach (var pattern in new[] { TsDynamicImport, TsRequire }) + foreach (Match match in pattern.Matches(code)) + { + var argument = match.Groups[1].Value; + var literal = TsLiteralArgument.Match(argument); + if (literal.Success) + Specifier(context, info, file, literal.Groups[2].Value, workspaces); + else + info.Findings.Add(new PolicyFinding("computed-import", file, "Release imports must be statically resolved.")); + } + + var generated = new HashSet(StringComparer.Ordinal); + foreach (Match import in TsGeneratedImport.Matches(code)) + foreach (var item in import.Groups[1].Value.Split(',')) + { + var parts = item.Split(" as ", StringSplitOptions.TrimEntries); + var local = parts[^1]; + if (local.Length > 0) + generated.Add(local); + } + foreach (Match declaration in TsWireInterface.Matches(code)) + info.Findings.Add(new PolicyFinding("wire-source", file, "Wire shapes must come from the published generated package: " + declaration.Groups[1].Value)); + foreach (Match alias in TsWireAlias.Matches(code)) + { + var target = alias.Groups[2].Value.Trim(); + if (!TsGeneratedAliasTarget.IsMatch(target) && !generated.Contains(target)) + info.Findings.Add(new PolicyFinding("wire-source", file, "Wire shapes must come from the published generated package: " + alias.Groups[1].Value)); + } + if (TsJsonCodec.IsMatch(code)) + info.Findings.Add(new PolicyFinding("wire-source", file, "Production wire serialization uses published generated codecs; JSON needs explicit HTTP exception admission.")); + if (!file.StartsWith("worker/", StringComparison.Ordinal) && TsFetch.IsMatch(code)) + info.Findings.Add(new PolicyFinding("wire-source", file, "Business network calls must use published generated transport.")); + if (TsDesktopPath.IsMatch(file)) + foreach (Match dom in TsDesktopDom.Matches(code)) + info.Findings.Add(new PolicyFinding("desktop-dom", file, dom.Value)); + return info; + } + + private static void Specifier(PolicyContext context, TypeScriptFile info, string file, string value, IReadOnlyDictionary workspaces) + { + if (TsProtobufSpecifier.IsMatch(value)) + info.Findings.Add(new PolicyFinding("wire-source", file, "Production cannot author wire descriptors or codecs.")); + if (NpmPrivateSpecifier.IsMatch(value)) + info.Findings.Add(new PolicyFinding("private-import", file, value)); + if (TsSdkPath.IsMatch(file) && TsSdkSpecifier.IsMatch(value)) + info.Findings.Add(new PolicyFinding("sdk-ui", file, value)); + if (TsDesktopPath.IsMatch(file) && TsDesktopSpecifier.IsMatch(value)) + info.Findings.Add(new PolicyFinding("desktop-dom", file, value)); + + var stem = LocalStem(context, file, value, workspaces); + if (stem is null) + return; + if (stem.StartsWith("../", StringComparison.Ordinal)) + { + info.Findings.Add(new PolicyFinding("private-import", file, "Sibling source escape: " + value)); + return; + } + var stripped = stem.EndsWith(".js", StringComparison.Ordinal) ? stem[..^3] : stem; + var candidates = new[] { stem }.Concat(ResolutionSuffixes.Select(suffix => stripped + suffix)).ToArray(); + var resolved = candidates.FirstOrDefault(candidate => context.Sources.ContainsKey(candidate)); + if (resolved is null) + { + if (!value.Contains("+types/", StringComparison.Ordinal) && !AssetExtensions.Any(extension => value.EndsWith(extension, StringComparison.Ordinal))) + info.Findings.Add(new PolicyFinding("unresolved-import", file, value)); + return; + } + info.Edges.Add(resolved); + } + + private static string? LocalStem(PolicyContext context, string file, string value, IReadOnlyDictionary workspaces) + { + if (value.StartsWith('.')) + return PolicyText.Join(PolicyText.DirectoryOf(file), value); + if (value.StartsWith("~/", StringComparison.Ordinal)) + { + var segments = file.Split('/'); + return PolicyText.Join(string.Join('/', segments.Take(2)) + "/app", value[2..]); + } + foreach (var (name, directory) in workspaces) + { + if (value != name && !value.StartsWith(name + "/", StringComparison.Ordinal)) + continue; + var manifest = PolicyText.ParseObject(context.Text(PolicyText.ManifestPath(directory))); + var exports = manifest?["exports"]; + var key = value == name ? "." : "." + value[name.Length..]; + string? mapped = exports is JsonValue single && single.TryGetValue(out string? text) + ? text + : (exports as JsonObject)?[key]?.ToJsonString().Trim('"'); + if (mapped is not null) + return PolicyText.Join(directory, mapped); + } + return null; + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs new file mode 100644 index 0000000..28749c1 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; + +namespace ArcForges.Web.Policy.Tests.Architecture; + +/// +/// The Web architecture rules of the GOV.11 successor: repository shape, exact pins, npm manifests and scripts, MSBuild +/// targets, C# and TypeScript import and wire rules, desktop scope and the release route graph. Each rule family +/// reports through ; the fixtures in the test project keep one passing and one failing +/// example for every rule. +/// +public static partial class ArchitecturePolicy +{ + internal static readonly Regex ExactVersion = new(@"^\d+\.\d+\.\d+(?:-[\w.-]+)?$", RegexOptions.CultureInvariant); + private static readonly Regex ExactSdk = new(@"^\d+\.\d+\.\d+$", RegexOptions.CultureInvariant); + private static readonly Regex ExactDownload = new(@"^\[\d+\.\d+\.\d+(?:-[\w.-]+)?\]$", RegexOptions.CultureInvariant); + private static readonly Regex NpmManagerPin = new(@"^npm@\d+\.\d+\.\d+$", RegexOptions.CultureInvariant); + private static readonly Regex NpmImplicitInstall = new( + @"\b(?:npm\s+(?:install|i|ci)|npx\b|(?:vite|react-router|wrangler)\s+dev|--hmr)\b", RegexOptions.CultureInvariant); + private static readonly Regex NpmProductionReach = new( + @"\b(?:npm\s+(?:install|i|ci|exec)|npx\b|(?:vite|react-router|wrangler)\s+(?:dev|serve)|--hmr)\b", RegexOptions.CultureInvariant); + private static readonly Regex NpmRunCall = new(@"\bnpm\s+run\s+([\w:-]+)", RegexOptions.CultureInvariant); + private static readonly Regex NpmWorkspaceFlag = new(@"(?:--workspace(?:=|\s+)|-w\s+)([\w@/.-]+)", RegexOptions.CultureInvariant); + private static readonly Regex ManifestSpecialWorkspace = new(@"[*?\\]|^\.", RegexOptions.CultureInvariant); + private static readonly Regex PrivateNpmOwner = new(@"^@arcforges/(?!proto$|api-client$|web-)", RegexOptions.CultureInvariant); + private static readonly Regex NpmPrivateSpecifier = new( + @"^@arcforges/(?!proto$|api-client$|web-)|(?:^|/)(?:private|server|local-rpc|internal)(?:/|$)|^(?:https?:|file:|[A-Za-z]:[\\/])", + RegexOptions.CultureInvariant); + private static readonly string[] DependencySections = + ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]; + private static readonly string[] DevelopmentScripts = ["dev", "preview", "test:e2e"]; + private static readonly string[] NpmLockNames = + ["package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb"]; + private static readonly string[] SolutionFiles = ["win.slnx"]; + private static readonly string[] PackageLockOnly = ["package-lock.json"]; + + /// Audits the repository text inputs and returns every refusal, in a stable order. + public static IReadOnlyList Audit(IReadOnlyDictionary sources) + { + var context = new PolicyContext(sources); + AuditWorkspace(context); + AuditPins(context); + AuditNpmManifests(context); + AuditMsBuildProjects(context); + AuditReleaseGraph(context); + AuditCSharpSources(context); + AuditTypeScriptSources(context); + return context.Findings + .OrderBy(finding => finding.File, StringComparer.Ordinal) + .ThenBy(finding => finding.Rule, StringComparer.Ordinal) + .ThenBy(finding => finding.Detail, StringComparer.Ordinal) + .ToArray(); + } + + private static void AuditWorkspace(PolicyContext context) + { + var solutions = context.Find(name => name.EndsWith(".sln", StringComparison.Ordinal) + || name.EndsWith(".slnx", StringComparison.Ordinal)); + if (!solutions.SequenceEqual(SolutionFiles, StringComparer.Ordinal)) + context.Report("workspace", "win.slnx", "Exactly one solution, win.slnx, is allowed."); + + foreach (var required in new[] + { + "global.json", "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", + "NuGet.config", ".node-version", + }) + RequireRootFile(context, required); + + foreach (var file in context.Find(name => string.Equals(PolicyText.BaseName(name), "packages.config", StringComparison.OrdinalIgnoreCase))) + context.Report("workspace", file, "Legacy packages.config is not allowed; use central package management."); + + var nugetConfig = context.Text("NuGet.config") ?? string.Empty; + var nugetSources = Regex.Matches(nugetConfig, "]*>", RegexOptions.CultureInvariant) + .Select(match => PolicyText.Attribute(match.Value, "value")) + .ToArray(); + if (!Regex.IsMatch(nugetConfig, "", RegexOptions.CultureInvariant) + || nugetSources.Length != 1 + || nugetSources[0] != "https://api.nuget.org/v3/index.json") + context.Report("workspace", "NuGet.config", "Only the nuget.org source, after a clear, is admitted."); + + var global = PolicyText.ParseObject(context.Text("global.json")); + var sdk = global?["sdk"] as JsonObject; + if (sdk is null + || !ExactSdk.IsMatch(JsonString(sdk, "version")) + || JsonString(sdk, "rollForward") != "disable" + || JsonString(sdk, "allowPrerelease") != "false") + context.Report("pins", "global.json", "An exact .NET SDK pin with rollForward disabled is required."); + + foreach (var project in context.Find(name => name.EndsWith(".csproj", StringComparison.Ordinal))) + if (!context.Sources.ContainsKey(SiblingPath(project, "packages.lock.json"))) + context.Report("workspace", project, "Every C# project needs its sibling packages.lock.json."); + + foreach (var locked in context.Find(name => PolicyText.BaseName(name) == "packages.lock.json")) + if (!HasSiblingProject(context, locked)) + context.Report("workspace", locked, "A packages.lock.json without a sibling C# project is an orphan."); + + var npmLocks = context.Find(name => NpmLockNames.Contains(PolicyText.BaseName(name), StringComparer.Ordinal)); + if (!npmLocks.SequenceEqual(PackageLockOnly, StringComparer.Ordinal)) + context.Report("workspace", "package-lock.json", "One root npm v3 lock, and no alternate npm lock, is allowed."); + var npmLock = PolicyText.ParseObject(context.Text("package-lock.json")); + if (npmLock is null || JsonString(npmLock, "lockfileVersion") != "3") + context.Report("workspace", "package-lock.json", "The npm lock must be lockfileVersion 3."); + + var root = PolicyText.ParseObject(context.Text("package.json")); + if (root is null) + { + context.Report("workspace", "package.json", "The root package.json is missing or malformed."); + return; + } + if (root["workspaces"] is not JsonArray workspaceArray) + { + context.Report("workspace", "package.json", "One explicit root workspace array is required."); + return; + } + var workspaces = workspaceArray.Select(node => node?.GetValue() ?? string.Empty).ToArray(); + var expected = new[] { "package.json" }.Concat(workspaces.Select(name => name + "/package.json")) + .Order(StringComparer.Ordinal).ToArray(); + var manifests = context.Find(name => PolicyText.BaseName(name) == "package.json"); + if (!expected.SequenceEqual(manifests.Order(StringComparer.Ordinal), StringComparer.Ordinal) + || workspaces.Any(name => ManifestSpecialWorkspace.IsMatch(name))) + context.Report("workspace", "package.json", "Workspace manifest inventory must be exact."); + + foreach (var manifest in manifests) + { + var parsed = PolicyText.ParseObject(context.Text(manifest)); + if (parsed is null) + { + context.Report("workspace", manifest, "The npm manifest is malformed."); + continue; + } + if (manifest != "package.json" && parsed["workspaces"] is not null) + context.Report("workspace", manifest, "Nested workspace."); + var entry = npmLock?["packages"]?[PolicyText.DirectoryOf(manifest)] as JsonObject; + foreach (var section in DependencySections) + if (SectionText(parsed, section) != SectionText(entry, section)) + context.Report("workspace", manifest, "Lock differs: " + section); + } + } + + private static void RequireRootFile(PolicyContext context, string name) + { + var found = context.Find(file => string.Equals(PolicyText.BaseName(file), name, StringComparison.OrdinalIgnoreCase)); + if (found.Count != 1 || found[0] != name) + context.Report("workspace", name, "Exactly one root " + name + " is required."); + } + + private static void AuditPins(PolicyContext context) + { + var node = context.Text(".node-version")?.Trim(); + if (node is null || !ExactVersion.IsMatch(node)) + context.Report("pins", ".node-version", "An exact Node pin is required."); + + var root = PolicyText.ParseObject(context.Text("package.json")); + var devDependencies = root?["devDependencies"] as JsonObject; + if (!ExactVersion.IsMatch(JsonString(devDependencies, "typescript")) + || !ExactVersion.IsMatch(JsonString(devDependencies, "@react-router/dev"))) + context.Report("pins", "package.json", "Exact compiler and static generator pins are required."); + + var packageManager = JsonString(root, "packageManager"); + var engines = root?["engines"] as JsonObject; + if (node is null + || !ExactVersion.IsMatch(node) + || !NpmManagerPin.IsMatch(packageManager) + || !JsonString(engines, "node").Contains(node, StringComparison.Ordinal) + || !JsonString(engines, "npm").Contains(packageManager[4..], StringComparison.Ordinal)) + context.Report("pins", "package.json", "Exact Node and npm pins with matching engines are required."); + + foreach (var file in context.Find(name => name.EndsWith(".props", StringComparison.Ordinal) + || name.EndsWith(".csproj", StringComparison.Ordinal) + || name.EndsWith(".targets", StringComparison.Ordinal))) + foreach (Match download in Regex.Matches(context.Text(file) ?? string.Empty, "]*)>", RegexOptions.CultureInvariant)) + if (!ExactDownload.IsMatch(PolicyText.Attribute(download.Value, "Version") ?? string.Empty)) + context.Report("pins", file, "Every PackageDownload must name an exact bracketed version."); + + var central = context.Text("Directory.Packages.props") ?? string.Empty; + if (!Regex.IsMatch(central, "\\s*true\\s*<", RegexOptions.CultureInvariant)) + context.Report("pins", "Directory.Packages.props", "Central package management must be enabled."); + foreach (Match version in Regex.Matches(central, "]*>", RegexOptions.CultureInvariant)) + if (!ExactVersion.IsMatch(PolicyText.Attribute(version.Value, "Version") ?? string.Empty)) + context.Report("pins", "Directory.Packages.props", "Every central PackageVersion must be exact: " + PolicyText.Attribute(version.Value, "Include")); + + foreach (var file in context.Find(name => name.EndsWith(".csproj", StringComparison.Ordinal) + || name.EndsWith(".props", StringComparison.Ordinal) + || name.EndsWith(".targets", StringComparison.Ordinal))) + { + var xml = PolicyText.StripXmlComments(context.Text(file) ?? string.Empty); + if (Regex.IsMatch(xml, "]*\\bVersion\\s*=", RegexOptions.CultureInvariant) + || Regex.IsMatch(xml, "]*>[\\s\\S]*?", RegexOptions.CultureInvariant)) + context.Report("pins", file, "PackageReference must not carry a version; versions are central."); + } + } + + private static void AuditNpmManifests(PolicyContext context) + { + var manifests = context.Find(name => PolicyText.BaseName(name) == "package.json"); + var parsed = manifests.ToDictionary(name => name, name => PolicyText.ParseObject(context.Text(name)), StringComparer.Ordinal); + var workspaceByName = new Dictionary(StringComparer.Ordinal); + foreach (var manifest in manifests) + { + var name = JsonString(parsed[manifest], "name"); + if (name.Length > 0) + workspaceByName[name] = PolicyText.DirectoryOf(manifest); + } + + foreach (var manifest in manifests) + { + var document = parsed[manifest]; + if (document is null) + continue; + foreach (var section in DependencySections) + foreach (var (dependency, version) in Entries(document[section])) + { + var value = version?.ToJsonString().Trim('"') ?? string.Empty; + if (!ExactVersion.IsMatch(value)) + context.Report("pins", manifest, "Nonexact dependency " + dependency); + if (dependency.Contains("blazor", StringComparison.OrdinalIgnoreCase)) + context.Report("obsolete-target", manifest, dependency); + if (PrivateNpmOwner.IsMatch(dependency)) + context.Report("private-import", manifest, dependency); + if (JsonString(document, "license") == "Apache-2.0" + && dependency is "@arcforges/web-ui" or "react-dom") + context.Report("sdk-ui", manifest, dependency); + } + foreach (var (script, command) in Entries(document["scripts"])) + if (!DevelopmentScripts.Contains(script, StringComparer.Ordinal) + && NpmImplicitInstall.IsMatch(command?.ToJsonString().Trim('"') ?? string.Empty)) + context.Report("production-command", manifest, script + ": implicit install or development server"); + if (manifest != "package.json" && document["workspaces"] is not null) + context.Report("workspace", manifest, "Nested workspace."); + } + + foreach (var manifest in manifests) + { + var document = parsed[manifest]; + if (document is null) + continue; + foreach (var (script, _) in Entries(document["scripts"])) + if (!DevelopmentScripts.Contains(script, StringComparer.Ordinal)) + CheckNpmScript(context, parsed, workspaceByName, manifest, script, []); + } + + foreach (var manifest in manifests) + { + var document = parsed[manifest]; + if (document is null || JsonString(document, "license") != "Apache-2.0") + continue; + var seen = new HashSet(StringComparer.Ordinal); + var pending = new Stack(); + pending.Push(manifest); + while (pending.Count > 0) + { + var current = pending.Pop(); + if (!seen.Add(current)) + continue; + var currentDocument = parsed.TryGetValue(current, out var found) ? found : PolicyText.ParseObject(context.Text(current)); + if (JsonString(currentDocument, "license") != "Apache-2.0") + context.Report("sdk-ui", manifest, "Apache workspace reaches " + current); + foreach (var section in DependencySections) + foreach (var (dependency, _) in Entries(currentDocument?[section])) + if (workspaceByName.TryGetValue(dependency, out var directory)) + pending.Push(PolicyText.ManifestPath(directory)); + } + } + } + + private static void CheckNpmScript( + PolicyContext context, + IReadOnlyDictionary manifests, + IReadOnlyDictionary workspaceByName, + string file, + string name, + HashSet ancestors) + { + var key = file + ":" + name; + if (ancestors.Contains(key)) + { + context.Report("production-command", file, "Script cycle: " + key); + return; + } + var scripts = manifests.TryGetValue(file, out var manifest) ? manifest?["scripts"] as JsonObject : null; + if (scripts?[name]?.GetValue() is not { } script) + { + context.Report("production-command", file, "Unknown script: " + name); + return; + } + if (NpmProductionReach.IsMatch(script)) + context.Report("production-command", file, "Production script reaches dev/install: " + name); + var next = new HashSet(ancestors, StringComparer.Ordinal) { key }; + foreach (var command in script.Split(["&&", "||", ";"], StringSplitOptions.None)) + { + var call = NpmRunCall.Match(command); + if (!call.Success) + continue; + var workspace = NpmWorkspaceFlag.Match(command); + string? directory = null; + if (workspace.Success && !workspaceByName.TryGetValue(workspace.Groups[1].Value, out directory)) + { + context.Report("production-command", file, "Unknown script workspace: " + workspace.Groups[1].Value); + continue; + } + var target = workspace.Success ? PolicyText.ManifestPath(directory!) : file; + CheckNpmScript(context, manifests, workspaceByName, target, call.Groups[1].Value, next); + } + } + + /// Returns the JSON text of one dependency section, or the empty object text when it is absent. + private static string SectionText(JsonObject? document, string section) => + document?[section] is { } node ? node.ToJsonString() : "{}"; + + /// Returns a string member, or the literal text of a number or boolean member, or the empty string. + private static string JsonString(JsonNode? document, string name) + { + var node = document?[name]; + if (node is not JsonValue value) + return string.Empty; + return value.TryGetValue(out string? text) ? text ?? string.Empty : value.ToJsonString(); + } + + private static IEnumerable> Entries(JsonNode? node) => + node is JsonObject entries ? entries : Array.Empty>(); + + private static string SiblingPath(string file, string name) + { + var directory = PolicyText.DirectoryOf(file); + return directory.Length == 0 ? name : directory + "/" + name; + } + + private static bool HasSiblingProject(PolicyContext context, string lockPath) => + context.Find(name => name.EndsWith(".csproj", StringComparison.Ordinal) + && PolicyText.DirectoryOf(name) == PolicyText.DirectoryOf(lockPath)).Count > 0; +} diff --git a/tests/ArcForges.Web.Policy.Tests/Architecture/PolicyCore.cs b/tests/ArcForges.Web.Policy.Tests/Architecture/PolicyCore.cs new file mode 100644 index 0000000..b5ac3d0 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Architecture/PolicyCore.cs @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using System.Xml.Linq; + +namespace ArcForges.Web.Policy.Tests.Architecture; + +/// One refusal from the Web policy suite (the C# successor of the GOV.11 Node/TypeScript mechanism). +/// The rule family, for example wire-source or production-command. +/// The repository-relative POSIX path of the offending input. +/// A diagnostic for the reviewer. It never contains a secret. +public sealed record PolicyFinding(string Rule, string File, string Detail); + +/// +/// The text inputs of the policy suite, keyed by repository-relative POSIX path in ordinal order. The repository +/// inventory is the tracked and nonignored file set, exactly as the GOV.11 mechanism read it. +/// +public static class PolicySources +{ + private static readonly string[] AuditedExtensions = + [ + ".cs", ".razor", ".csproj", ".fsproj", ".vbproj", ".vcxproj", ".esproj", ".props", ".targets", ".slnx", ".sln", + ".ts", ".tsx", ".js", ".mjs", ".cjs", ".cts", ".mts", ".gradle", ".kts", ".lockfile", + ]; + + private static readonly string[] AuditedNames = + [ + "package.json", "package-lock.json", "packages.lock.json", "global.json", "nuget.config", "packages.config", + ".node-version", ".npmrc", "licence-boundary.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb", + "npm-shrinkwrap.json", "cmakelists.txt", "wrangler.json", + ]; + + /// Returns true when the policy suite reads the file. + public static bool IsAudited(string name) + { + var lower = PolicyText.BaseName(name).ToLowerInvariant(); + return AuditedNames.Contains(lower, StringComparer.Ordinal) + || AuditedExtensions.Any(extension => lower.EndsWith(extension, StringComparison.Ordinal)); + } + + /// Reads every audited tracked or untracked-but-not-ignored file of the repository. + public static IReadOnlyDictionary FromRepository(string root) + { + var listed = Git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"); + var files = new SortedDictionary(StringComparer.Ordinal); + foreach (var name in listed.Split('\0', StringSplitOptions.RemoveEmptyEntries).Distinct(StringComparer.Ordinal)) + { + if (!IsAudited(name)) + continue; + var path = Path.Combine(root, name.Replace('/', Path.DirectorySeparatorChar)); + files[name] = File.ReadAllText(path, Encoding.UTF8); + } + return files; + } + + /// Runs git with fixed arguments and returns its standard output. + public static string Git(string root, params string[] arguments) + { + var start = new ProcessStartInfo("git") + { + WorkingDirectory = root, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + }; + foreach (var argument in arguments) + start.ArgumentList.Add(argument); + using var process = Process.Start(start) ?? throw new InvalidOperationException("git could not be started."); + var errorTask = process.StandardError.ReadToEndAsync(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + var error = errorTask.GetAwaiter().GetResult(); + if (process.ExitCode != 0) + throw new InvalidOperationException("git " + string.Join(' ', arguments) + " failed: " + error); + return output; + } +} + +/// The mutable finding sink shared by the rule families of one audit. +internal sealed class PolicyContext +{ + private readonly List _findings = []; + + public PolicyContext(IReadOnlyDictionary sources) + { + Sources = sources; + } + + public IReadOnlyDictionary Sources { get; } + + public IReadOnlyList Findings => _findings; + + public void Report(string rule, string file, string detail) => + _findings.Add(new PolicyFinding(rule, file, detail)); + + public IReadOnlyList Find(Func predicate) => + Sources.Keys.Where(predicate).Order(StringComparer.Ordinal).ToArray(); + + public string? Text(string file) => Sources.TryGetValue(file, out var text) ? text : null; +} + +/// Path, comment and parse helpers shared by the rule families. +internal static class PolicyText +{ + private static readonly Regex XmlComment = new("", RegexOptions.CultureInvariant); + + public static string BaseName(string file) => file[(file.LastIndexOf('/') + 1)..]; + + public static string DirectoryOf(string file) => + file.IndexOf('/', StringComparison.Ordinal) >= 0 ? file[..file.LastIndexOf('/')] : string.Empty; + + public static string ManifestPath(string directory) => + directory is "" or "." ? "package.json" : directory + "/package.json"; + + /// Resolves a relative path against a directory, keeping leading parent segments. + public static string Join(string directory, string relative) => + Normalize(directory.Length == 0 ? relative : directory + "/" + relative); + + public static string Normalize(string path) + { + var parts = new List(); + foreach (var part in path.Replace('\\', '/').Split('/')) + { + if (part is "" or ".") + continue; + if (part == ".." && parts.Count > 0 && parts[^1] != "..") + parts.RemoveAt(parts.Count - 1); + else + parts.Add(part); + } + return string.Join('/', parts); + } + + /// Removes XML comments, replacing each with a space so that comments cannot join two fragments. + public static string StripXmlComments(string text) => XmlComment.Replace(text, " "); + + /// + /// Removes C-style comments while keeping string and template contents, so that code inside a comment is never + /// read as a dependency and a quoted fake import is still visible as text. + /// + public static string StripCode(string text, bool templates) + { + var builder = new StringBuilder(text.Length); + for (var index = 0; index < text.Length; index++) + { + var current = text[index]; + var next = index + 1 < text.Length ? text[index + 1] : '\0'; + if (current == '/' && next == '/') + { + while (index < text.Length && text[index] != '\n') + index++; + if (index < text.Length) + builder.Append('\n'); + continue; + } + if (current == '/' && next == '*') + { + var end = text.IndexOf("*/", index + 2, StringComparison.Ordinal); + var stop = end < 0 ? text.Length : end + 2; + builder.Append(' '); + index = stop - 1; + continue; + } + if (current == '"' || current == '\'' || (templates && current == '`')) + { + var verbatim = current == '"' && index > 0 && text[index - 1] == '@'; + var multiline = verbatim || (templates && current == '`'); + builder.Append(current); + index++; + while (index < text.Length) + { + var character = text[index]; + if (character == '\n' && !multiline) + { + builder.Append('\n'); + break; + } + if (character == '\\' && !verbatim) + { + builder.Append(character); + if (index + 1 < text.Length) + builder.Append(text[index + 1]); + index += 2; + continue; + } + if (character == current) + { + if (verbatim && index + 1 < text.Length && text[index + 1] == current) + { + builder.Append(character).Append(character); + index += 2; + continue; + } + builder.Append(character); + break; + } + builder.Append(character); + index++; + } + continue; + } + builder.Append(current); + } + return builder.ToString(); + } + + public static JsonObject? ParseObject(string? text) + { + if (text is null) + return null; + try + { + return JsonNode.Parse(text)?.AsObject(); + } + catch (JsonException) + { + return null; + } + } + + public static IEnumerable Values(string xml, string element) => + Regex.Matches(xml, "<" + element + "(?:\\s[^>]*)?>([^<]*)", RegexOptions.CultureInvariant) + .Select(match => match.Groups[1].Value); + + public static bool IsDocument(string? text, out XDocument? document) + { + document = null; + if (text is null) + return false; + try + { + document = XDocument.Parse(text); + return true; + } + catch (System.Xml.XmlException) + { + return false; + } + } + + /// Returns the value of an XML attribute from an element's opening tag text, or null. + public static string? Attribute(string tag, string name) + { + var match = Regex.Match(tag, "\\s" + name + "=\"([^\"]*)\"", RegexOptions.CultureInvariant); + return match.Success ? match.Groups[1].Value : null; + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs b/tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs new file mode 100644 index 0000000..bfc2b34 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using ArcForges.Web.Policy.Tests.Architecture; +using ArcForges.Web.Policy.Tests.Fixtures; +using ArcForges.Web.Policy.Tests.Licence; +using Xunit; + +namespace ArcForges.Web.Policy.Tests; + +/// +/// The GOV.11 successor refusal fixtures: the passing repository is admitted, every rule refuses its negative example +/// and the accepted forms are not refused. The fixture identifiers name the rule family and the sub-rule. +/// +public sealed class ArchitecturePolicyTests +{ + public static TheoryData RefusalIdentifiers() + { + var data = new TheoryData(); + foreach (var identifier in PolicyCases.Refusals.Keys.Order(StringComparer.Ordinal)) + data.Add(identifier); + return data; + } + + public static TheoryData AcceptedIdentifiers() + { + var data = new TheoryData(); + foreach (var identifier in PolicyCases.Accepted.Keys.Order(StringComparer.Ordinal)) + data.Add(identifier); + return data; + } + + [Fact] + public void PassingRepositoryIsAdmittedByEveryArchitectureAndLicenceRule() + { + var sources = PolicyBaseline.Create(); + Assert.Empty(ArchitecturePolicy.Audit(sources)); + Assert.Empty(LicencePolicy.Audit(sources)); + } + + [Theory] + [MemberData(nameof(RefusalIdentifiers))] + public void EveryRuleRefusesItsNegativeExample(string identifier) + { + var refusal = PolicyCases.Refusals[identifier]; + var sources = PolicyBaseline.Create(); + refusal.Mutate(sources); + var findings = ArchitecturePolicy.Audit(sources).Concat(LicencePolicy.Audit(sources)).ToArray(); + Assert.Contains(findings, finding => finding.Rule == refusal.Rule); + } + + [Theory] + [MemberData(nameof(AcceptedIdentifiers))] + public void AcceptedFormsAreNotRefused(string identifier) + { + var sources = PolicyBaseline.Create(); + PolicyCases.Accepted[identifier](sources); + Assert.Empty(ArchitecturePolicy.Audit(sources)); + Assert.Empty(LicencePolicy.Audit(sources)); + } + + [Fact] + public void EveryGovernedRuleFamilyHasANegativeExample() + { + var families = new[] + { + "workspace", "pins", "obsolete-target", "portable-reference", "production-command", "sdk-ui", "wire-source", + "private-import", "desktop-dom", "computed-import", "unresolved-import", "release-fixture", "inventory", + "boundary", "spdx", "override", "reference", "owner", "source-reference", "gradle", + }; + foreach (var family in families) + Assert.Contains(PolicyCases.Refusals.Values, refusal => refusal.Rule == family); + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs new file mode 100644 index 0000000..c5d924d --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; + +namespace ArcForges.Web.Policy.Tests.Fixtures; + +/// +/// The passing in-memory Web repository of the policy fixtures. It has the same shape as the real repository (one +/// solution, central packages, one locked C# project, one npm workspace, one TypeScript production root) and no +/// finding of any rule. Each refusal fixture changes one copy of it. +/// +public static class PolicyBaseline +{ + private const string Solution = """ + + + + + + """; + + private const string NuGetConfig = """ + + + + + + + + """; + + private const string BuildProps = """ + + + AGPL + AGPL-3.0-only + true + + + """; + + private const string CentralPackages = """ + + + true + + + + + + """; + + private const string UiProject = """ + + + + + + AGPL + AGPL-3.0-only + + + """; + + private const string UiLock = """ + {"version":2,"dependencies":{"net10.0":{"ArcForges.Contracts.PublicApi":{"type":"Direct","requested":"[1.0.0-ci.287.1, )","resolved":"1.0.0-ci.287.1"}}}} + """; + + private const string RootManifest = """ + {"name":"web","workspaces":["apps/site"],"packageManager":"npm@11.19.0","engines":{"node":">=24.21.0 <25","npm":">=11.19.0 <12"},"devDependencies":{"typescript":"7.0.2","@react-router/dev":"8.4.0"},"license":"AGPL-3.0-only","arcforges":{"licenceBoundary":"AGPL"}} + """; + + private const string SiteManifest = """ + {"name":"site","license":"AGPL-3.0-only","arcforges":{"licenceBoundary":"AGPL"}} + """; + + private const string RootLock = """ + {"lockfileVersion":3,"packages":{"":{"devDependencies":{"typescript":"7.0.2","@react-router/dev":"8.4.0"}},"apps/site":{}}} + """; + + private const string Inventory = """ + {"schemaVersion":1,"repository":"Web","spdxLicense":"AGPL-3.0-only","licenceBoundary":"AGPL","projects":[{"path":"apps/site/package.json","kind":"npm"},{"path":"package.json","kind":"npm"},{"path":"src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj","kind":"msbuild"}]} + """; + + /// Returns a new mutable copy of the passing repository. + public static Dictionary Create() => new(StringComparer.Ordinal) + { + ["win.slnx"] = Solution, + ["global.json"] = """{"sdk":{"version":"10.0.401","rollForward":"disable","allowPrerelease":false},"test":{"runner":"Microsoft.Testing.Platform"}}""", + ["NuGet.config"] = NuGetConfig, + ["Directory.Build.props"] = BuildProps, + ["Directory.Build.targets"] = "", + ["Directory.Packages.props"] = CentralPackages, + [".node-version"] = "24.21.0\n", + ["package.json"] = RootManifest, + ["package-lock.json"] = RootLock, + ["apps/site/package.json"] = SiteManifest, + ["apps/site/app/root.ts"] = "import type { Message } from \"@arcforges/proto\";\n\ndocument.title = \"Web DOM is allowed\";\n", + ["eng/policy/licence-boundary.json"] = Inventory, + ["src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj"] = UiProject, + ["src/ArcForges.Web.Ui/packages.lock.json"] = UiLock, + ["src/ArcForges.Web.Ui/Greeting.cs"] = "namespace ArcForges.Web.Ui;\n\npublic sealed class Greeting\n{\n}\n", + ["src/ArcForges.Web.Ui/Greeting.razor"] = "

Hello

\n", + }; + + /// Edits one JSON file of a copy in place. + public static void EditJson(Dictionary sources, string file, Action edit) + { + var document = JsonNode.Parse(sources[file])!.AsObject(); + edit(document); + sources[file] = document.ToJsonString(); + } + + /// Edits one XML-like file of a copy by replacing a fragment, failing when the fragment is absent. + public static void Replace(Dictionary sources, string file, string from, string to) + { + var text = sources[file]; + if (!text.Contains(from, StringComparison.Ordinal)) + throw new InvalidOperationException("The fixture fragment is absent from " + file + ": " + from); + sources[file] = text.Replace(from, to, StringComparison.Ordinal); + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs new file mode 100644 index 0000000..586cced --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: AGPL-3.0-only +namespace ArcForges.Web.Policy.Tests.Fixtures; + +/// One negative fixture: the rule that must refuse the mutated repository, and the mutation. +public sealed record PolicyCase(string Rule, Action> Mutate); + +/// +/// The negative examples of the GOV.11 successor, one for every rule family and sub-rule the Node/TypeScript mechanism +/// enforced, plus the accepted forms that the successor must not refuse (the passing examples). +/// +public static class PolicyCases +{ + private const string Ui = "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj"; + private const string Root = "package.json"; + private const string Site = "apps/site/package.json"; + private const string RootTs = "apps/site/app/root.ts"; + + /// Every refusal case, keyed by its stable identifier. + public static IReadOnlyDictionary Refusals { get; } = new Dictionary(StringComparer.Ordinal) + { + // workspace: one solution, one lock per project, one npm lock and manifest inventory + ["workspace-second-solution"] = new("workspace", s => s["ArcForges.sln"] = "Microsoft Visual Studio Solution File"), + ["workspace-unlocked-project"] = new("workspace", s => s["src/ArcForges.Web.Site/ArcForges.Web.Site.csproj"] = ""), + ["workspace-orphan-lock"] = new("workspace", s => s["tests/Orphan/packages.lock.json"] = "{}"), + ["workspace-packages-config"] = new("workspace", s => s["src/ArcForges.Web.Ui/packages.config"] = ""), + ["workspace-yarn-lock"] = new("workspace", s => s["yarn.lock"] = "alternate"), + ["workspace-nested-manifest"] = new("workspace", s => s["hidden/package.json"] = "{\"name\":\"hidden\"}"), + ["workspace-nested-workspaces"] = new("workspace", s => s["apps/site/package.json"] = "{\"name\":\"site\",\"workspaces\":[\"x\"],\"license\":\"AGPL-3.0-only\",\"arcforges\":{\"licenceBoundary\":\"AGPL\"}}"), + ["workspace-lock-differs"] = new("workspace", s => PolicyBaseline.EditJson(s, Site, o => o["devDependencies"] = new System.Text.Json.Nodes.JsonObject { ["left-pad"] = "1.3.0" })), + ["workspace-restore-without-lock"] = new("workspace", s => PolicyBaseline.Replace(s, "Directory.Build.props", "true", "false")), + ["workspace-alternate-nuget-source"] = new("workspace", s => PolicyBaseline.Replace(s, "NuGet.config", "", "")), + ["workspace-alternate-restore-source"] = new("workspace", s => PolicyBaseline.Replace(s, Ui, "", "https://example.invalid/index.json
")), + + // pins: exact SDK, Node and npm, central versions, no inline versions, exact downloads + ["pins-sdk-floating"] = new("pins", s => PolicyBaseline.Replace(s, "global.json", "\"version\":\"10.0.401\"", "\"version\":\"10.0.*\"")), + ["pins-sdk-roll-forward"] = new("pins", s => PolicyBaseline.Replace(s, "global.json", "\"rollForward\":\"disable\"", "\"rollForward\":\"latestMinor\"")), + ["pins-inline-version"] = new("pins", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["pins-floating-central-version"] = new("pins", s => PolicyBaseline.Replace(s, "Directory.Packages.props", "Version=\"1.0.0-ci.287.1\"", "Version=\"1.0.*\"")), + ["pins-central-management-off"] = new("pins", s => PolicyBaseline.Replace(s, "Directory.Packages.props", "true", "false")), + ["pins-download-unbracketed"] = new("pins", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["pins-npm-caret"] = new("pins", s => PolicyBaseline.EditJson(s, Root, o => ((System.Text.Json.Nodes.JsonObject)o["devDependencies"]!)["typescript"] = "^7.0.2")), + ["pins-package-manager-floating"] = new("pins", s => PolicyBaseline.EditJson(s, Root, o => o["packageManager"] = "npm@latest")), + ["pins-node-version-floating"] = new("pins", s => s[".node-version"] = "24\n"), + + // obsolete-target: Blazor WebAssembly standalone only + ["obsolete-blazor-server-package"] = new("obsolete-target", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["obsolete-interactive-server-markup"] = new("obsolete-target", s => s["src/ArcForges.Web.Ui/Greeting.razor"] = "@rendermode InteractiveServer\n

Hello

\n"), + ["obsolete-circuit-code"] = new("obsolete-target", s => s["src/ArcForges.Web.Ui/Host.cs"] = "namespace ArcForges.Web.Ui;\npublic static class Host { public static void Start(IServiceCollection services) => services.AddInteractiveServerComponents(); }\n"), + ["obsolete-npm-blazor-dependency"] = new("obsolete-target", s => PolicyBaseline.EditJson(s, Site, o => o["dependencies"] = new System.Text.Json.Nodes.JsonObject { ["blazor-server-kit"] = "1.0.0" })), + + // portable-reference and production-command + ["portable-esproj-reference"] = new("portable-reference", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["production-esproj-install"] = new("production-command", s => s["ArcForges.Web.esproj"] = "true"), + ["production-esproj-dev-build"] = new("production-command", s => s["ArcForges.Web.esproj"] = "npm run dev"), + ["production-exec-npm-install"] = new("production-command", s => PolicyBaseline.Replace(s, "Directory.Build.targets", "", "")), + ["production-exec-dotnet-restore"] = new("production-command", s => PolicyBaseline.Replace(s, "Directory.Build.targets", "", "")), + ["production-spa-proxy"] = new("production-command", s => PolicyBaseline.Replace(s, Ui, "", "npm run dev")), + ["production-script-vite-dev"] = new("production-command", s => PolicyBaseline.EditJson(s, Root, o => o["scripts"] = new System.Text.Json.Nodes.JsonObject { ["build"] = "vite dev" })), + ["production-script-install"] = new("production-command", s => PolicyBaseline.EditJson(s, Root, o => o["scripts"] = new System.Text.Json.Nodes.JsonObject { ["build"] = "npm install && vite build" })), + ["production-script-cycle"] = new("production-command", s => PolicyBaseline.EditJson(s, Root, o => o["scripts"] = new System.Text.Json.Nodes.JsonObject { ["build"] = "npm run other", ["other"] = "npm run build" })), + ["production-script-unknown"] = new("production-command", s => PolicyBaseline.EditJson(s, Root, o => o["scripts"] = new System.Text.Json.Nodes.JsonObject { ["build"] = "npm run missing" })), + + // sdk-ui: an Apache project never reaches an AGPL project + ["sdk-apache-csharp-reaches-agpl"] = new("sdk-ui", s => s["packages/sdk/Sdk.csproj"] = "ApacheApache-2.0"), + ["sdk-apache-npm-reaches-agpl"] = new("sdk-ui", s => s["packages/sdk/package.json"] = "{\"name\":\"@arcforges/web-sdk\",\"license\":\"Apache-2.0\",\"dependencies\":{\"site\":\"1.0.0\"}}"), + ["sdk-ts-imports-web-ui"] = new("sdk-ui", s => s["packages/sdk/src/index.ts"] = "export * from \"@arcforges/web-ui\";\n"), + + // wire-source: generated wire types only, the admitted codec and transport + ["wire-csharp-dto-declaration"] = new("wire-source", s => s["src/ArcForges.Web.Ui/LoginRequest.cs"] = "namespace ArcForges.Web.Ui;\npublic sealed record LoginRequest(string Name);\n"), + ["wire-csharp-json-codec"] = new("wire-source", s => s["src/ArcForges.Web.Ui/Codec.cs"] = "namespace ArcForges.Web.Ui;\npublic static class Codec { public static string Text() => System.Text.Json.JsonSerializer.Serialize(1); }\n"), + ["wire-csharp-direct-http"] = new("wire-source", s => s["src/ArcForges.Web.Ui/Fetcher.cs"] = "namespace ArcForges.Web.Ui;\npublic static class Fetcher { public static HttpClient Make() => new HttpClient(); }\n"), + ["wire-ts-json-codec"] = new("wire-source", s => s["apps/site/app/codec.ts"] = "export const text = JSON.stringify({ a: 1 });\n"), + ["wire-ts-direct-fetch"] = new("wire-source", s => s["apps/site/app/net.ts"] = "export const reply = fetch(\"/api\");\n"), + ["wire-ts-declared-request"] = new("wire-source", s => s["apps/site/app/model.ts"] = "export interface LoginRequest { token: string }\n"), + ["wire-ts-protobuf-codegen"] = new("wire-source", s => s["apps/site/app/codecs.ts"] = "import { messageDesc } from \"@bufbuild/protobuf/codegen\";\nexport const d = messageDesc;\n"), + + // private-import: no private, server or local-RPC namespace or package + // The import text is split so that the raw fixture file never contains the contiguous private import that the + // dependency gate scans for; the fixture value at run time is the same refused import. + ["private-csharp-namespace"] = new("private-import", s => s["src/ArcForges.Web.Ui/Bridge.cs"] = "using ArcForges" + ".Contracts.Internal;\nnamespace ArcForges.Web.Ui;\npublic static class Bridge { }\n"), + ["private-package-reference"] = new("private-import", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["private-npm-scope"] = new("private-import", s => PolicyBaseline.EditJson(s, Site, o => o["dependencies"] = new System.Text.Json.Nodes.JsonObject { ["@arcforges/operator-client"] = "1.0.0" })), + ["private-ts-package"] = new("private-import", s => s[RootTs] = "import \"@arcforges/operator-client\";\n"), + ["private-ts-local-rpc"] = new("private-import", s => s[RootTs] = "type T = import(\"@arcforges/local-rpc\").T;\n"), + ["private-ts-server-path"] = new("private-import", s => s[RootTs] = "const x = import(\"../server/private.ts\");\n"), + ["private-ts-sibling-escape"] = new("private-import", s => s[RootTs] = "import \"../../../../outside\";\n"), + + // desktop-dom: the desktop graph has no DOM or JavaScript interop + ["desktop-ts-dom"] = new("desktop-dom", s => s["packages/desktop/src/index.ts"] = "document.createElement('div');\n"), + ["desktop-csharp-js-interop"] = new("desktop-dom", s => s["desktop/Shell.cs"] = "namespace Desktop;\npublic sealed class Shell { public Microsoft.JSInterop.IJSRuntime? Js { get; set; } }\n"), + + // computed-import and unresolved-import + ["computed-ts-dynamic"] = new("computed-import", s => s[RootTs] = "const x = import(variable);\n"), + ["computed-csproj-reference"] = new("computed-import", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["unresolved-ts-relative"] = new("unresolved-import", s => s[RootTs] = "import \"./missing\";\n"), + ["unresolved-csproj-reference"] = new("unresolved-import", s => PolicyBaseline.Replace(s, Ui, "", "")), + + // release-fixture: no test, fixture, double or debug route in the release route graph + ["release-project-reference-tests"] = new("release-fixture", s => + { + s["tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj"] = ""; + PolicyBaseline.Replace(s, Ui, "", ""); + }), + ["release-csharp-test-double"] = new("release-fixture", s => s["src/ArcForges.Web.Ui/TestDoubles/Scripted.cs"] = "namespace ArcForges.Web.Ui.TestDoubles;\npublic sealed class Scripted { }\n"), + ["release-debug-route"] = new("release-fixture", s => s["src/ArcForges.Web.Ui/Greeting.razor"] = "@page \"/fixtures/greeting\"\n

Hello

\n"), + ["release-ts-test-helper"] = new("release-fixture", s => + { + s[RootTs] = "import \"../bridge\";\n"; + s["apps/site/bridge.ts"] = "import \"../../tests/helper\";\nexport const b = 1;\n"; + s["tests/helper.ts"] = "export const fake = true;\n"; + }), + ["release-ts-fixture-file"] = new("release-fixture", s => + { + s[RootTs] = "import \"./fixtures/secret\";\n"; + s["apps/site/app/fixtures/secret.ts"] = "export const fake = true;\n"; + }), + + // licence-boundary: inventory, boundary, overrides, references and first-party owners + ["licence-inventory-drift"] = new("inventory", s => s["unregistered/package.json"] = "{\"name\":\"unregistered\",\"license\":\"AGPL-3.0-only\",\"arcforges\":{\"licenceBoundary\":\"AGPL\"}}"), + ["licence-missing-boundary"] = new("boundary", s => PolicyBaseline.Replace(s, Ui, "AGPL", string.Empty)), + ["licence-apache-spdx"] = new("spdx", s => PolicyBaseline.Replace(s, Ui, "AGPL-3.0-only", "Apache-2.0")), + ["licence-props-override"] = new("override", s => PolicyBaseline.Replace(s, "Directory.Build.props", "AGPL", "Apache")), + ["licence-escaped-reference"] = new("reference", s => PolicyBaseline.Replace(s, Ui, "", "")), + ["licence-comment-split-boundary"] = new("boundary", s => PolicyBaseline.Replace(s, Ui, "AGPL", "oundary>AGPL")), + ["licence-npm-unknown-owner"] = new("owner", s => PolicyBaseline.EditJson(s, Site, o => o["dependencies"] = new System.Text.Json.Nodes.JsonObject { ["@arcforges/unknown"] = "1.0.0" })), + ["licence-npm-alias-owner"] = new("owner", s => PolicyBaseline.EditJson(s, Site, o => o["dependencies"] = new System.Text.Json.Nodes.JsonObject { ["alias"] = "npm:@arcforges/unknown@1.0.0" })), + ["licence-npm-unpublished-source"] = new("source-reference", s => PolicyBaseline.EditJson(s, Site, o => o["dependencies"] = new System.Text.Json.Nodes.JsonObject { ["local-lib"] = "file:../lib" })), + ["licence-locked-unknown-project"] = new("owner", s => PolicyBaseline.Replace(s, "src/ArcForges.Web.Ui/packages.lock.json", "\"ArcForges.Contracts.PublicApi\":{\"type\":\"Direct\"", "\"ArcForges.Contracts.Unknown\":{\"type\":\"Project\"},\"ArcForges.Contracts.PublicApi\":{\"type\":\"Direct\"")), + ["licence-locked-unknown-transitive"] = new("owner", s => PolicyBaseline.Replace(s, "src/ArcForges.Web.Ui/packages.lock.json", "\"ArcForges.Contracts.PublicApi\":{\"type\":\"Direct\"", "\"ArcForges.Contracts.Unknown\":{\"type\":\"Transitive\"},\"ArcForges.Contracts.PublicApi\":{\"type\":\"Direct\"")), + ["licence-lock-unknown-owner"] = new("owner", s => s["package-lock.json"] = "{\"lockfileVersion\":3,\"packages\":{\"node_modules/@arcforges/unknown\":{}}}"), + ["licence-gradle-declaration"] = new("gradle", s => s["build.gradle.kts"] = "extra[\"spdxLicense\"] = \"AGPL-3.0-only\"\nextra[\"licenceBoundary\"] = \"Apache\"\n"), + }; + + /// Accepted forms: each mutation keeps the repository admitted, so the audit must report nothing. + public static IReadOnlyDictionary>> Accepted { get; } = new Dictionary>>(StringComparer.Ordinal) + { + ["blazor-webassembly-standalone"] = s => PolicyBaseline.Replace(s, Ui, "", ""), + ["npm-ci-with-ignored-scripts-exec"] = s => PolicyBaseline.Replace(s, "Directory.Build.targets", "", ""), + ["admitted-json-codec"] = s => s["src/ArcForges.Web.App/Probe/WireJson.cs"] = "using System.Text.Json;\nnamespace ArcForges.Web.App.Probe;\npublic static class WireJson { public static object? Read(ReadOnlySpan bytes) => JsonSerializer.Deserialize(bytes); }\n", + ["admitted-http-probe"] = s => s["src/ArcForges.Web.App/Probe/SessionProbe.cs"] = "namespace ArcForges.Web.App.Probe;\npublic sealed class SessionProbe { public System.Net.Http.HttpRequestMessage Request() => new(System.Net.Http.HttpMethod.Get, \"/\"); }\n", + ["generated-alias-type"] = s => s["apps/site/app/alias.ts"] = "import type { SayHelloRequest } from \"@arcforges/proto\";\ntype HelloRequest = SayHelloRequest;\ntype OtherRequest = import(\"@arcforges/proto\").SayHelloRequest;\nexport type Keep = HelloRequest | OtherRequest;\n", + ["comments-and-strings-are-not-code"] = s => s[RootTs] = "// import x from \"@arcforges/private\";\nconst text = \"document @arcforges/private\";\ndocument.title = text;\n", + ["comment-mentions-json-and-http"] = s => s["src/ArcForges.Web.Ui/Notes.cs"] = "namespace ArcForges.Web.Ui;\n// JsonSerializer and new HttpClient are described here, not used.\npublic static class Notes { }\n", + }; +} diff --git a/tests/ArcForges.Web.Policy.Tests/Licence/LicencePolicy.cs b/tests/ArcForges.Web.Policy.Tests/Licence/LicencePolicy.cs new file mode 100644 index 0000000..0d6bfa5 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/Licence/LicencePolicy.cs @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.Json.Nodes; +using System.Text.RegularExpressions; +using ArcForges.Web.Policy.Tests.Architecture; + +namespace ArcForges.Web.Policy.Tests.Licence; + +/// +/// The licence-boundary rules of the GOV.11 successor, over the same inventory file (eng/policy/licence-boundary.json). +/// The inventory must match the tracked project files exactly; each project declares the AGPL boundary itself; imported +/// overrides, escaped or unregistered references, non-literal references, unknown first-party owners and unpublished +/// sources are refused. Comments are removed before any value is read, so a comment cannot join two fragments. +/// +public static class LicencePolicy +{ + private const string InventoryPath = "eng/policy/licence-boundary.json"; + private static readonly string[] MsBuildExtensions = [".csproj", ".fsproj", ".vbproj", ".vcxproj", ".esproj"]; + private static readonly string[] ImportedExtensions = [".props", ".targets"]; + private static readonly string[] SectionNames = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]; + private static readonly string[] InventoryKeys = ["licenceBoundary", "projects", "repository", "schemaVersion", "spdxLicense"]; + + private static readonly HashSet NpmOwners = new(StringComparer.Ordinal) + { + "@arcforges/proto", "@arcforges/api-client", "@arcforges/contract-fixtures", "@arcforges/ai-internal", + "@arcforges/ai", "@arcforges/cloud-workspace", "@arcforges/web-workspace", "@arcforges/web-app", + "@arcforges/web-site", "@arcforges/web-ui", + }; + + private static readonly HashSet ArcForgesOwners = new(StringComparer.Ordinal) + { + "arcforges.contracts.publicapi", "arcforges.contracts.foundation", "arcforges.build.policy", + }; + + private static readonly HashSet GithubOwners = new(StringComparer.Ordinal) + { + "io.github.arcforges:contracts-proto", "io.github.arcforges:contracts-client", + "io.github.arcforges:contracts-connect-client", "io.github.arcforges:contract-fixtures", + }; + + private static readonly Regex ProjectInclude = new( + "]*?Include=[\"']([^\"']+)[\"']", RegexOptions.CultureInvariant); + private static readonly Regex PackageIdentity = new( + "]*?(?:Include|Update)=[\"']([^\"']+)[\"']", RegexOptions.CultureInvariant); + private static readonly Regex NonLiteral = new(@"[$@*?;]", RegexOptions.CultureInvariant); + private static readonly Regex UnpublishedSource = new(@"^(?:file:|link:|git\+|\.\.?/)", RegexOptions.CultureInvariant); + private static readonly Regex GradleSource = new(@"includeBuild\s*\(|mavenLocal\s*\(", RegexOptions.CultureInvariant); + private static readonly Regex GradleProject = new(@"\bproject\(\s*[""']:", RegexOptions.CultureInvariant); + private static readonly Regex GradleOwner = new(@"io\.github\.arcforges:[A-Za-z0-9_.-]+", RegexOptions.CultureInvariant); + private static readonly Regex GradleFile = new(@"^(?:build|settings)\.gradle(?:\.kts)?$", RegexOptions.CultureInvariant); + + /// Audits the licence boundary of every audited project and returns every refusal. + public static IReadOnlyList Audit(IReadOnlyDictionary sources) + { + var findings = new List(); + void Report(string rule, string file, string detail) => findings.Add(new PolicyFinding(rule, file, detail)); + + var policy = PolicyText.ParseObject(sources.GetValueOrDefault(InventoryPath)); + if (policy is null) + { + Report("inventory", InventoryPath, "The licence inventory is missing or malformed."); + return findings; + } + if (!policy.Select(entry => entry.Key).Order(StringComparer.Ordinal).SequenceEqual(InventoryKeys, StringComparer.Ordinal) + || Scalar(policy, "schemaVersion") != "1" + || Scalar(policy, "repository") != "Web" + || Scalar(policy, "spdxLicense") != "AGPL-3.0-only" + || Scalar(policy, "licenceBoundary") != "AGPL") + Report("inventory", InventoryPath, "The licence inventory header is not the Web AGPL boundary."); + + var actual = sources.Keys + .Select(name => (Path: name, Kind: Kind(name))) + .Where(row => row.Kind is not null) + .Select(row => (row.Path, Kind: row.Kind!)) + .OrderBy(row => row.Path, StringComparer.Ordinal) + .ToArray(); + if (actual.Length == 0) + Report("inventory", InventoryPath, "No licence-bearing project is tracked."); + var declared = (policy["projects"] as JsonArray ?? new JsonArray()) + .Select(node => node as JsonObject) + .Where(row => row is not null) + .Select(row => (Path: Scalar(row, "path"), Kind: Scalar(row, "kind"))) + .OrderBy(row => row.Path, StringComparer.Ordinal) + .ToArray(); + if (!declared.SequenceEqual(actual)) + Report("inventory", InventoryPath, "Project licence inventory drift."); + + var npmNames = new Dictionary(StringComparer.Ordinal); + var projectNames = new HashSet(StringComparer.Ordinal); + var msbuildPaths = new HashSet(StringComparer.Ordinal); + foreach (var (path, kind) in actual) + { + if (kind == "msbuild") + { + msbuildPaths.Add(path); + projectNames.Add(PolicyText.BaseName(path)[..PolicyText.BaseName(path).LastIndexOf('.')].ToLowerInvariant()); + } + if (kind == "npm") + { + var name = Scalar(PolicyText.ParseObject(sources[path]), "name"); + if (name.Length > 0 && !npmNames.TryAdd(name, path)) + Report("inventory", path, "Duplicate npm identity: " + name); + } + } + + void CheckPackage(string name, string file) + { + var lower = name.ToLowerInvariant(); + if ((lower.StartsWith("@arcforges/", StringComparison.Ordinal) && !NpmOwners.Contains(name)) + || (lower.StartsWith("arcforges.", StringComparison.Ordinal) && !ArcForgesOwners.Contains(lower)) + || (lower.StartsWith("io.github.arcforges:", StringComparison.Ordinal) && !GithubOwners.Contains(name))) + Report("owner", file, "Unknown first-party package owner: " + name); + } + + foreach (var (path, kind) in actual) + { + var text = sources[path]; + switch (kind) + { + case "npm": + AuditManifest(path, text, npmNames, CheckPackage, Report); + break; + case "msbuild": + var xml = PolicyText.StripXmlComments(text); + ExpectSingle(PolicyText.Values(xml, "PackageLicenseExpression"), "AGPL-3.0-only", () => Report("spdx", path, "Incorrect SPDX licence expression.")); + ExpectSingle(PolicyText.Values(xml, "LicenceBoundary"), "AGPL", () => Report("boundary", path, "Incorrect licence boundary.")); + foreach (var name in PolicyText.Values(xml, "AssemblyName").Concat(PolicyText.Values(xml, "PackageId"))) + if (name.Length > 0) + projectNames.Add(name.ToLowerInvariant()); + break; + case "gradle": + foreach (var (key, value) in new[] { ("spdxLicense", "AGPL-3.0-only"), ("licenceBoundary", "AGPL") }) + if (!Regex.Matches(text, "extra\\[\"" + key + "\"\\]\\s*=\\s*\"([^\"\\n]+)\"", RegexOptions.CultureInvariant) + .Select(match => match.Groups[1].Value) + .SequenceEqual(new[] { value }, StringComparer.Ordinal)) + Report("gradle", path, "Incorrect Gradle declaration: " + key); + break; + default: + Report("build-system", path, "A new build system needs a reviewed licence verifier."); + break; + } + } + + foreach (var file in sources.Keys.Order(StringComparer.Ordinal)) + { + var text = sources[file]; + var name = PolicyText.BaseName(file); + if (MsBuildExtensions.Any(extension => name.EndsWith(extension, StringComparison.Ordinal)) + || ImportedExtensions.Any(extension => name.EndsWith(extension, StringComparison.Ordinal))) + { + var xml = PolicyText.StripXmlComments(text); + if (!PolicyText.Values(xml, "PackageLicenseExpression").All(value => value == "AGPL-3.0-only") + || !PolicyText.Values(xml, "LicenceBoundary").All(value => value == "AGPL")) + Report("override", file, "Imported licence override."); + foreach (Match reference in ProjectInclude.Matches(xml)) + { + var include = reference.Groups[1].Value; + if (NonLiteral.IsMatch(include)) + { + Report("reference", file, "Nonliteral reference: " + include); + continue; + } + var target = PolicyText.Join(PolicyText.DirectoryOf(file), include.Replace('\\', '/')); + if (!msbuildPaths.Contains(target)) + Report("reference", file, "Escaped/unregistered reference: " + include); + } + foreach (Match package in PackageIdentity.Matches(xml)) + CheckPackage(package.Groups[1].Value, file); + } + else if (name == "package-lock.json") + { + foreach (var (key, entry) in Entries(PolicyText.ParseObject(text)?["packages"])) + { + var identity = Scalar(entry as JsonObject, "name"); + if (identity.Length == 0) + identity = key.Split("node_modules/", StringSplitOptions.None).Last(); + if (!npmNames.ContainsKey(identity)) + CheckPackage(identity, file); + } + } + else if (name == "packages.lock.json") + { + foreach (var (_, framework) in Entries(PolicyText.ParseObject(text)?["dependencies"])) + foreach (var (dependency, entry) in Entries(framework)) + if (string.Equals(Scalar(entry as JsonObject, "type"), "project", StringComparison.OrdinalIgnoreCase)) + { + if (!projectNames.Contains(dependency.ToLowerInvariant())) + Report("owner", file, "Unknown locked project: " + dependency); + } + else + { + CheckPackage(dependency, file); + } + } + else if (file.EndsWith(".lockfile", StringComparison.Ordinal)) + { + foreach (var line in text.Split('\n').Where(line => line.Length > 0 && !line.StartsWith('#'))) + CheckPackage(string.Join(':', line.Split(':').Take(2)), file); + } + else if (GradleFile.IsMatch(name)) + { + if (GradleSource.IsMatch(text)) + Report("gradle", file, "Unpublished Gradle source."); + if (GradleProject.IsMatch(text)) + Report("gradle", file, "New Gradle project graph requires licence review."); + foreach (Match owner in GradleOwner.Matches(text)) + CheckPackage(owner.Value, file); + } + } + return findings; + } + + private static void AuditManifest( + string path, + string text, + IReadOnlyDictionary npmNames, + Action checkPackage, + Action report) + { + var manifest = PolicyText.ParseObject(text); + if (manifest is null) + { + report("inventory", path, "The npm manifest is malformed."); + return; + } + if (Scalar(manifest, "license") != "AGPL-3.0-only") + report("spdx", path, "Incorrect SPDX licence expression."); + if (manifest["arcforges"] is not JsonObject boundary || boundary.Count != 1 || Scalar(boundary, "licenceBoundary") != "AGPL") + report("boundary", path, "Incorrect licence boundary."); + foreach (var section in SectionNames) + foreach (var (name, node) in Entries(manifest[section])) + { + var version = node?.ToJsonString().Trim('"') ?? string.Empty; + if (UnpublishedSource.IsMatch(version)) + report("source-reference", path, "Unpublished source reference: " + name); + if (!npmNames.ContainsKey(name)) + checkPackage(name, path); + if (version.StartsWith("npm:", StringComparison.Ordinal)) + { + var alias = version[4..]; + var end = alias.IndexOf('@', 1); + checkPackage(end < 0 ? alias : alias[..end], path); + } + } + } + + private static void ExpectSingle(IEnumerable values, string expected, Action report) + { + var list = values.ToArray(); + if (list.Length != 1 || list[0] != expected) + report(); + } + + private static IEnumerable> Entries(JsonNode? node) => + node is JsonObject entries ? entries : Array.Empty>(); + + private static string Scalar(JsonNode? node, string name) + { + if (node?[name] is not JsonValue value) + return string.Empty; + return value.TryGetValue(out string? text) ? text ?? string.Empty : value.ToJsonString(); + } + + private static string? Kind(string file) + { + var name = PolicyText.BaseName(file); + if (name == "package.json") + return "npm"; + if (name is "build.gradle.kts" or "build.gradle") + return "gradle"; + if (name == "CMakeLists.txt") + return "cmake"; + return MsBuildExtensions.Any(extension => name.EndsWith(extension, StringComparison.Ordinal)) ? "msbuild" : null; + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs b/tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs new file mode 100644 index 0000000..010b0f0 --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Diagnostics; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json.Nodes; +using Xunit; + +namespace ArcForges.Web.Policy.Tests; + +/// +/// The forbidden-term scanner of WP-05.02 in the C# policy suite. The scanner and its product-name policy are the +/// published Contracts naming authority in node_modules; this suite verifies the pinned identity and runs the scanner +/// under python -I on an isolated local Git fixture, once for the current terms and once for every forbidden term. +/// +public sealed class NamingScannerTests +{ + private const string Clean = "ArcForges ArcScope"; + + [Fact] + public void PublishedNamingAuthorityMatchesTheAdmittedCandidate() + { + var root = RepositoryRoot.Find(); + var candidate = JsonNode.Parse(File.ReadAllText(Path.Combine(root, "eng", "policy", "naming-candidate.json")))!.AsObject(); + var package = NamingPackage(root, candidate["package"]!.GetValue()); + + var manifest = JsonNode.Parse(File.ReadAllText(Path.Combine(package, "package.json")))!.AsObject(); + Assert.Equal(candidate["version"]!.GetValue(), manifest["version"]!.GetValue()); + var source = JsonNode.Parse(File.ReadAllText(Path.Combine(package, "source.json")))!.AsObject(); + Assert.Equal(candidate["sourceCommit"]!.GetValue(), source["commit"]!.GetValue()); + foreach (var (asset, expected) in candidate["assets"]!.AsObject()) + Assert.Equal(expected!.GetValue(), Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(Path.Combine(package, asset)))).ToLowerInvariant()); + } + + [Fact] + public void PublishedScannerAcceptsCurrentTermsAndDetectsEveryForbiddenTerm() + { + var root = RepositoryRoot.Find(); + var package = NamingPackage(root, "@arcforges/proto"); + var script = Path.Combine(package, "tools", "naming", "eng", "check_naming.py"); + var policy = JsonNode.Parse(File.ReadAllText(Path.Combine(package, "tools", "naming", "eng", "policy", "product-names.json")))!.AsObject(); + + var repository = Path.Combine(Path.GetTempPath(), "web-naming-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(repository); + try + { + Git(repository, "init", "-q"); + Git(repository, "remote", "add", "origin", "https://github.com/ArcForges/Web.git"); + Git(repository, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", "-c", "commit.gpgsign=false", "commit", "--allow-empty", "-qm", "fixture"); + var probe = Path.Combine(repository, "probe.txt"); + + File.WriteAllText(probe, Clean, Encoding.UTF8); + var accepted = Scan(script, repository); + Assert.Equal(0, accepted.ExitCode); + + foreach (var term in policy["forbiddenNames"]!.AsArray()) + { + var name = term!["name"]!.GetValue(); + File.WriteAllText(probe, name, Encoding.UTF8); + var refused = Scan(script, repository); + Assert.True(refused.ExitCode == 1, "Forbidden term was not refused: " + name); + Assert.Contains("\"kind\": \"forbidden content\"", refused.Output, StringComparison.Ordinal); + } + } + finally + { + DeleteFixtureRepository(repository); + } + } + + /// Deletes the fixture repository. Git writes its object files read-only on Windows, so they are made writable first. + private static void DeleteFixtureRepository(string repository) + { + foreach (var file in Directory.EnumerateFiles(repository, "*", SearchOption.AllDirectories)) + File.SetAttributes(file, FileAttributes.Normal); + Directory.Delete(repository, recursive: true); + } + + private static string NamingPackage(string root, string package) + { + var parts = package.Split('/'); + var directory = Path.Combine([root, "node_modules", .. parts]); + Assert.True(Directory.Exists(directory), "The published naming package is not installed: " + package); + return directory; + } + + /// + /// The interpreter name for the scanner. Windows images name it python; the GitHub Ubuntu image provides python3 + /// and does not guarantee a python alias. + /// + private static string PythonInterpreter() => OperatingSystem.IsWindows() ? "python" : "python3"; + + private static (int ExitCode, string Output) Scan(string script, string repository) + { + var start = new ProcessStartInfo(PythonInterpreter()) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + }; + start.ArgumentList.Add("-I"); + start.ArgumentList.Add(script); + start.ArgumentList.Add("--repository"); + start.ArgumentList.Add("Web=" + repository); + using var process = Process.Start(start) ?? throw new InvalidOperationException("python could not be started."); + var errorTask = process.StandardError.ReadToEndAsync(); + var output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + errorTask.GetAwaiter().GetResult(); + return (process.ExitCode, output); + } + + private static void Git(string repository, params string[] arguments) + { + var start = new ProcessStartInfo("git") { WorkingDirectory = repository, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + foreach (var argument in arguments) + start.ArgumentList.Add(argument); + using var process = Process.Start(start) ?? throw new InvalidOperationException("git could not be started."); + var errorTask = process.StandardError.ReadToEndAsync(); + process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + errorTask.GetAwaiter().GetResult(); + Assert.Equal(0, process.ExitCode); + } +} diff --git a/tests/ArcForges.Web.Policy.Tests/RepositoryPolicyTests.cs b/tests/ArcForges.Web.Policy.Tests/RepositoryPolicyTests.cs new file mode 100644 index 0000000..16df48e --- /dev/null +++ b/tests/ArcForges.Web.Policy.Tests/RepositoryPolicyTests.cs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using ArcForges.Web.Policy.Tests.Architecture; +using ArcForges.Web.Policy.Tests.Licence; +using Xunit; + +namespace ArcForges.Web.Policy.Tests; + +/// The policy gate over the real Web repository: the tracked and nonignored inputs must pass every rule. +public sealed class RepositoryPolicyTests +{ + [Fact] + public void TheRepositoryPassesEveryArchitectureRule() + { + var sources = PolicySources.FromRepository(RepositoryRoot.Find()); + Assert.NotEmpty(sources); + Assert.Empty(ArchitecturePolicy.Audit(sources)); + } + + [Fact] + public void TheRepositoryPassesEveryLicenceBoundaryRule() + { + var sources = PolicySources.FromRepository(RepositoryRoot.Find()); + Assert.Empty(LicencePolicy.Audit(sources)); + } +} From dfbc8befce2e49df27d866c58d57ce9e763b82f4 Mon Sep 17 00:00:00 2001 From: sammiller Date: Thu, 8 Oct 2026 23:48:51 +0100 Subject: [PATCH 13/38] [WEB.40] Parity record for the C# public Site, with its stylesheet bytes (U5, step 1) Before the React sources are removed, the C# static Site is compared with the React prerender of 80900a1 (docs/web-40-site-parity.md): - index, hello and cloud-hello have equal element skeletons (99, 112 and 94 elements), equal visible text, titles, metadata, anchors, form controls, landmarks and noscript text. Only the React hydration scripts and modulepreload links differ (TB-01). - 404.html, 404.css, robots.txt and favicon.svg are byte identical. - The public stylesheet is now the React build's Tailwind v4.3.3 output, byte identical (site.d4912dbd6dc5e22b.css). The previous hand-written approximation differed in the system font stack, the placeholder colour and 15 selectors, which would have changed rendering. - The 17 security and cache header lines are identical to the React securityHeaders template; the CSP differs only by the seven React hydration script hashes, because the public pages carry no script. SiteOutputTests expects the Tailwind range-syntax breakpoint of the reviewed stylesheet. The Site suite passes 65/65. docs/web-40-site-parity.md records the open decision on the interactive /hello and /cloud-hello checks, which the static Site cannot run. eng/provenance/files.json lists the new record. Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-site-parity.md | 61 +++ eng/provenance/files.json | 1 + src/ArcForges.Web.Site/Assets/site.css | 398 +----------------- .../SiteOutputTests.cs | 4 +- 4 files changed, 67 insertions(+), 397 deletions(-) create mode 100644 docs/web-40-site-parity.md diff --git a/docs/web-40-site-parity.md b/docs/web-40-site-parity.md new file mode 100644 index 0000000..527f8e5 --- /dev/null +++ b/docs/web-40-site-parity.md @@ -0,0 +1,61 @@ +# WEB.40 public Site parity with the React prerender + +Status: recorded 2026-10-08 at source 80900a1 (WEB.40 unit U5, before the React sources were removed). + +This record shows that the C# static Site (`ArcForges.Web.Site`, first-party Razor `HtmlRenderer`) publishes the same +public pages, texts, metadata, element structure, stylesheet bytes, public files and security header values as the React +prerender it replaces. It is the evidence that allowed `apps/site`, `apps/app` and `packages/ui` to be removed. + +## Method + +- React reference: `npm run build --workspace @arcforges/web-site` with `VITE_SOURCE_REF=80900a1430c0fbb483e4a220e20980e3c18728b3` + (React Router prerender, Vite 8.3.0, Tailwind v4.3.3). The published client directory was copied before removal. +- C# output: `dotnet tools/ArcForges.Web.Tooling/bin/Release/net10.0/ArcForges.Web.Tooling.dll site build --out --source-ref 80900a1430c0fbb483e4a220e20980e3c18728b3`, + built twice with identical trees, after the stylesheet change below. +- Comparison: HTML parsed with the Python standard library. Compared per page: title, `lang`, every `meta`, every element + with its sorted attributes in document order (excluding `"; + + private static byte[] Utf8(string text) => Encoding.UTF8.GetBytes(text); + + private static Dictionary StandardFiles() => new(StringComparer.Ordinal) + { + ["index.html"] = Utf8(Shell), + ["_framework/blazor.webassembly.js"] = Utf8("// loader"), + ["_framework/dotnet.js"] = Utf8("// dotnet loader"), + ["_framework/dotnet.runtime.a1b2c3d4e5.js"] = Utf8("export {};"), + ["app.css"] = Utf8("body{margin:0}"), + }; + + /// A temporary publish directory. The recorded map is what the static web assets manifest names. + private sealed class Publish : IDisposable + { + public Publish(IReadOnlyDictionary files, IReadOnlyDictionary? recorded = null, bool writeManifest = true, bool writeWwwroot = true) + { + Root = Path.Combine(Path.GetTempPath(), "arcforges-bundle-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(Root); + if (!writeWwwroot) + return; + var wwwroot = Path.Combine(Root, "wwwroot"); + Directory.CreateDirectory(wwwroot); + foreach (var (path, bytes) in files) + { + var target = Path.Combine(wwwroot, path.Replace('/', Path.DirectorySeparatorChar)); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.WriteAllBytes(target, bytes); + } + if (writeManifest) + WriteManifest(recorded ?? files); + } + + public string Root { get; } + + public string Location => Root; + + private void WriteManifest(IReadOnlyDictionary recorded) + { + var endpoints = new System.Text.Json.Nodes.JsonArray(); + foreach (var (path, bytes) in recorded.OrderBy(pair => pair.Key, StringComparer.Ordinal)) + { + var integrity = "sha256-" + Convert.ToBase64String(SHA256.HashData(bytes)); + endpoints.Add(new System.Text.Json.Nodes.JsonObject + { + ["Route"] = path, + ["AssetFile"] = path, + ["EndpointProperties"] = new System.Text.Json.Nodes.JsonArray(new System.Text.Json.Nodes.JsonObject + { + ["Name"] = "integrity", + ["Value"] = integrity, + }), + }); + } + var document = new System.Text.Json.Nodes.JsonObject + { + ["Version"] = 1, + ["Endpoints"] = endpoints, + }; + File.WriteAllText(System.IO.Path.Combine(Root, "app.staticwebassets.endpoints.json"), document.ToJsonString()); + } + + public void Dispose() + { + if (Directory.Exists(Root)) + Directory.Delete(Root, recursive: true); + } + } + + private static ProfileBundleResult BuildStandard() + { + using var publish = new Publish(StandardFiles()); + return ProfileBundle.Build(publish.Location); + } + + [Fact] + public void AStandardPublishBuildsADigestNamedBundleThatVerifies() + { + using var publish = new Publish(StandardFiles()); + + var bundle = ProfileBundle.Build(publish.Location); + var summary = ProfileBundle.Verify(bundle.Archive, bundle.Digest); + var paths = TarArchive.Read(bundle.Archive).Select(entry => entry.Path).ToArray(); + + Assert.Equal(ProfileBundle.BundleName(bundle.Digest), bundle.Name); + Assert.Matches("^web-profiles-[a-f0-9]{64}\\.tar$", bundle.Name); + Assert.Equal(bundle.Members, summary.Members); + Assert.Equal(ProfileBundle.ManifestPath, paths[0]); + Assert.Contains("account/index.html", paths); + Assert.Contains("chat/index.html", paths); + Assert.Contains(ProfileBundle.HeadersPath, paths); + Assert.Contains("app.css", paths); + Assert.DoesNotContain(ProfileBundle.ShellPath, paths); + } + + [Fact] + public void TheSameBytesGiveTheSameBundleAndAChangedFileGivesADifferentOne() + { + using var first = new Publish(StandardFiles()); + using var second = new Publish(StandardFiles()); + var changedFiles = StandardFiles(); + changedFiles["app.css"] = Utf8("body{margin:1px}"); + using var changed = new Publish(changedFiles); + + var original = ProfileBundle.Build(first.Location); + + Assert.Equal(original.Digest, ProfileBundle.Build(second.Location).Digest); + Assert.NotEqual(original.Digest, ProfileBundle.Build(changed.Location).Digest); + } + + [Theory] + [InlineData("account/index.html")] + [InlineData("chat/index.html")] + [InlineData("chat/extra.html")] + [InlineData("_headers")] + [InlineData("manifest.json")] + public void APublishThatCarriesAGeneratedOrReservedBundlePathIsRefused(string path) + { + var files = StandardFiles(); + files[path] = Utf8("reserved"); + using var publish = new Publish(files); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("reserved bundle path: " + path, error.Message, StringComparison.Ordinal); + } + + [Fact] + public void APublishWithoutItsApplicationShellIsRefused() + { + var files = StandardFiles(); + files.Remove("index.html"); + using var publish = new Publish(files); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("no application shell", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void APublishWithoutItsWwwrootDirectoryIsRefused() + { + using var publish = new Publish(new Dictionary(), writeWwwroot: false); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("no wwwroot directory", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void AFileWhoseBytesDifferFromItsRecordedIntegrityIsRefused() + { + // Two builds that fill one path with different bytes: the manifest records the original bytes of app.css. + var recorded = StandardFiles(); + var files = StandardFiles(); + files["app.css"] = Utf8("body{margin:2px}"); + using var publish = new Publish(files, recorded); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("Served bytes differ from the build's integrity: app.css", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void AFileThatTheStaticWebAssetsManifestDoesNotNameIsRefused() + { + var files = StandardFiles(); + files["extra.txt"] = Utf8("unlisted"); + var recorded = StandardFiles(); + using var publish = new Publish(files, recorded); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("not named by the build's static web assets manifest", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void AManifestThatNamesAMissingFileIsRefused() + { + var recorded = StandardFiles(); + recorded["ghost.css"] = Utf8("body{}"); + using var publish = new Publish(StandardFiles(), recorded); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("The manifest names a missing file: ghost.css", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void APublishWithoutItsStaticWebAssetsManifestIsRefused() + { + using var publish = new Publish(StandardFiles(), writeManifest: false); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("exactly one static web assets manifest", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void AnUnfingerprintedFrameworkFileFailsTheBuildBeforeAnyBundleIsWritten() + { + var files = StandardFiles(); + files["_framework/app.js"] = Utf8("export {};"); + using var publish = new Publish(files); + + var error = Assert.Throws(() => ProfileBundle.Build(publish.Location)); + + Assert.Contains("Unfingerprinted framework file: _framework/app.js", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void WritingABundleLeavesOneDigestNamedArchiveAndKeepsOtherFiles() + { + using var publish = new Publish(StandardFiles()); + var output = Path.Combine(Path.GetTempPath(), "arcforges-bundle-out-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(output); + try + { + File.WriteAllText(Path.Combine(output, "web-profiles-" + new string('0', 64) + ".tar"), "stale"); + File.WriteAllText(Path.Combine(output, "unrelated.txt"), "kept"); + var bundle = ProfileBundle.Build(publish.Location); + + ProfileBundle.Write(output, bundle); + + var names = Directory.EnumerateFiles(output).Select(file => Path.GetFileName(file)!).Order(StringComparer.Ordinal).ToArray(); + Assert.Equal(new[] { "unrelated.txt", bundle.Name }.Order(StringComparer.Ordinal).ToArray(), names); + } + finally + { + Directory.Delete(output, recursive: true); + } + } + + [Fact] + public void AnArchiveWhoseMemberChangedIsRefusedByItsManifest() + { + var bundle = BuildStandard(); + var entries = TarArchive.Read(bundle.Archive) + .Select(entry => new TarArchive.Entry(entry.Path, (byte[])entry.Bytes.Clone())) + .ToList(); + var target = entries.Single(entry => entry.Path == "app.css"); + target.Bytes[0] ^= 1; + var tampered = TarArchive.Write(entries); + + var error = Assert.Throws(() => ProfileBundle.Verify(tampered)); + + Assert.Contains("Content of app.css", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void AnArchiveWithAnExtraMemberIsRefusedByItsManifest() + { + var bundle = BuildStandard(); + var entries = TarArchive.Read(bundle.Archive).ToList(); + entries.Add(new TarArchive.Entry("zz-extra.txt", Utf8("x"))); + var extra = TarArchive.Write(entries); + + var error = Assert.Throws(() => ProfileBundle.Verify(extra)); + + Assert.Contains("does not hold exactly the manifest's files", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void AnArchiveWhoseDigestDoesNotMatchIsRefused() + { + var bundle = BuildStandard(); + + var error = Assert.Throws(() => ProfileBundle.Verify(bundle.Archive, new string('a', 64))); + + Assert.Contains("Bundle digest mismatch.", error.Message, StringComparison.Ordinal); + } +} diff --git a/tests/ArcForges.Web.Tooling.Tests/packages.lock.json b/tests/ArcForges.Web.Tooling.Tests/packages.lock.json index 4bc3eb0..02b5f03 100644 --- a/tests/ArcForges.Web.Tooling.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Tooling.Tests/packages.lock.json @@ -176,4 +176,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/ArcForges.Web.Ui.Tests/packages.lock.json b/tests/ArcForges.Web.Ui.Tests/packages.lock.json index 2b49ecc..9e40c11 100644 --- a/tests/ArcForges.Web.Ui.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Ui.Tests/packages.lock.json @@ -483,4 +483,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalBuildIdentityBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalBuildIdentityBrowserTests.cs new file mode 100644 index 0000000..1d18759 --- /dev/null +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalBuildIdentityBrowserTests.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The local opt-in port of tests/browser/build-identity.spec.ts: a real browser reads the served build identity of the +// sealed candidate and compares it with the identity the candidate sealed. It skips unless LocalOptIn is enabled and +// never runs on CI (P2-017). +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Playwright; +using Xunit; + +namespace ArcForges.Web.Browser.Tests; + +public sealed class LocalBuildIdentityBrowserTests +{ + [Fact] + public async Task TheBrowserReadsTheSealedSourceRunIdentityWithNoStore() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + var sealedIdentity = Path.Combine(RepositoryRoot(), "artifacts", "candidate", "assets", "__build-info.json"); + if (!File.Exists(sealedIdentity)) + Assert.Skip("Build and verify the sealed candidate first (artifacts/candidate); see docs/validation.md."); + var expected = JsonNode.Parse(File.ReadAllText(sealedIdentity)); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + var page = await browser.NewPageAsync(); + await page.GotoAsync(new Uri(baseUrl, "/").ToString()); + var observed = await page.EvaluateAsync( + """ + async () => { + const response = await fetch('/__build-info.json', { cache: 'no-store' }); + if (!response.ok) throw new Error('Build identity returned ' + response.status); + return { identity: await response.json(), cache: response.headers.get('cache-control') }; + } + """); + + var identity = JsonNode.Parse(observed.GetProperty("identity").GetRawText()); + var cache = observed.GetProperty("cache").GetString() ?? string.Empty; + Assert.True(JsonNode.DeepEquals(expected, identity), "The served identity differs from the sealed identity."); + Assert.Contains("no-store", cache, StringComparison.Ordinal); + Assert.Contains("no-transform", cache, StringComparison.Ordinal); + } + + /// Walks up from the test output directory to the repository root (the directory with win.slnx). + private static string RepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null) + { + if (File.Exists(Path.Combine(directory.FullName, "win.slnx")) && File.Exists(Path.Combine(directory.FullName, "global.json"))) + return directory.FullName; + directory = directory.Parent; + } + throw new DirectoryNotFoundException("The repository root with win.slnx and global.json was not found."); + } +} diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalSiteBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalSiteBrowserTests.cs new file mode 100644 index 0000000..58343a4 --- /dev/null +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalSiteBrowserTests.cs @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Local opt-in checks of the static public Site (the successor of tests/browser/site.spec.ts). They skip unless LocalOptIn +// is enabled and never run on CI (P2-017). axe-core is injected only into the page under test, from a local script. +using System.Text.Json; +using Microsoft.Playwright; +using Xunit; + +namespace ArcForges.Web.Browser.Tests; + +public sealed class LocalSiteBrowserTests +{ + private static readonly string[] PublicPages = ["/", "/hello/", "/cloud-hello/"]; + + [Fact] + public async Task PublicPagesReadWithJavaScriptDisabled() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + await using var context = await browser.NewContextAsync(new BrowserNewContextOptions { JavaScriptEnabled = false }); + var page = await context.NewPageAsync(); + + foreach (var path in PublicPages) + { + await page.GotoAsync(new Uri(baseUrl, path).ToString()); + var heading = (await page.Locator("h1").First.TextContentAsync() ?? string.Empty).Trim(); + Assert.False(string.IsNullOrEmpty(heading), path); + Assert.Equal(0, await page.Locator("script").CountAsync()); + } + } + + [Fact] + public async Task ServerConnectionPageMakesNoAutomaticApiCall() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + var page = await browser.NewPageAsync(); + var apiRequests = new List(); + page.Request += (_, request) => + { + if (request.Url.Contains("/api/", StringComparison.Ordinal)) + apiRequests.Add(request.Url); + }; + + await page.GotoAsync(new Uri(baseUrl, "/cloud-hello/").ToString()); + await page.WaitForTimeoutAsync(500); + + Assert.Empty(apiRequests); + } + + [Fact] + public async Task PublicPagesFitANarrowScreenWithoutHorizontalScrolling() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + var page = await browser.NewPageAsync(new BrowserNewPageOptions { ViewportSize = new ViewportSize { Width = 375, Height = 812 } }); + + foreach (var path in PublicPages) + { + await page.GotoAsync(new Uri(baseUrl, path).ToString()); + var overflow = await page.EvaluateAsync( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth"); + Assert.False(overflow, path); + } + } + + [Fact] + public async Task PublicPagesPassTheAxeRulesWhenTheLocalScriptIsNamed() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var axeScript = environment.GetValueOrDefault(LocalOptIn.AxeScriptVariable); + if (string.IsNullOrEmpty(axeScript)) + Assert.Skip("Set ARCFORGES_AXE_CORE_PATH to a local axe-core script to run the axe rules."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await playwright.Chromium.LaunchAsync(); + var page = await browser.NewPageAsync(); + + foreach (var path in PublicPages) + { + await page.GotoAsync(new Uri(baseUrl, path).ToString()); + await page.AddScriptTagAsync(new PageAddScriptTagOptions { Path = axeScript }); + var result = await page.EvaluateAsync("async () => await axe.run()"); + Assert.Equal(0, result.GetProperty("violations").GetArrayLength()); + } + } +} diff --git a/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json b/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json index ce3af04..0d36a3a 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json +++ b/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json @@ -144,4 +144,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/browser/build-identity.spec.ts b/tests/browser/build-identity.spec.ts deleted file mode 100644 index 5232c86..0000000 --- a/tests/browser/build-identity.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { readFileSync } from "node:fs"; -import { expect, test } from "@playwright/test"; -import { expectedIdentity } from "../../tooling/build-identity.ts"; - -const manifest = JSON.parse( - readFileSync(new URL("../../artifacts/candidate/manifest.json", import.meta.url), "utf8"), -); - -test("the actual browser reads the sealed source/run and nine-axis report", async ({ page }) => { - await page.goto("/"); - const observed = await page.evaluate(async () => { - const response = await fetch("/__build-info.json", { cache: "no-store" }); - if (!response.ok) throw new Error(`Build identity returned ${response.status}`); - return { identity: await response.json(), cache: response.headers.get("cache-control") }; - }); - expect(observed.identity).toEqual(expectedIdentity(manifest.version)); - expect(observed.cache).toContain("no-store"); - expect(observed.cache).toContain("no-transform"); -}); diff --git a/tests/browser/cloud-hello-fixture.spec.ts b/tests/browser/cloud-hello-fixture.spec.ts deleted file mode 100644 index 741717e..0000000 --- a/tests/browser/cloud-hello-fixture.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Explicit browser wire fixtures; these do not call or validate a C# container. -import { expect, test } from "@playwright/test"; -import { helloApiPath } from "../../apps/site/app/cloud-hello"; -import { decodeHelloRequest, helloResponse } from "../fixtures/grpc-web"; - -test("published client reports unavailable API, then accepts a real protobuf response fixture", async ({ - page, - request, -}) => { - // Candidate-only: before Cloud owns /api/*, static assets must not fake an API response. - const missingApi = await request.get(helloApiPath); - expect(missingApi.status()).toBe(404); - expect(await missingApi.text()).toContain("Page not found"); - expect((await request.post(helloApiPath)).status()).toBe(405); - let requests = 0; - let unavailable = true; - await page.route("**/api/**", async (route) => { - requests++; - const request = route.request(); - expect(new URL(request.url()).pathname).toBe(helloApiPath); - expect(request.method()).toBe("POST"); - expect(request.headers()["content-type"]).toContain("application/grpc-web+proto"); - expect(request.headers().authorization).toBeUndefined(); - expect(decodeHelloRequest(request.postDataBuffer() ?? new Uint8Array()).name).toBe("ArcForges"); - if (unavailable) await route.fulfill({ status: 503, body: "Unavailable" }); - else - await route.fulfill({ - status: 200, - contentType: "application/grpc-web+proto", - body: Buffer.from(helloResponse()), - }); - }); - await page.goto("/cloud-hello/"); - const button = page.getByRole("button", { name: "Check connection" }); - await expect(button).toBeEnabled(); - expect(requests).toBe(0); - await button.click(); - await expect(page.getByRole("alert")).toContainText("server is unavailable"); - expect(requests).toBe(1); - unavailable = false; - await button.click(); - await expect(page.getByRole("status")).toContainText("Hello, ArcForges!"); - expect(requests).toBe(2); -}); diff --git a/tests/browser/site.spec.ts b/tests/browser/site.spec.ts deleted file mode 100644 index fdcda3d..0000000 --- a/tests/browser/site.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import AxeBuilder from "@axe-core/playwright"; -import { expect, test } from "@playwright/test"; -test("production pages hydrate under CSP and support keyboard greeting without network requests", async ({ - page, -}) => { - const errors: string[] = []; - page.on("pageerror", (error) => errors.push(error.message)); - page.on("console", (message) => { - if (message.type() === "error") errors.push(message.text()); - }); - await page.goto("/"); - await expect(page.getByRole("heading", { level: 1 })).toHaveText("Hello,world."); - await page.getByRole("link", { name: "Make it your hello" }).click(); - await expect(page.getByRole("button", { name: "Say hello" })).toBeEnabled(); - const requests: string[] = []; - page.on("request", (request) => requests.push(request.url())); - const input = page.getByLabel("Your name"); - await input.fill("世界 🌍"); - await input.press("Enter"); - await expect(page.getByRole("status")).toContainText("Hello, 世界 🌍!"); - await input.fill(" "); - await input.press("Enter"); - await expect(page.getByRole("alert")).toBeVisible(); - await input.fill(""); - await input.press("Enter"); - await expect(page.getByRole("alert")).toHaveCount(0); - await expect(page.getByRole("status")).toContainText(""); - expect(requests).toEqual([]); - expect(errors).toEqual([]); -}); -test("public content and navigation work without JavaScript", async ({ browser, baseURL }) => { - const context = await browser.newContext({ javaScriptEnabled: false }); - const page = await context.newPage(); - await page.goto(`${baseURL}/`); - await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); - await page.getByRole("link", { name: "Make it your hello" }).click(); - await expect(page.getByRole("status")).toContainText("Hello, World!"); - await expect(page.getByLabel("Your name")).toBeDisabled(); - await expect(page.getByRole("button", { name: "Say hello" })).toBeDisabled(); - await page.getByRole("link", { name: "Check the server connection" }).click(); - await expect(page.getByRole("button", { name: "Check connection" })).toBeDisabled(); - await context.close(); -}); -test("pages are accessible and fit narrow screens", async ({ page }, info) => { - for (const route of ["/", "/hello/", "/cloud-hello/"]) { - const pageName = route === "/" ? "home" : route.split("/")[1]; - await page.goto(route); - const results = await new AxeBuilder({ page }) - .withTags(["wcag2a", "wcag2aa", "wcag21aa"]) - .analyze(); - expect(results.violations).toEqual([]); - await page.setViewportSize({ width: 360, height: 800 }); - expect( - await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), - ).toBe(true); - await page.screenshot({ - path: info.outputPath(`${pageName}-mobile.png`), - fullPage: true, - }); - await page.setViewportSize({ width: 1440, height: 1000 }); - await page.screenshot({ - path: info.outputPath(`${pageName}-desktop.png`), - fullPage: true, - }); - } -}); -test("server connection page makes no automatic API calls", async ({ page }) => { - const requests: string[] = []; - const errors: string[] = []; - page.on("request", (request) => { - if (new URL(request.url()).pathname.startsWith("/api/")) requests.push(request.url()); - }); - page.on("pageerror", (error) => errors.push(error.message)); - await page.goto("/cloud-hello/"); - await expect(page.getByRole("heading", { level: 1 })).toContainText("from the server"); - await expect(page.getByRole("button", { name: "Check connection" })).toBeEnabled(); - await expect(page.getByRole("status")).toContainText("No request sent yet"); - expect(requests).toEqual([]); - expect(errors).toEqual([]); -}); -test("static delivery has security/cache headers and real missing-page and asset responses", async ({ - request, -}) => { - const home = await request.get("/"); - expect(home.status()).toBe(200); - expect(home.headers()["content-security-policy"]).toContain("script-src 'self' 'sha256-"); - expect(home.headers()["content-security-policy"]).not.toMatch(/unsafe-inline|unsafe-eval/); - expect(home.headers()["x-content-type-options"]).toBe("nosniff"); - expect(home.headers()["cache-control"]).toContain("no-transform"); - const html = await home.text(); - const asset = html.match(/href="(\/assets\/[^"]+\.css)"/)?.[1]; - expect(asset).toBeDefined(); - expect((await request.get(asset ?? "")).headers()["cache-control"]).toContain("immutable"); - expect((await request.get("/__build.json")).headers()["cache-control"]).toContain("no-store"); - for (const path of ["/no-such-page", "/assets/missing.js", "/__spa-fallback.html"]) { - const missing = await request.get(path); - expect(missing.status()).toBe(404); - expect(await missing.text()).toContain("Page not found"); - } -}); diff --git a/tests/fixtures/app-probe.ts b/tests/fixtures/app-probe.ts deleted file mode 100644 index 260d130..0000000 --- a/tests/fixtures/app-probe.ts +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Test-only fixtures for the Account/Chat profile probes. Never part of a release graph. -// They produce wire shapes with the published generated codecs; they do not call a C# server. -import { - serializeBrowserBootstrapResponseJson, - serializeBrowserReceiptJson, -} from "@arcforges/api-client"; - -export const origin = "https://account.example.test"; - -export const authenticated = { - csrfToken: "c".repeat(24), - authenticated: true, - session: { - sessionId: "6d1d4c2a-62a0-4b86-9d3f-0c6a3d0b5c11", - expiresAt: "2026-10-03T08:00:00.000000Z", - idleExpiresAt: "2026-10-02T20:30:00.000000Z", - userId: "0f0e6a30-5d1c-4c7e-8b53-5b6b7f9a4a10", - deviceId: "8f2a1d77-1c1b-4b0a-9a55-2f1d0e5c7b21", - workspaceIds: ["3f1d3b1e-2a8c-4c44-a1c8-77a1e8f0b9a1", "a2b84f55-0c2d-4b0d-8e6a-5d6e44a6c7d3"], - recoveryGeneration: "18446744073709551615", - purpose: "authenticate", - }, - profile: { - displayName: "Ada Lovelace", - locale: "en-GB", - timezone: "Europe/London", - revision: "9007199254740993", - }, -} as const; - -export const anonymous = { - csrfToken: "a".repeat(24), - authenticated: false, -} as const; - -export function bootstrapBody(value: unknown): Uint8Array { - return serializeBrowserBootstrapResponseJson( - value as Parameters[0], - ); -} - -export function receiptBody(effect = "happened"): Uint8Array { - return serializeBrowserReceiptJson({ - commandId: "7c9e6679-7425-40de-944b-e07fc1f90ae7", - effect, - }); -} - -export function jsonResponse(body: Uint8Array | string, init: ResponseInit = {}): Response { - return new Response(body as BodyInit, { - status: 200, - ...init, - headers: { "content-type": "application/json", ...(init.headers as object | undefined) }, - }); -} - -/** A response whose body is delivered in the given chunks, then optionally fails. */ -export function streamed( - chunks: Uint8Array[], - failWith?: Error, - init: ResponseInit = {}, -): Response { - let index = 0; - const body = new ReadableStream({ - pull(controller) { - const chunk = chunks[index++]; - if (chunk) controller.enqueue(chunk); - else if (failWith) controller.error(failWith); - else controller.close(); - }, - }); - return new Response(body, { - status: 200, - ...init, - headers: { "content-type": "application/json", ...(init.headers as object | undefined) }, - }); -} diff --git a/tests/fixtures/grpc-web.ts b/tests/fixtures/grpc-web.ts deleted file mode 100644 index 12afc4c..0000000 --- a/tests/fixtures/grpc-web.ts +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Test-only gRPC-Web fixture. Never included in the deployed static candidate. -import { SayHelloRequestSchema, SayHelloResponseSchema } from "@arcforges/proto"; -import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; - -export function frame(payload: Uint8Array, flag: number) { - const bytes = new Uint8Array(payload.length + 5); - bytes[0] = flag; - new DataView(bytes.buffer).setUint32(1, payload.length); - bytes.set(payload, 5); - return bytes; -} - -export function decodeHelloRequest(bytes: Uint8Array) { - if ( - bytes.length < 5 || - bytes[0] !== 0 || - new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(1) !== bytes.length - 5 - ) - throw new Error("Expected one uncompressed protobuf request frame"); - return fromBinary(SayHelloRequestSchema, bytes.slice(5)); -} - -export function helloResponse(message = "Hello, ArcForges!") { - const data = frame( - toBinary(SayHelloResponseSchema, create(SayHelloResponseSchema, { message })), - 0, - ); - const trailers = frame(new TextEncoder().encode("grpc-status: 0\r\n"), 0x80); - const body = new Uint8Array(data.length + trailers.length); - body.set(data); - body.set(trailers, data.length); - return body; -} diff --git a/tests/provenance/build-identity.test.ts b/tests/provenance/build-identity.test.ts index 3e4b0e6..02e4bee 100644 --- a/tests/provenance/build-identity.test.ts +++ b/tests/provenance/build-identity.test.ts @@ -108,7 +108,10 @@ test("published ContractSet preserves protobuf and JSON schema axes independentl const f = fixture(); const receipt = JSON.parse( readFileSync( - new URL("../../node_modules/@arcforges/api-client/build-identity.json", import.meta.url), + new URL( + "../../eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1/build-identity.json", + import.meta.url, + ), "utf8", ), ); diff --git a/tests/provenance/candidate.test.ts b/tests/provenance/candidate.test.ts deleted file mode 100644 index 0b7e090..0000000 --- a/tests/provenance/candidate.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import assert from "node:assert/strict"; -import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { test } from "node:test"; -import { candidate, digest, files, json, save, seal, verify } from "../../tooling/project.ts"; - -// A positive case must consume a real built candidate. There is no synthetic positive shortcut. -const baseline = await verify(); -test("the actual compiled candidate has the complete browser and legal closure", async () => { - const receipt = await json(path.join(candidate, "provenance/receipt.json")); - assert.equal(receipt.parsedModules, 303); - assert.equal(receipt.emittedModules, 136); - const bom = await json(path.join(candidate, "runtime-sbom.cdx.json")); - const names = new Set(bom.components.map((component: { name: string }) => component.name)); - for (const name of [ - "react", - "react-dom", - "react-router", - "scheduler", - "vite", - "rolldown", - "tailwindcss", - "turbo-stream", - "protobuf-wkt", - ]) - assert(names.has(name), `Missing emitted origin: ${name}`); - assert(!names.has("isbot") && !names.has("cookie-es")); -}); - -async function reseal(root: string) { - const receipt = await json(path.join(root, "provenance/receipt.json")); - receipt.members = Object.fromEntries( - await Promise.all( - (await files(root)) - .filter((file) => !["manifest.json", "provenance/receipt.json"].includes(file)) - .map(async (file) => [file, digest(await readFile(path.join(root, file)))]), - ), - ); - await save(path.join(root, "provenance/receipt.json"), receipt); - await seal(root, { source: baseline.source, version: baseline.version, dirty: baseline.dirty }); -} -function rejects(name: string, change: (root: string) => Promise, reason: RegExp) { - test(name, async () => { - const root = await mkdtemp(path.join(tmpdir(), "web-candidate-provenance-")); - try { - await cp(candidate, root, { recursive: true }); - await change(root); - await reseal(root); - await assert.rejects(verify(root, baseline.source), reason); - } finally { - assert.equal(path.dirname(root), tmpdir()); - assert(path.basename(root).startsWith("web-candidate-provenance-")); - await rm(root, { recursive: true, force: true }); - } - }); -} -async function edit(root: string, file: string, change: (text: string) => string) { - const full = path.join(root, file); - await writeFile(full, change(await readFile(full, "utf8"))); -} -rejects( - "reject executable changes after both manifests are resealed", - async (root) => { - const file = (await files(root)).find((file) => - /^assets\/assets\/entry.client-.*\.js$/u.test(file), - ); - assert(file); - await edit(root, file, (text) => `${text}\nfetch('/unreviewed');`); - }, - /independently reviewed two-build oracle/u, -); -rejects( - "reject added executable modules with plausible generated names", - async (root) => { - await writeFile(path.join(root, "assets/assets/extra-12345678.js"), "console.log('new');"); - }, - /Unclassified compiled browser resource/u, -); -rejects( - "reject omitted compiled modules", - async (root) => { - const file = (await files(root)).find((file) => /\/hello_pb-.*\.js$/u.test(file)); - assert(file); - await rm(path.join(root, file)); - }, - /independently reviewed two-build oracle/u, -); -rejects( - "reject added static files", - async (root) => { - await writeFile(path.join(root, "assets/new.txt"), "unreviewed"); - }, - /Unclassified public member/u, -); -rejects( - "reject HTML with a changed inline script", - async (root) => { - await edit(root, "assets/index.html", (text) => - text.replace("", ""), - ); - }, - /independently reviewed two-build oracle/u, -); -rejects( - "reject a forged Router fingerprint", - async (root) => { - const file = (await files(root)).find((file) => /\/manifest-.*\.js$/u.test(file)); - assert(file); - await edit(root, file, (text) => text.replace('"version":"', '"version":"0')); - }, - /Router manifest fingerprint mismatch/u, -); -rejects( - "reject a changed module graph", - async (root) => { - const graph = await json(path.join(root, "provenance/browser-graph.json")); - graph.modules[0].imports = []; - graph.modules[0].id = "foreign-source.js"; - await save(path.join(root, "provenance/browser-graph.json"), graph); - }, - /Candidate graph differs/u, -); -rejects( - "reject a substituted profile", - async (root) => { - const value = await json(path.join(root, "provenance/profile.json")); - value.templates = {}; - await save(path.join(root, "provenance/profile.json"), value); - }, - /Candidate substituted/u, -); -rejects( - "reject a modified used record", - async (root) => { - const file = "provenance/records/browser-resources-r2.json"; - const value = await json(path.join(root, file)); - value.review.rationale = "forged"; - await save(path.join(root, file), value); - }, - /Candidate record changed/u, -); -rejects( - "reject omitted original legal terms", - async (root) => { - await edit(root, "assets/third-party-notices.txt", (text) => - text.replaceAll("Evan Wallace", "Removed Author"), - ); - }, - /Required full third-party notices changed/u, -); -rejects( - "reject reformatting of immutable candidate records", - async (root) => { - await edit(root, "provenance/records/browser-resources-r2.json", (text) => `${text}\n`); - }, - /Candidate record bytes changed/u, -); -rejects( - "reject omitted full AGPL legal body", - async (root) => { - await writeFile(path.join(root, "assets/license.txt"), "AGPL-3.0-only\n"); - }, - /Full AGPL legal text changed/u, -); -rejects( - "reject weakened CSP after resealing", - async (root) => { - await edit(root, "assets/_headers", (text) => - text.replace("default-src 'self'", "default-src *"), - ); - }, - /Security\/cache headers changed/u, -); -rejects( - "reject browser SBOM dependency-edge tampering", - async (root) => { - const value = await json(path.join(root, "runtime-sbom.cdx.json")); - value.dependencies = []; - await save(path.join(root, "runtime-sbom.cdx.json"), value); - }, - /Emitted browser SBOM identities\/edges changed/u, -); -rejects( - "reject build SBOM identity tampering", - async (root) => { - const value = await json(path.join(root, "sbom.cdx.json")); - value.metadata.component.name = "foreign-checkout"; - await save(path.join(root, "sbom.cdx.json"), value); - }, - /Build SBOM identities\/edges changed/u, -); -rejects( - "reject changed Contracts source identity", - async (root) => { - await edit(root, "contracts/proto/source.json", (text) => - text.replace("1fb1dfaaaaa7a9f2f4c64a6e1c6a2b7de47d67b0", "a".repeat(40)), - ); - }, - /Published Contracts member changed/u, -); -rejects( - "reject added candidate-private files", - async (root) => { - await writeFile(path.join(root, "private-extra.txt"), "unclassified"); - }, - /Unclassified candidate member/u, -); - -rejects( - "reject build metadata mutation even after both manifests are resealed", - async (root) => { - const file = path.join(root, "assets/__build-info.json"); - const value = await json(file); - value.build.buildId = "another-run"; - await save(file, value); - }, - /Built identity differs/u, -); -rejects( - "reject axis mutation even after both manifests are resealed", - async (root) => { - const file = path.join(root, "assets/__build-info.json"); - const value = await json(file); - value.axes.ContractSet.values[0].version = "999"; - await save(file, value); - }, - /Built identity differs/u, -); diff --git a/tests/provenance/cloudflare-delivery.test.ts b/tests/provenance/cloudflare-delivery.test.ts new file mode 100644 index 0000000..93d3888 --- /dev/null +++ b/tests/provenance/cloudflare-delivery.test.ts @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The deployment helpers that remain in Node (tooling/cloudflare.ts) and the CI version rule. Ported from the vitest file +// tests/unit/delivery.test.ts; the candidate seal and CSP cases retire to tests/provenance/csharp-candidate.test.ts and the +// C# Site suite, which verify the same bytes. +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { + deploymentUrl, + requireCustomDomain, + waitForDelivery, + waitForIdentity, +} from "../../tooling/cloudflare.ts"; +import { releaseVersion } from "../../tooling/project.ts"; + +test("CI versions are unique across runs and reruns", () => { + assert.equal(releaseVersion("10", "2"), "0.1.0-ci.10.2"); + assert.equal(releaseVersion(undefined, undefined), "0.1.0-local"); + assert.throws(() => releaseVersion("1", "bad")); +}); + +test("live verification tolerates propagation with read-only requests", async () => { + const identity = { source: "a".repeat(40), version: "0.1.0-ci.1.1" }; + let calls = 0; + await waitForIdentity(deploymentUrl, identity, { + attempts: 3, + pause: async () => {}, + fetcher: async (_input, init) => { + assert.equal(init?.method, undefined); + calls++; + return Response.json(calls < 2 ? { ...identity, version: "old" } : identity); + }, + }); + assert.equal(calls, 2); + await assert.rejects( + waitForIdentity(deploymentUrl, identity, { + attempts: 2, + pause: async () => {}, + fetcher: async () => Response.json({ version: "old" }), + }), + /No redeployment/u, + ); + await assert.rejects( + waitForIdentity("https://other.example.com", identity), + /Unexpected deployment host/u, + ); +}); + +test("deployment requires the custom domain to belong to this production Worker", () => { + const domain = { + hostname: "arcforges.com", + service: "arcforges-web", + environment: "production", + }; + assert.doesNotThrow(() => requireCustomDomain([domain])); + for (const domains of [ + [], + [{ ...domain, service: "another-worker" }], + [{ ...domain, hostname: "other.example.com" }], + [{ ...domain, environment: "staging" }], + ]) + assert.throws(() => requireCustomDomain(domains), /Attach arcforges\.com/u); +}); + +test("asset verification waits for headers as well as identity and fails boundedly", async () => { + let reads = 0; + await waitForDelivery( + async (signal) => { + assert.equal(signal.aborted, false); + reads++; + if (reads === 1) throw new Error("Missing no-transform: /404.css"); + }, + { timeoutMs: 1000, intervalMs: 0 }, + ); + assert.equal(reads, 2); + await assert.rejects( + waitForDelivery( + async () => { + throw new Error("Deployed bytes differ: /hello/"); + }, + { timeoutMs: 10, intervalMs: 0 }, + ), + /no redeployment was attempted\. Error: Deployed bytes differ: \/hello\//u, + ); +}); diff --git a/tests/unit/lock-provenance.test.ts b/tests/provenance/lock-provenance.test.ts similarity index 75% rename from tests/unit/lock-provenance.test.ts rename to tests/provenance/lock-provenance.test.ts index acf75c4..3f7d1bb 100644 --- a/tests/unit/lock-provenance.test.ts +++ b/tests/provenance/lock-provenance.test.ts @@ -1,5 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0-only -import { expect, test } from "vitest"; +// The npm lock provenance rule (tooling/project.ts verifyLockProvenance). Ported from the vitest file +// tests/unit/lock-provenance.test.ts to node:test. The repository has no npm workspaces after the React removal, so a +// workspace link is refused in every form. +import assert from "node:assert/strict"; +import { test } from "node:test"; import { verifyLockProvenance } from "../../tooling/project.ts"; const registry = { @@ -10,7 +14,7 @@ const bundled = { inBundle: true }; test("accepts the bundled optional dependency layout reported by Dependabot", () => { // Preserve the failing layout as a fixture even if a future Tailwind release stops bundling it. - expect(() => + assert.doesNotThrow(() => verifyLockProvenance({ "node_modules/@tailwindcss/oxide-wasm32-wasi": { resolved: @@ -20,17 +24,17 @@ test("accepts the bundled optional dependency layout reported by Dependabot", () }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": bundled, }), - ).not.toThrow(); + ); }); test("nested bundled dependencies inherit the enclosing registry artifact", () => { - expect(() => + assert.doesNotThrow(() => verifyLockProvenance({ "node_modules/outer": registry, "node_modules/outer/node_modules/@scope/inner": bundled, "node_modules/outer/node_modules/@scope/inner/node_modules/leaf": bundled, }), - ).not.toThrow(); + ); }); test("rejects orphaned bundles, workspace ancestors and incomplete or foreign artifacts", () => { @@ -58,20 +62,21 @@ test("rejects orphaned bundles, workspace ancestors and incomplete or foreign ar }, }, ]) - expect(() => verifyLockProvenance(packages)).toThrow(); + assert.throws(() => verifyLockProvenance(packages)); }); -test("ordinary downloads and workspace links keep their provenance requirements", () => { - expect(() => +test("ordinary downloads keep their provenance requirements and workspace links are refused", () => { + assert.doesNotThrow(() => verifyLockProvenance({ "node_modules/outer": registry, - "node_modules/@arcforges/web-site": { link: true, resolved: "apps/site" }, }), - ).not.toThrow(); + ); for (const entry of [ { resolved: registry.resolved }, { ...registry, resolved: "https://example.com/outer.tgz" }, { link: true, resolved: "../other-repo" }, + { link: true, resolved: "apps/site" }, + { link: true, resolved: "packages/ui" }, ]) - expect(() => verifyLockProvenance({ "node_modules/outer": entry })).toThrow(); + assert.throws(() => verifyLockProvenance({ "node_modules/outer": entry })); }); diff --git a/tests/unit/app-bundle.test.ts b/tests/unit/app-bundle.test.ts deleted file mode 100644 index 6b72735..0000000 --- a/tests/unit/app-bundle.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { createHash } from "node:crypto"; -import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import { afterEach, expect, test } from "vitest"; -import { - buildBundle, - bundleName, - headersFile, - manifestName, - mergeProfiles, - profileCsp, - readTar, - verifyBundle, - writeBundle, - writeTar, -} from "../../apps/app/scripts/bundle.ts"; -import { pageFile, servedFile } from "../../apps/app/scripts/measure.ts"; - -const roots: string[] = []; -afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); -}); -const sha256 = (value: Uint8Array | string) => createHash("sha256").update(value).digest("hex"); - -function page(profile: string, script = `window.profile = "${profile}";`) { - return ``; -} - -function client(profile: "account" | "chat", extra: Record = {}) { - const root = mkdtempSync(join(tmpdir(), "cloud71-")); - roots.push(root); - const files: Record = { - [pageFile(profile)]: page(profile), - // The fallback shell the build writes at the root is never part of a bundle. - "index.html": "", - "__spa-fallback.html": "", - ".vite/manifest.json": "{}", - "favicon.svg": "", - "robots.txt": "User-agent: *\n", - "assets/entry.js": "export const entry = 1;", - "assets/root.css": "body{color:red}", - [`assets/${profile}-1234.js`]: `export const own = "${profile}";`, - ...extra, - }; - for (const [name, content] of Object.entries(files)) { - mkdirSync(dirname(join(root, name)), { recursive: true }); - writeFileSync(join(root, name), content); - } - return root; -} - -const clients = () => ({ account: client("account"), chat: client("chat") }); -const measure = (_client: string, profile: string) => ({ digest: sha256(`build ${profile}`) }); - -test("a request path maps to a served file and the root is not a page", () => { - expect(servedFile("/")).toBeUndefined(); - expect(servedFile("/index.html")).toBeUndefined(); - expect(servedFile("/__spa-fallback.html")).toBeUndefined(); - expect(servedFile("/.vite/manifest.json")).toBeUndefined(); - expect(servedFile("/chat/")).toBe("chat/index.html"); - expect(servedFile("/account/")).toBe("account/index.html"); - expect(servedFile("/assets/entry.js")).toBe("assets/entry.js"); - expect(servedFile("/favicon.svg")).toBe("favicon.svg"); -}); - -test("the merged tree holds each profile's page, one copy of the shared files and no fallback shell", () => { - const merged = mergeProfiles(clients()); - expect([...merged.keys()].sort()).toEqual([ - "account/index.html", - "assets/account-1234.js", - "assets/chat-1234.js", - "assets/entry.js", - "assets/root.css", - "chat/index.html", - "favicon.svg", - "robots.txt", - ]); -}); - -test("a path that two builds fill with different bytes refuses the bundle", () => { - expect(() => - mergeProfiles({ - account: client("account"), - chat: client("chat", { "assets/entry.js": "export const entry = 2;" }), - }), - ).toThrow(/different bytes/u); - expect(() => - mergeProfiles({ - account: client("account"), - chat: client("chat", { "favicon.svg": "other" }), - }), - ).toThrow(/favicon\.svg/u); -}); - -test("a build without its own page or with a foreign page is refused", () => { - const missing = client("chat"); - rmSync(join(missing, "chat/index.html")); - expect(() => mergeProfiles({ account: client("account"), chat: missing })).toThrow( - /chat has no chat\/index\.html/u, - ); - expect(() => - mergeProfiles({ - account: client("account", { "chat/index.html": page("chat") }), - chat: client("chat"), - }), - ).toThrow(/foreign path/u); - expect(() => - mergeProfiles({ - account: client("account", { "account/extra.html": "" }), - chat: client("chat"), - }), - ).toThrow(/foreign path/u); -}); - -test("each profile's policy is derived from its own page and applies to its own path only", () => { - const accountPolicy = profileCsp(page("account")); - const chatPolicy = profileCsp(page("chat")); - const hash = (script: string) => - `'sha256-${createHash("sha256").update(script).digest("base64")}'`; - expect(accountPolicy).toContain(hash('window.profile = "account";')); - expect(accountPolicy).not.toContain(hash('window.profile = "chat";')); - expect(chatPolicy).toContain(hash('window.profile = "chat";')); - expect(accountPolicy).toContain("connect-src 'self'"); - expect(accountPolicy).not.toContain("unsafe-inline"); - // The bytes a browser executes use LF; a CRLF page hashes to the same policy. - expect(profileCsp(page("account").replaceAll(";<", ";\r\n<"))).toBe( - profileCsp(page("account").replaceAll(";<", ";\n<")), - ); - const headers = headersFile({ account: accountPolicy, chat: chatPolicy }); - const blocks = headers.split(/\n(?=\/)/u); - const own = (path: string) => blocks.find((block) => block.startsWith(`${path}\n`)) ?? ""; - expect(own("/account/*")).toContain(`Content-Security-Policy: ${accountPolicy}`); - expect(own("/account/*")).not.toContain(chatPolicy); - expect(own("/chat/*")).toContain(`Content-Security-Policy: ${chatPolicy}`); - expect(own("/chat/*")).toContain("X-Frame-Options: DENY"); - expect(own("/*")).not.toContain("Content-Security-Policy"); - expect(own("/*")).toContain("X-Content-Type-Options: nosniff"); - expect(own("/assets/*")).toContain("! Cache-Control"); - expect(own("/assets/*")).toContain("max-age=31536000, immutable"); - expect(headers.split("\n").every((line) => line.length < 2000)).toBe(true); - expect(() => headersFile({ account: accountPolicy })).toThrow(/No policy for chat/u); - expect(() => headersFile({ account: "x".repeat(1800), chat: chatPolicy })).toThrow( - /header line budget/u, - ); -}); - -test("the archive is deterministic and reads back exactly what was written", () => { - const entries = [ - { path: manifestName, bytes: Buffer.from("{}\n") }, - { path: "assets/a.js", bytes: Buffer.alloc(512, 7) }, - { path: "assets/b.js", bytes: Buffer.alloc(0) }, - { path: "z.txt", bytes: Buffer.from("tail") }, - ]; - const first = writeTar(entries); - expect(sha256(writeTar(entries))).toBe(sha256(first)); - expect(first.length % 512).toBe(0); - const back = readTar(first); - expect(back.map((entry) => entry.path)).toEqual(entries.map((entry) => entry.path)); - for (const [index, entry] of back.entries()) - expect(Buffer.compare(Buffer.from(entry.bytes), entries[index]?.bytes ?? Buffer.alloc(0))).toBe( - 0, - ); - // No timestamp, owner or mode varies: the first header carries zeros and the fixed mode. - expect(first.subarray(100, 108).toString("ascii")).toBe("0000644\0"); - expect(first.subarray(136, 148).toString("ascii")).toBe("00000000000\0"); -}); - -test("the archive writer and reader refuse unsafe paths, links, damage and trailing data", () => { - for (const path of ["../x", "/abs", "a//b", "a/", "a b", "", "a/../b", "x".repeat(101)]) - expect(() => writeTar([{ path, bytes: Buffer.alloc(0) }]), path).toThrow(); - expect(() => - writeTar([ - { path: "a", bytes: Buffer.alloc(0) }, - { path: "a", bytes: Buffer.alloc(0) }, - ]), - ).toThrow(/Duplicate/u); - const good = writeTar([{ path: "a.txt", bytes: Buffer.from("hello") }]); - const symlink = Buffer.from(good); - symlink.write("2", 156, "ascii"); - expect(() => readTar(symlink)).toThrow(/plain regular file/u); - const flipped = Buffer.from(good); - flipped[0] = (flipped[0] ?? 0) ^ 1; - expect(() => readTar(flipped)).toThrow(); - expect(() => readTar(good.subarray(0, good.length - 512))).toThrow(); - expect(() => readTar(Buffer.concat([good, Buffer.alloc(512, 1)]))).toThrow(); - expect(() => readTar(Buffer.concat([good, Buffer.alloc(512)]))).toThrow(/after the terminator/u); - expect(() => readTar(Buffer.alloc(10))).toThrow(/Not a bundle/u); -}); - -test("equal content is the same bundle and its name is the digest of its own bytes", () => { - const first = buildBundle(clients(), measure); - const second = buildBundle(clients(), measure); - expect(second.digest).toBe(first.digest); - expect(first.digest).toBe(sha256(first.archive)); - expect(first.name).toBe(bundleName(first.digest)); - expect(first.name).toMatch(/^web-profiles-[0-9a-f]{64}\.tar$/u); - expect(Object.keys(first.manifest.files)).toEqual(Object.keys(first.manifest.files).sort()); - expect(first.manifest.files._headers?.sha256).toBe( - sha256(readTar(first.archive).find((entry) => entry.path === "_headers")?.bytes ?? ""), - ); - expect(first.manifest.profiles.account?.page).toBe("account/index.html"); - expect(first.manifest.profiles.chat?.csp).toBe(profileCsp(page("chat"))); - expect(readTar(first.archive)[0]?.path).toBe(manifestName); - const changed = buildBundle( - { - account: client("account", { "assets/root.css": "body{color:blue}" }), - chat: client("chat", { "assets/root.css": "body{color:blue}" }), - }, - measure, - ); - expect(changed.digest).not.toBe(first.digest); - const otherBuild = buildBundle(clients(), (_client, profile) => ({ - digest: sha256(`x ${profile}`), - })); - expect(otherBuild.digest).not.toBe(first.digest); -}); - -test("verification checks the digest, the exact file set and every file against the manifest", () => { - const bundle = buildBundle(clients(), measure); - expect(verifyBundle(bundle.archive, bundle.digest).schema).toBe(1); - expect(() => verifyBundle(bundle.archive, sha256("another"))).toThrow(/digest mismatch/u); - // A content change that keeps the size, with the new digest expected, is still caught by the manifest. - const entries = readTar(bundle.archive).map((entry) => ({ - path: entry.path, - bytes: Buffer.from(entry.bytes), - })); - const target = entries.find((entry) => entry.path === "assets/entry.js"); - expect(target).toBeDefined(); - if (!target) return; - target.bytes[0] = (target.bytes[0] ?? 0) ^ 1; - const tampered = writeTar(entries); - expect(() => verifyBundle(tampered, sha256(tampered))).toThrow(/Content of assets\/entry\.js/u); - const extra = writeTar([...entries, { path: "assets/zz.js", bytes: Buffer.from("x") }]); - expect(() => verifyBundle(extra, sha256(extra))).toThrow(/exactly the manifest's files/u); - const reordered = writeTar([ - entries[1] as (typeof entries)[0], - entries[0] as (typeof entries)[0], - ]); - expect(() => verifyBundle(reordered, sha256(reordered))).toThrow(/manifest must be the first/u); -}); - -test("writing a bundle leaves exactly one digest-named archive in the output directory", () => { - const out = mkdtempSync(join(tmpdir(), "cloud71-out-")); - roots.push(out); - writeFileSync(join(out, `web-profiles-${"0".repeat(64)}.tar`), "stale"); - writeFileSync(join(out, "unrelated.txt"), "kept"); - const written = writeBundle(clients(), out, measure); - expect(readdirSync(out).sort()).toEqual([written.name, "unrelated.txt"].sort()); - expect(sha256(readFileSync(join(out, written.name)))).toBe(written.digest); -}); diff --git a/tests/unit/app-hello.test.ts b/tests/unit/app-hello.test.ts deleted file mode 100644 index bf44ebc..0000000 --- a/tests/unit/app-hello.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { createPublicGrpcWebTransport } from "@arcforges/api-client"; -import { - EventService, - EventServicePollRequestSchema, - EventServicePollResponseSchema, - EventSchema, - EventServicePollValueSchema, - ResponseMetaSchema, - RevisionSchema, -} from "@arcforges/proto"; -import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; -import { Code, createClient } from "@connectrpc/connect"; -import { afterEach, expect, test, vi } from "vitest"; -import { failureFromCode, helloApiPath, sayHello } from "../../apps/app/app/probe/hello.ts"; -import { type FailureKind, ProbeFailure } from "../../apps/app/app/probe/failure.ts"; -import { decodeHelloRequest, frame, helloResponse } from "../fixtures/grpc-web.ts"; - -const origin = "https://chat.example.test"; -const grpcWeb = { "content-type": "application/grpc-web+proto" }; -const signal = () => new AbortController().signal; -const trailers = (text: string) => frame(new TextEncoder().encode(text), 0x80); -const concat = (...parts: Uint8Array[]) => { - const bytes = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); - let offset = 0; - for (const part of parts) { - bytes.set(part, offset); - offset += part.length; - } - return bytes; -}; - -afterEach(() => { - vi.useRealTimers(); -}); - -async function kindOf(task: Promise): Promise { - const error = await task.then( - () => undefined, - (reason: unknown) => reason, - ); - expect(error).toBeInstanceOf(ProbeFailure); - return (error as ProbeFailure).kind; -} - -test("the greeting is one anonymous binary gRPC-Web call to same-origin /api", async () => { - let seen: Request | undefined; - const reply = await sayHello(origin, "ArcForges 世界", signal(), async (input, init) => { - seen = new Request(input, init); - return new Response(helloResponse("Hello, ArcForges 世界!") as BodyInit, { headers: grpcWeb }); - }); - expect(reply).toBe("Hello, ArcForges 世界!"); - expect(seen?.url).toBe(`${origin}${helloApiPath}`); - expect(seen?.method).toBe("POST"); - expect(seen?.headers.get("content-type")).toContain("application/grpc-web+proto"); - expect(seen?.credentials).toBe("omit"); - expect(seen?.redirect).toBe("error"); - expect(seen?.headers.has("authorization")).toBe(false); - expect(seen?.headers.has("cookie")).toBe(false); - expect(decodeHelloRequest(new Uint8Array(await (seen as Request).arrayBuffer())).name).toBe( - "ArcForges 世界", - ); -}); - -test("every gRPC status maps to a closed failure kind and none is a success", async () => { - const expected: Record = { - [Code.Canceled]: "unexpected", - [Code.Unknown]: "unexpected", - [Code.InvalidArgument]: "rejected", - [Code.DeadlineExceeded]: "timeout", - [Code.NotFound]: "rejected", - [Code.AlreadyExists]: "rejected", - [Code.PermissionDenied]: "forbidden", - [Code.ResourceExhausted]: "limit", - [Code.FailedPrecondition]: "rejected", - [Code.Aborted]: "unavailable", - [Code.OutOfRange]: "rejected", - [Code.Unimplemented]: "rejected", - [Code.Internal]: "unexpected", - [Code.Unavailable]: "unavailable", - [Code.DataLoss]: "unexpected", - [Code.Unauthenticated]: "unauthenticated", - }; - for (const [code, kind] of Object.entries(expected)) { - expect(failureFromCode(Number(code) as Code), `code ${code}`).toBe(kind); - // The same status arriving as a real trailers-only gRPC-Web answer through the generated client. - const actual = await kindOf( - sayHello(origin, "x", signal(), async () => { - return new Response( - concat(trailers(`grpc-status: ${code}\r\ngrpc-message: Server%20text\r\n`)) as BodyInit, - { headers: grpcWeb }, - ); - }), - ); - expect(actual, `status ${code}`).toBe(kind); - } -}); - -test("a server message is never echoed into the failure", async () => { - const error = await sayHello( - origin, - "x", - signal(), - async () => - new Response(trailers("grpc-status: 3\r\ngrpc-message: secret-token-123\r\n") as BodyInit, { - headers: grpcWeb, - }), - ).then( - () => undefined, - (reason: unknown) => reason as ProbeFailure, - ); - expect(error?.kind).toBe("rejected"); - expect(error?.message).not.toContain("secret-token-123"); -}); - -test("HTTP statuses and non-gRPC answers are typed failures, never a local greeting", async () => { - const answers: [string, () => Response, FailureKind][] = [ - ["404", () => new Response("Not found", { status: 404 }), "rejected"], - ["405", () => new Response("Method not allowed", { status: 405 }), "rejected"], - ["429", () => new Response("Slow down", { status: 429 }), "limit"], - ["500", () => new Response("Broken", { status: 500 }), "unexpected"], - ["503", () => new Response(null, { status: 503 }), "unavailable"], - ["504", () => new Response(null, { status: 504 }), "timeout"], - [ - "html", - () => new Response("Not an API", { headers: { "content-type": "text/html" } }), - "malformed", - ], - [ - "valid frames under another content type", - () => - new Response(helloResponse() as BodyInit, { - headers: { "content-type": "application/json" }, - }), - "malformed", - ], - ["no content type", () => new Response(helloResponse() as BodyInit), "malformed"], - ]; - for (const [name, answer, kind] of answers) - expect(await kindOf(sayHello(origin, "ArcForges", signal(), async () => answer())), name).toBe( - kind, - ); - expect( - await kindOf( - sayHello(origin, "ArcForges", signal(), async () => { - throw new TypeError("Failed to fetch"); - }), - ), - ).toBe("unavailable"); -}); - -test("malformed frames and status never resolve", async () => { - const data = helloResponse("Hello, ArcForges!"); - const messageFrameLength = 5 + new DataView(data.buffer).getUint32(1); - const messageFrame = data.slice(0, messageFrameLength); - const bodies: [string, Uint8Array][] = [ - ["empty body", new Uint8Array()], - ["truncated header", messageFrame.slice(0, 3)], - ["truncated message", messageFrame.slice(0, messageFrameLength - 2)], - ["no trailers", messageFrame], - ["trailers without a status", concat(messageFrame, trailers("x-other: 1\r\n"))], - ["unparsable status", concat(messageFrame, trailers("grpc-status: ok\r\n"))], - [ - "compressed frame without negotiated compression", - concat(Uint8Array.of(1, ...messageFrame.slice(1)), trailers("grpc-status: 0\r\n")), - ], - [ - "frame length beyond the body", - concat(Uint8Array.of(0, 0, 0, 1, 0, 1), trailers("grpc-status: 0\r\n")), - ], - ["trailers only, status ok, no message", trailers("grpc-status: 0\r\n")], - ["random bytes", Uint8Array.from({ length: 64 }, (_, index) => (index * 37 + 11) & 0xff)], - ]; - for (const [name, bytes] of bodies) - expect( - await kindOf( - sayHello( - origin, - "ArcForges", - signal(), - async () => new Response(bytes as BodyInit, { headers: grpcWeb }), - ), - ), - name, - ).toBe("malformed"); -}); - -test("a reply that is not the greeting for this name is malformed", async () => { - expect( - await kindOf( - sayHello( - origin, - "ArcForges", - signal(), - async () => - new Response(helloResponse("Hello, someone else!") as BodyInit, { headers: grpcWeb }), - ), - ), - ).toBe("malformed"); -}); - -test("the user's cancellation aborts the request and is reported as cancelled", async () => { - const controller = new AbortController(); - let transportSignal: AbortSignal | undefined; - const pending = sayHello( - origin, - "ArcForges", - controller.signal, - (_input, init) => - new Promise((_resolve, reject) => { - transportSignal = init?.signal ?? undefined; - init?.signal?.addEventListener("abort", () => - reject(new DOMException("aborted", "AbortError")), - ); - }), - ); - await vi.waitFor(() => expect(transportSignal).toBeDefined()); - expect(transportSignal?.aborted).toBe(false); - controller.abort(); - expect(await kindOf(pending)).toBe("cancelled"); - expect(transportSignal?.aborted).toBe(true); -}); - -test("a call that has already been cancelled never reaches a success", async () => { - const controller = new AbortController(); - controller.abort(); - expect( - await kindOf( - sayHello(origin, "ArcForges", controller.signal, async (_input, init) => { - if (init?.signal?.aborted) throw new DOMException("aborted", "AbortError"); - return new Response(helloResponse() as BodyInit, { headers: grpcWeb }); - }), - ), - ).toBe("cancelled"); -}); - -test("a server that never answers ends at the ten second deadline as a timeout", async () => { - vi.useFakeTimers(); - const pending = kindOf( - sayHello( - origin, - "ArcForges", - signal(), - (_input, init) => - new Promise((_resolve, reject) => { - init?.signal?.addEventListener("abort", () => - reject(new DOMException("aborted", "AbortError")), - ); - }), - ), - ); - await vi.advanceTimersByTimeAsync(9_999); - let settled = false; - void pending.then(() => { - settled = true; - }); - await vi.advanceTimersByTimeAsync(0); - expect(settled).toBe(false); - await vi.advanceTimersByTimeAsync(2); - expect(await pending).toBe("timeout"); -}); - -test("the generated EventService client keeps 64-bit values exact over binary gRPC-Web", async () => { - // Fixture-labelled: the Cloud probe does not serve EventService. This exercises the generated client, - // transport and bigint mapping with values a JavaScript number cannot hold. - const seq = 9007199254740993n; - const generation = 18446744073709551615n; - const entitlement = 9223372036854775807n; - const revision = 9007199254740995n; - const response = create(EventServicePollResponseSchema, { - meta: create(ResponseMetaSchema, { - resultRev: create(RevisionSchema, { value: revision }), - entitlementVersion: entitlement, - recoveryGeneration: generation, - }), - outcome: { - case: "value", - value: create(EventServicePollValueSchema, { - events: [create(EventSchema, { subscriptionKey: "k", seq })], - }), - }, - }); - let request: Request | undefined; - const transport = createPublicGrpcWebTransport({ - baseUrl: `${origin}/api`, - fetch: async (input, init) => { - request = new Request(input, init); - return new Response( - concat( - frame(toBinary(EventServicePollResponseSchema, response), 0), - trailers("grpc-status: 0\r\n"), - ) as BodyInit, - { headers: grpcWeb }, - ); - }, - }); - const reply = await createClient(EventService, transport).poll({ - cursor: "c", - limit: 4294967295, - }); - expect(request?.url).toBe(`${origin}/api/arcforges.events.v1.EventService/Poll`); - const sent = fromBinary( - EventServicePollRequestSchema, - new Uint8Array(await (request as Request).arrayBuffer()).slice(5), - ); - expect(sent.cursor).toBe("c"); - expect(sent.limit).toBe(4294967295); - expect(reply.meta?.recoveryGeneration).toBe(generation); - expect(reply.meta?.entitlementVersion).toBe(entitlement); - expect(reply.meta?.resultRev?.value).toBe(revision); - expect(reply.outcome.case).toBe("value"); - const events = reply.outcome.case === "value" ? reply.outcome.value.events : []; - expect(events[0]?.seq).toBe(seq); - expect(typeof events[0]?.seq).toBe("bigint"); - expect(String(events[0]?.seq)).toBe("9007199254740993"); -}); diff --git a/tests/unit/app-profiles.test.ts b/tests/unit/app-profiles.test.ts deleted file mode 100644 index 9e32022..0000000 --- a/tests/unit/app-profiles.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import { afterEach, expect, test } from "vitest"; -import { activeProfile, isProfile, profiles } from "../../apps/app/profile.ts"; -import { - type Budgets, - checkBudgets, - initialResources, - measureProfile, - toBaseline, - verifyProfile, -} from "../../apps/app/scripts/measure.ts"; - -const roots: string[] = []; -afterEach(() => { - for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); -}); - -function build( - profile: "account" | "chat", - extra: Record = {}, - drop: string[] = [], -) { - const root = mkdtempSync(join(tmpdir(), "prf08-")); - roots.push(root); - const own = profile === "account" ? "/session/v1/bootstrap" : "application/grpc-web"; - const files: Record = { - [`${profile}/index.html`]: `source`, - "favicon.svg": "", - "assets/entry.js": `export const entry = "${"e".repeat(500)}";`, - "assets/route.js": `export const route = "${own}${"r".repeat(800)}";`, - "assets/root.css": `body{color:red}${"/*x*/".repeat(100)}`, - // The build also writes the single-page-app fallback shell at the root; it is not a page of either profile. - "index.html": - "", - "__spa-fallback.html": "", - ".vite/manifest.json": "{}", - ...extra, - }; - for (const name of drop) delete files[name]; - for (const [name, content] of Object.entries(files)) { - mkdirSync(dirname(join(root, name)), { recursive: true }); - writeFileSync(join(root, name), content); - } - return root; -} - -test("only the two named profiles exist and a build must name one", () => { - expect([...profiles]).toEqual(["account", "chat"]); - expect(isProfile("account")).toBe(true); - expect(isProfile("chat")).toBe(true); - for (const value of ["", "Account", "operator", undefined, 1, null]) - expect(isProfile(value)).toBe(false); - expect(activeProfile({ ARCFORGES_PROFILE: "chat" })).toBe("chat"); - expect(() => activeProfile({})).toThrow(/ARCFORGES_PROFILE/); - expect(() => activeProfile({ ARCFORGES_PROFILE: "site" })).toThrow(/ARCFORGES_PROFILE/); -}); - -test("initial resources are the preloaded modules, script sources and stylesheets, not anchors", () => { - const found = initialResources(readFileSync(join(build("chat"), "chat/index.html"), "utf8")); - expect(found.js).toEqual(["/assets/entry.js", "/assets/route.js"]); - expect(found.css).toEqual(["/assets/root.css"]); - expect(found.references).not.toContain("https://example.test/outside"); - expect(found.references).toContain("/favicon.svg"); -}); - -test("measurement counts only served files and requests, weighs gzip and fingerprints content", () => { - const root = build("chat"); - const measured = measureProfile(root, "chat"); - expect(measured.files).toBe(5); - expect(measured.initialRequests).toBe(4); - expect(measured.initialJs.bytes).toBe( - readFileSync(join(root, "assets/entry.js")).length + - readFileSync(join(root, "assets/route.js")).length, - ); - expect(measured.initialJs.gzip).toBeLessThan(measured.initialJs.bytes); - expect(measured.initialJs.gzip).toBeGreaterThan(0); - expect(measured.initialCss.bytes).toBe(readFileSync(join(root, "assets/root.css")).length); - expect(measured.totalJs).toEqual(measured.initialJs); - expect(measured.htmlBytes).toBe(readFileSync(join(root, "chat/index.html")).length); - const same = measureProfile(build("chat"), "chat"); - expect(same.digest).toBe(measured.digest); - writeFileSync( - join(root, "assets/route.js"), - `export const route = "application/grpc-web${"r".repeat(799)}";`, - ); - expect(measureProfile(root, "chat").digest).not.toBe(measured.digest); - const renamed = build("chat", { "assets/extra.js": "export {};" }); - expect(measureProfile(renamed, "chat").digest).not.toBe(measured.digest); - expect(measureProfile(renamed, "chat").totalJs.bytes).toBeGreaterThan(measured.totalJs.bytes); - expect(measureProfile(renamed, "chat").initialJs).toEqual(measured.initialJs); -}); - -test("a build with a missing initial resource, no entry page or a foreign reference is refused", () => { - expect(() => measureProfile(build("chat", {}, ["assets/route.js"]), "chat")).toThrow( - /missing from the build/, - ); - expect(() => measureProfile(build("chat", {}, ["chat/index.html"]), "chat")).toThrow( - /no prerendered chat\/index.html/, - ); - const absolute = build("chat", { - "chat/index.html": - '', - }); - expect(() => measureProfile(absolute, "chat")).toThrow(/Not same-origin/); - const protocolRelative = build("chat", { - "chat/index.html": '', - }); - expect(() => measureProfile(protocolRelative, "chat")).toThrow(/Not same-origin/); -}); - -test("the structural gates pass a clean profile and name every violation otherwise", () => { - expect(verifyProfile(build("account"), "account")).toEqual([]); - expect(verifyProfile(build("chat"), "chat")).toEqual([]); - expect(verifyProfile(build("chat"), "operator")).toEqual(["Unknown profile operator"]); - expect(verifyProfile(build("chat", { "assets/route.js.map": "{}" }), "chat")).toEqual([ - "Source map shipped: assets/route.js.map", - ]); - expect(verifyProfile(build("chat", { "node_modules/x.js": "" }), "chat")).toEqual([ - "Private file shipped: node_modules/x.js", - ]); - expect(verifyProfile(build("chat", { ".env": "A=1" }), "chat")).toEqual([ - "Private file shipped: .env", - ]); - expect(verifyProfile(build("chat", {}, ["chat/index.html"]), "chat")).toEqual([ - "No prerendered chat/index.html", - ]); - expect( - verifyProfile( - build("chat", { - "chat/index.html": - '', - }), - "chat", - ), - ).toEqual(["Reference is not same-origin: assets/route.js"]); - expect( - verifyProfile( - build("chat", { - "chat/index.html": - '', - }), - "chat", - ), - ).toEqual(["Reference is not same-origin: //cdn.example.test/a.js"]); -}); - -test("one profile's wire code must not reach the other profile", () => { - const crossed = build("chat", { "assets/extra.js": 'export const p = "/session/v1/bootstrap";' }); - expect(verifyProfile(crossed, "chat")).toEqual([ - "Profile chat contains the other profile's wire marker", - ]); - const crossedAccount = build("account", { - "assets/extra.js": 'export const p = "application/grpc-web";', - }); - expect(verifyProfile(crossedAccount, "account")).toEqual([ - "Profile account contains the other profile's wire marker", - ]); - expect(verifyProfile(build("chat", { "assets/route.js": "export {};" }), "chat")).toEqual([ - "Profile chat lost its own wire marker", - ]); - expect(verifyProfile(build("account", { "assets/route.js": "export {};" }), "account")).toEqual([ - "Profile account lost its own wire marker", - ]); -}); - -const budgets = (): Budgets => ({ - schema: 1, - regressionPercent: 10, - profiles: { - chat: { - initialRequests: 10, - initialJsGzip: 1000, - initialCssGzip: 200, - totalGzip: 1500, - htmlBytes: 3000, - }, - }, -}); - -test("the regression budget allows exactly ten percent growth per metric and catches a deliberate regression", () => { - const measured = measureProfile(build("chat"), "chat"); - const actual = toBaseline(measured); - expect(Object.keys(actual).sort()).toEqual( - ["htmlBytes", "initialCssGzip", "initialJsGzip", "initialRequests", "totalGzip"].sort(), - ); - expect(actual).toEqual({ - initialRequests: measured.initialRequests, - initialJsGzip: measured.initialJs.gzip, - initialCssGzip: measured.initialCss.gzip, - totalGzip: measured.totalJs.gzip + measured.totalCss.gzip, - htmlBytes: measured.htmlBytes, - }); - expect(actual.htmlBytes).toBeGreaterThan(0); - const exact: Budgets = { schema: 1, regressionPercent: 10, profiles: { chat: { ...actual } } }; - expect(checkBudgets(measured, exact)).toEqual([]); - // A baseline whose ten percent ceiling equals the measurement passes; one byte lower fails. - for (const key of Object.keys(actual) as (keyof typeof actual)[]) { - const value = actual[key]; - const baseline = Math.ceil((value * 100) / 110); - const at: Budgets = { - schema: 1, - regressionPercent: 10, - profiles: { chat: { ...actual, [key]: baseline } }, - }; - expect(Math.floor((baseline * 110) / 100)).toBeGreaterThanOrEqual(value); - expect(checkBudgets(measured, at), key).toEqual([]); - const below: Budgets = { - schema: 1, - regressionPercent: 10, - profiles: { chat: { ...actual, [key]: Math.floor((value * 100) / 110) - 1 } }, - }; - const problems = checkBudgets(measured, below); - expect(problems, key).toHaveLength(1); - expect(problems[0]).toContain(key); - } - const regressed = build("chat", { - "assets/entry.js": `export const entry = "${Array.from({ length: 4000 }, (_, i) => i * 7919).join(",")}";`, - }); - expect( - checkBudgets(measureProfile(regressed, "chat"), { - ...budgets(), - profiles: { chat: { ...actual } }, - }).length, - ).toBeGreaterThan(0); -}); - -test("a budget needs a baseline for the profile it judges", () => { - expect(checkBudgets(measureProfile(build("account"), "account"), budgets())).toEqual([ - "No baseline for profile account", - ]); -}); - -test("the committed baseline covers both profiles with positive measured values", () => { - const committed = JSON.parse( - readFileSync(new URL("../../apps/app/budgets.json", import.meta.url), "utf8"), - ) as Budgets; - expect(committed.schema).toBe(1); - expect(committed.regressionPercent).toBe(10); - expect(Object.keys(committed.profiles).sort()).toEqual([...profiles]); - for (const baseline of Object.values(committed.profiles)) - for (const value of Object.values(baseline)) { - expect(Number.isInteger(value)).toBe(true); - expect(value).toBeGreaterThan(0); - } -}); diff --git a/tests/unit/app-proof.test.ts b/tests/unit/app-proof.test.ts deleted file mode 100644 index bd1893a..0000000 --- a/tests/unit/app-proof.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { createHash, generateKeyPairSync, verify } from "node:crypto"; -import { expect, test } from "vitest"; -import { - assertProofOrigin, - checkInteractionBudgets, - forwardTarget, - isOwnLoopbackRequest, - forwardedHeaders, - type InteractionBudgets, - nextCookie, - operatorMessage, - signOperatorRequest, - summarize, -} from "../../apps/app/scripts/proof-lib.ts"; - -test("an operator request is signed over the exact message the Worker verifies", () => { - const { privateKey, publicKey } = generateKeyPairSync("ed25519"); - const body = new TextEncoder().encode('{"a":1}'); - const header = signOperatorRequest( - privateKey, - "POST", - "proof.example.test", - "/proof/v1/session/issue", - body, - { nowSeconds: 1_790_000_000, nonce: "AAAAAAAAAAAAAAAAAAAAAA" }, - ); - const match = /^AF-Operator t=(\d{10}),n=([A-Za-z0-9_-]{22}),s=([A-Za-z0-9_-]{86})$/u.exec( - header, - ); - expect(match).not.toBeNull(); - const [, time = "", nonce = "", signature = ""] = match ?? []; - const digest = createHash("sha256").update(body).digest("hex"); - const message = operatorMessage( - "POST", - "proof.example.test", - "/proof/v1/session/issue", - time, - nonce, - digest, - ); - expect(message).toBe( - `AF-OPERATOR-V2\nPOST\nproof.example.test\n/proof/v1/session/issue\n1790000000\nAAAAAAAAAAAAAAAAAAAAAA\n${digest}`, - ); - expect(verify(null, Buffer.from(message), publicKey, Buffer.from(signature, "base64url"))).toBe( - true, - ); - // A different host, path or body does not verify. - for (const other of [ - operatorMessage("POST", "other.example.test", "/proof/v1/session/issue", time, nonce, digest), - operatorMessage("POST", "proof.example.test", "/proof/v1/exact", time, nonce, digest), - operatorMessage( - "POST", - "proof.example.test", - "/proof/v1/session/issue", - time, - nonce, - "0".repeat(64), - ), - ]) - expect(verify(null, Buffer.from(other), publicKey, Buffer.from(signature, "base64url"))).toBe( - false, - ); -}); - -test("the forwarder replaces Origin, injects only the session cookie and drops every other header", () => { - const headers = forwardedHeaders( - { - origin: "http://127.0.0.1:5000", - cookie: "other=1; __Host-af_session=page", - authorization: "Bearer leak", - accept: "application/json", - "x-af-csrf": "token", - "content-type": "application/grpc-web+proto", - "x-grpc-web": "1", - host: "127.0.0.1:5000", - }, - "https://proof.example.test", - "handle", - ); - expect(headers).toEqual({ - origin: "https://proof.example.test", - accept: "application/json", - "x-af-csrf": "token", - "content-type": "application/grpc-web+proto", - "x-grpc-web": "1", - cookie: "__Host-af_session=handle", - }); - expect( - forwardedHeaders({ cookie: "__Host-af_session=page" }, "https://p.test", undefined), - ).toEqual({ - origin: "https://p.test", - }); -}); - -test("the cookie jar follows the server's Set-Cookie like a browser", () => { - const set = "__Host-af_session=abc; Path=/; Secure; HttpOnly; SameSite=Strict"; - expect(nextCookie(undefined, [set])).toBe("abc"); - expect(nextCookie("abc", [])).toBe("abc"); - expect(nextCookie("abc", ["other=1; Path=/"])).toBe("abc"); - expect(nextCookie("abc", ["__Host-af_session=; Path=/; Secure; Max-Age=0"])).toBeUndefined(); - expect( - nextCookie("abc", ["__Host-af_session=zzz; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT"]), - ).toBeUndefined(); - expect(nextCookie("abc", ["__Host-af_session=def; Path=/; Max-Age=60"])).toBe("def"); -}); - -test("summaries are exact for odd and even sample counts", () => { - expect(summarize([30, 10, 20])).toEqual({ samples: 3, minMs: 10, medianMs: 20, maxMs: 30 }); - expect(summarize([10, 20, 30, 41])).toEqual({ samples: 4, minMs: 10, medianMs: 25, maxMs: 41 }); - expect(() => summarize([])).toThrow(); -}); - -const budgets: InteractionBudgets = { - schema: 1, - basis: "test", - minimumSamples: 3, - interactions: { a: { medianCeilingMs: 100, maxCeilingMs: 200 } }, -}; - -test("interaction budgets pass at the ceiling and fail one millisecond above it", () => { - const ok = { a: { samples: 3, minMs: 1, medianMs: 100, maxMs: 200 } }; - expect(checkInteractionBudgets(ok, budgets)).toEqual([]); - expect(checkInteractionBudgets({ a: { ...ok.a, medianMs: 101 } }, budgets)).toEqual([ - "a: median 101 ms over 100 ms", - ]); - expect(checkInteractionBudgets({ a: { ...ok.a, maxMs: 201 } }, budgets)).toEqual([ - "a: max 201 ms over 200 ms", - ]); - expect(checkInteractionBudgets({ a: { ...ok.a, samples: 2 } }, budgets)).toEqual([ - "a: 2 samples, 3 required", - ]); - expect(checkInteractionBudgets({}, budgets)).toEqual(["a: not measured"]); - expect(checkInteractionBudgets({ ...ok, b: ok.a }, budgets)).toEqual([ - "b: measured but has no budget", - ]); -}); - -test("only the three exact routes are forwarded, to fixed targets", () => { - expect(forwardTarget("/session/v1/bootstrap")).toBe("/session/v1/bootstrap"); - expect(forwardTarget("/session/v1/logout")).toBe("/session/v1/logout"); - expect(forwardTarget("/api/arcforges.hello.v1.HelloService/SayHello")).toBe( - "/api/arcforges.hello.v1.HelloService/SayHello", - ); - for (const other of [ - "/api/", - "/api/healthz", - "/api/arcforges.hello.v1.HelloService/SayHello/", - "/api/arcforges.hello.v1.HelloService/SayHello2", - "/session/v1/", - "/session/v1/bootstrap/", - "/session/v1/bootstrap/../logout", - "/proof/v1/session/issue", - "/assets", - "/", - "", - ]) - expect(forwardTarget(other)).toBeUndefined(); -}); - -test("a live run targets the proof origin unless another https origin is opted in", () => { - expect(assertProofOrigin(undefined, undefined)).toBe("https://proof.arcforges.com"); - expect(assertProofOrigin("https://proof.arcforges.com", undefined)).toBe( - "https://proof.arcforges.com", - ); - expect(() => assertProofOrigin("https://arcforges.com", undefined)).toThrow( - /unless PROOF_ALLOW_OTHER_ORIGIN=1/u, - ); - expect(() => assertProofOrigin("https://arcforges.com", "yes")).toThrow(); - expect(assertProofOrigin("https://other.example.test", "1")).toBe("https://other.example.test"); - expect(() => assertProofOrigin("http://proof.arcforges.com", "1")).toThrow(/https origin/u); - expect(() => assertProofOrigin("https://proof.arcforges.com/x", "1")).toThrow(/https origin/u); -}); - -test("the loopback server answers only its own Host and own Origin", () => { - expect(isOwnLoopbackRequest({ host: "127.0.0.1:5000" }, 5000)).toBe(true); - expect( - isOwnLoopbackRequest({ host: "127.0.0.1:5000", origin: "http://127.0.0.1:5000" }, 5000), - ).toBe(true); - expect( - isOwnLoopbackRequest({ host: "127.0.0.1:5000", origin: "https://evil.example" }, 5000), - ).toBe(false); - expect(isOwnLoopbackRequest({ host: "rebound.example:5000" }, 5000)).toBe(false); - expect(isOwnLoopbackRequest({ host: "127.0.0.1:5001" }, 5000)).toBe(false); - expect(isOwnLoopbackRequest({}, 5000)).toBe(false); -}); - -test("every allow-listed header is forwarded, including the gRPC-Web deadline", () => { - const headers = forwardedHeaders( - { "grpc-timeout": "10000m", "x-user-agent": "ua" }, - "https://p.test", - undefined, - ); - expect(headers["grpc-timeout"]).toBe("10000m"); - expect(headers["x-user-agent"]).toBe("ua"); -}); - -test("an empty value or Max-Age=0 clears the cookie on its own", () => { - expect(nextCookie("abc", ["__Host-af_session=; Path=/"])).toBeUndefined(); - expect(nextCookie("abc", ["__Host-af_session=zzz; Path=/; Max-Age=0"])).toBeUndefined(); - expect(nextCookie("abc", ["__Host-af_session=zzz; Path=/; max-age=0; Secure"])).toBeUndefined(); - expect(nextCookie("abc", ["__Host-af_session=zzz; Path=/; Max-Age=01"])).toBe("zzz"); -}); diff --git a/tests/unit/app-routes.test.tsx b/tests/unit/app-routes.test.tsx deleted file mode 100644 index da08841..0000000 --- a/tests/unit/app-routes.test.tsx +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// @vitest-environment jsdom -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { afterEach, beforeEach, expect, test, vi } from "vitest"; -import Account from "../../apps/app/app/routes/account"; -import Chat from "../../apps/app/app/routes/chat"; -import { failureText } from "../../apps/app/app/probe/failure.ts"; -import { csrfHeader } from "../../apps/app/app/probe/session.ts"; -import { frame, helloResponse } from "../fixtures/grpc-web.ts"; -import { - anonymous, - authenticated, - bootstrapBody, - jsonResponse, - receiptBody, -} from "../fixtures/app-probe.ts"; - -const grpcWeb = { "content-type": "application/grpc-web+proto" }; -let fetcher: ReturnType>; - -beforeEach(() => { - fetcher = vi.fn(); - window.fetch = fetcher as unknown as typeof fetch; -}); -afterEach(() => { - cleanup(); - vi.useRealTimers(); -}); - -const requestOf = (call: number) => { - const [input, init] = fetcher.mock.calls[call] as [RequestInfo | URL, RequestInit | undefined]; - return new Request(input, init); -}; - -test("account reads the session once on open and presents it as text with the exact generation", async () => { - fetcher.mockResolvedValue(jsonResponse(bootstrapBody(authenticated))); - render(); - expect(screen.getByText("Checking your session…")).toBeVisible(); - await screen.findByText("Ada Lovelace"); - expect(fetcher).toHaveBeenCalledTimes(1); - expect(requestOf(0).url).toBe(`${window.location.origin}/session/v1/bootstrap`); - expect(screen.getByText("18446744073709551615")).toHaveClass("exact"); - expect(screen.getByText("en-GB")).toBeVisible(); - expect(screen.getByText("Europe/London")).toBeVisible(); - expect(screen.getByText(authenticated.session.expiresAt)).toBeVisible(); - const text = document.body.textContent ?? ""; - for (const secret of [ - authenticated.csrfToken, - authenticated.session.sessionId, - authenticated.session.userId, - authenticated.session.deviceId, - ]) - expect(text).not.toContain(secret); -}); - -test("account renders an anonymous visitor with no sign-out control", async () => { - fetcher.mockResolvedValue(jsonResponse(bootstrapBody(anonymous))); - render(); - await screen.findByText("You are not signed in."); - expect(screen.queryByRole("button")).toBeNull(); -}); - -test("account renders display text from the server as text, never markup", async () => { - const hostile = { - ...authenticated, - profile: { ...authenticated.profile, displayName: "" }, - }; - fetcher.mockResolvedValue(jsonResponse(bootstrapBody(hostile))); - render(); - await screen.findByText(""); - expect(document.querySelector("img")).toBeNull(); -}); - -test("sign out posts the CSRF token from this page's bootstrap and then shows the ended state", async () => { - fetcher - .mockResolvedValueOnce(jsonResponse(bootstrapBody(authenticated))) - .mockResolvedValueOnce(jsonResponse(receiptBody("happened"))); - render(); - fireEvent.click(await screen.findByRole("button", { name: "Sign out" })); - await screen.findByText("You are signed out. Reload to check again."); - expect(fetcher).toHaveBeenCalledTimes(2); - const logout = requestOf(1); - expect(logout.method).toBe("POST"); - expect(logout.url).toBe(`${window.location.origin}/session/v1/logout`); - expect(logout.headers.get("x-af-csrf")).toBe(authenticated.csrfToken); - expect(logout.headers.get(csrfHeader)).toBe(authenticated.csrfToken); - expect(screen.queryByText("Ada Lovelace")).toBeNull(); -}); - -test("a session that ended on the server is shown as ended, not as a transient fault", async () => { - fetcher - .mockResolvedValueOnce(jsonResponse(bootstrapBody(authenticated))) - .mockResolvedValueOnce(new Response("{}", { status: 401 })); - render(); - fireEvent.click(await screen.findByRole("button", { name: "Sign out" })); - await screen.findByText("You are signed out. Reload to check again."); - expect(screen.queryByRole("alert")).toBeNull(); -}); - -test("a refused or unconfirmed sign out is reported as a failure and offers a fresh read", async () => { - for (const [answer, text] of [ - [() => new Response("{}", { status: 403 }), failureText.forbidden], - [() => jsonResponse(receiptBody("didNotHappen")), failureText.rejected], - [() => jsonResponse(receiptBody("unknown")), failureText.unexpected], - ] as const) { - cleanup(); - fetcher.mockReset(); - fetcher.mockResolvedValueOnce(jsonResponse(bootstrapBody(authenticated))); - fetcher.mockResolvedValueOnce(answer()); - render(); - fireEvent.click(await screen.findByRole("button", { name: "Sign out" })); - expect(await screen.findByRole("alert")).toHaveTextContent(text); - expect(screen.queryByText("You are signed out. Reload to check again.")).toBeNull(); - // The page does not claim a sign-out; it offers a fresh read (a new session and token). - expect(screen.getByRole("button", { name: "Try again" })).toBeVisible(); - expect(screen.queryByRole("button", { name: "Sign out" })).toBeNull(); - } -}); - -test("a failed read is announced and can be retried", async () => { - fetcher - .mockResolvedValueOnce(new Response("down", { status: 503 })) - .mockResolvedValueOnce(jsonResponse(bootstrapBody(anonymous))); - render(); - expect(await screen.findByRole("alert")).toHaveTextContent(failureText.unavailable); - fireEvent.click(screen.getByRole("button", { name: "Try again" })); - await screen.findByText("You are not signed in."); - expect(fetcher).toHaveBeenCalledTimes(2); -}); - -test("an unreadable answer is shown as malformed and leaves no session", async () => { - fetcher.mockResolvedValue(jsonResponse('{"csrfToken":"x"}')); - render(); - expect(await screen.findByRole("alert")).toHaveTextContent(failureText.malformed); -}); - -test("leaving the page cancels the read that is still in flight", async () => { - let signal: AbortSignal | undefined; - fetcher.mockImplementation( - (_input, init) => - new Promise((_resolve, reject) => { - signal = init?.signal ?? undefined; - init?.signal?.addEventListener("abort", () => reject(new DOMException("a", "AbortError"))); - }), - ); - const view = render(); - await waitFor(() => expect(signal).toBeDefined()); - view.unmount(); - expect(signal?.aborted).toBe(true); -}); - -test("leaving the chat page cancels the message that is still pending", async () => { - let signal: AbortSignal | undefined; - fetcher.mockImplementation( - (_input, init) => - new Promise((_resolve, reject) => { - signal = init?.signal ?? undefined; - init?.signal?.addEventListener("abort", () => reject(new DOMException("a", "AbortError"))); - }), - ); - const view = render(); - fireEvent.click(screen.getByRole("button", { name: "Send" })); - await waitFor(() => expect(signal).toBeDefined()); - expect(signal?.aborted).toBe(false); - view.unmount(); - expect(signal?.aborted).toBe(true); -}); - -test("chat sends nothing until asked, then shows both sides of one anonymous exchange", async () => { - fetcher.mockImplementation( - async () => new Response(helloResponse("Hello, ArcForges!") as BodyInit, { headers: grpcWeb }), - ); - render(); - expect(fetcher).not.toHaveBeenCalled(); - fireEvent.click(screen.getByRole("button", { name: "Send" })); - await screen.findByText("Hello, ArcForges!"); - expect(fetcher).toHaveBeenCalledTimes(1); - expect(requestOf(0).url).toBe( - `${window.location.origin}/api/arcforges.hello.v1.HelloService/SayHello`, - ); - expect(requestOf(0).credentials).toBe("omit"); - const items = screen.getAllByRole("listitem").map((item) => item.textContent); - expect(items).toEqual(["youArcForges", "serverHello, ArcForges!"]); -}); - -test("chat shows the server's typed refusal as a fixed notice and keeps working", async () => { - fetcher - .mockResolvedValueOnce( - new Response( - frame( - new TextEncoder().encode( - "grpc-status: 3\r\ngrpc-message: Name%20must%20not%20be%20empty.\r\n", - ), - 0x80, - ) as BodyInit, - { headers: grpcWeb }, - ), - ) - .mockResolvedValueOnce( - new Response(helloResponse("Hello, again!") as BodyInit, { headers: grpcWeb }), - ); - render(); - const input = screen.getByLabelText("Name"); - fireEvent.change(input, { target: { value: "" } }); - fireEvent.click(screen.getByRole("button", { name: "Send" })); - await screen.findByText(failureText.rejected); - expect(document.body.textContent).not.toContain("must not be empty"); - fireEvent.change(input, { target: { value: "again" } }); - fireEvent.click(screen.getByRole("button", { name: "Send" })); - await screen.findByText("Hello, again!"); -}); - -test("chat can cancel a pending message, reports it as cancelled and does not double-send", async () => { - let signal: AbortSignal | undefined; - fetcher.mockImplementation( - (_input, init) => - new Promise((_resolve, reject) => { - signal = init?.signal ?? undefined; - init?.signal?.addEventListener("abort", () => reject(new DOMException("a", "AbortError"))); - }), - ); - render(); - fireEvent.click(screen.getByRole("button", { name: "Send" })); - const send = await screen.findByRole("button", { name: "Sending…" }); - expect(send).toBeDisabled(); - fireEvent.submit(send.closest("form") as HTMLFormElement); - expect(fetcher).toHaveBeenCalledTimes(1); - fireEvent.click(screen.getByRole("button", { name: "Cancel" })); - await screen.findByText(failureText.cancelled); - expect(signal?.aborted).toBe(true); - expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull(); - expect(screen.getByRole("button", { name: "Send" })).toBeEnabled(); -}); - -test("chat renders untrusted names as text and keeps only the last twenty entries", async () => { - fetcher.mockImplementation(async (input, init) => { - const body = new Uint8Array(await new Request(input, init).arrayBuffer()); - const name = new TextDecoder().decode(body.slice(7)); - return new Response(helloResponse(`Hello, ${name}!`) as BodyInit, { headers: grpcWeb }); - }); - render(); - const input = screen.getByLabelText("Name"); - fireEvent.change(input, { target: { value: "n" } }); - for (let index = 0; index < 11; index++) { - fireEvent.click(screen.getByRole("button", { name: "Send" })); - await waitFor(() => expect(screen.getByRole("button", { name: "Send" })).toBeEnabled()); - } - expect(screen.getAllByRole("listitem")).toHaveLength(20); - expect(fetcher).toHaveBeenCalledTimes(11); - fireEvent.change(input, { target: { value: "" } }); - fireEvent.click(screen.getByRole("button", { name: "Send" })); - await waitFor(() => - expect(screen.getAllByText("").length).toBeGreaterThan(0), - ); - expect(document.querySelector("img")).toBeNull(); -}); diff --git a/tests/unit/app-session.test.ts b/tests/unit/app-session.test.ts deleted file mode 100644 index e3c2f00..0000000 --- a/tests/unit/app-session.test.ts +++ /dev/null @@ -1,413 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { expect, test } from "vitest"; -import { exactUnsigned, tryExactUnsigned } from "../../apps/app/app/probe/exact.ts"; -import { type FailureKind, ProbeFailure } from "../../apps/app/app/probe/failure.ts"; -import { csrfHeader, endSession, readSession } from "../../apps/app/app/probe/session.ts"; -import { - anonymous, - authenticated, - bootstrapBody, - jsonResponse, - origin, - receiptBody, - streamed, -} from "../fixtures/app-probe.ts"; - -const signal = () => new AbortController().signal; - -async function failureOf(task: Promise): Promise { - const error = await task.then( - () => undefined, - (reason: unknown) => reason, - ); - expect(error).toBeInstanceOf(ProbeFailure); - return (error as ProbeFailure).kind; -} - -test("bootstrap is a same-origin cookie GET driven by the generated route catalogue", async () => { - let seen: Request | undefined; - const session = await readSession({ - origin, - signal: signal(), - fetcher: async (input, init) => { - seen = new Request(input, init); - return jsonResponse(bootstrapBody(authenticated)); - }, - }); - expect(seen?.url).toBe(`${origin}/session/v1/bootstrap`); - expect(seen?.method).toBe("GET"); - expect(seen?.credentials).toBe("same-origin"); - expect(seen?.redirect).toBe("error"); - expect(seen?.cache).toBe("no-store"); - expect(seen?.headers.has(csrfHeader)).toBe(false); - expect(seen?.headers.has("authorization")).toBe(false); - expect(session).toEqual({ - state: "authenticated", - csrfToken: authenticated.csrfToken, - displayName: "Ada Lovelace", - locale: "en-GB", - timezone: "Europe/London", - expiresAt: authenticated.session.expiresAt, - idleExpiresAt: authenticated.session.idleExpiresAt, - workspaces: 2, - recoveryGeneration: "18446744073709551615", - }); -}); - -test("an anonymous answer carries only the CSRF token", async () => { - const session = await readSession({ - origin, - signal: signal(), - fetcher: async () => jsonResponse(bootstrapBody(anonymous)), - }); - expect(session).toEqual({ state: "anonymous", csrfToken: anonymous.csrfToken }); -}); - -test("an authenticated session without a profile still presents", async () => { - const { profile: _profile, ...withoutProfile } = authenticated; - const session = await readSession({ - origin, - signal: signal(), - fetcher: async () => jsonResponse(bootstrapBody(withoutProfile)), - }); - expect(session).toMatchObject({ - state: "authenticated", - displayName: undefined, - locale: undefined, - }); -}); - -test("contradictory or inexact bootstrap answers are malformed, never a session", async () => { - const { session: _session, ...noSession } = authenticated; - const cases: [string, unknown][] = [ - ["authenticated without a session", noSession], - ["anonymous with a session", { ...anonymous, session: authenticated.session }], - ["anonymous with a profile", { ...anonymous, profile: authenticated.profile }], - [ - "leading zero generation", - { ...authenticated, session: { ...authenticated.session, recoveryGeneration: "01" } }, - ], - [ - "generation above uint64", - { - ...authenticated, - session: { ...authenticated.session, recoveryGeneration: "18446744073709551616" }, - }, - ], - ]; - for (const [name, value] of cases) { - let bytes: Uint8Array; - try { - bytes = bootstrapBody(value); - } catch { - // The generated codec already refuses this shape on the server side; send raw text instead. - bytes = new TextEncoder().encode(JSON.stringify(value)); - } - const kind = await failureOf( - readSession({ origin, signal: signal(), fetcher: async () => jsonResponse(bytes) }), - ); - expect(kind, name).toBe("malformed"); - } -}); - -test("the generated codec refuses malformed, unknown-field and wrong-type documents", async () => { - const bodies = [ - "", - "{", - "[]", - "null", - '{"csrfToken":"x"}', - '{"csrfToken":"x","authenticated":"yes"}', - '{"csrfToken":"x","authenticated":false,"extra":1}', - '{"csrfToken":"x","authenticated":false,"authenticated":true}', - `{"csrfToken":"${"x".repeat(20)}","authenticated":false`, - ]; - for (const body of bodies) - expect( - await failureOf( - readSession({ origin, signal: signal(), fetcher: async () => jsonResponse(body) }), - ), - body, - ).toBe("malformed"); -}); - -test("only a JSON 200 is read; every other status is a typed failure with a fixed message", async () => { - const statuses: [number, FailureKind][] = [ - [301, "malformed"], - [400, "rejected"], - [401, "unauthenticated"], - [403, "forbidden"], - [404, "rejected"], - [408, "timeout"], - [413, "limit"], - [429, "limit"], - [500, "unexpected"], - [502, "unavailable"], - [503, "unavailable"], - [504, "timeout"], - ]; - for (const [status, kind] of statuses) { - const error = await readSession({ - origin, - signal: signal(), - // A body that would parse as a session proves the status is checked first. - fetcher: async () => jsonResponse(bootstrapBody(authenticated), { status }), - }).then( - () => undefined, - (reason: unknown) => reason as ProbeFailure, - ); - expect(error?.kind, String(status)).toBe(kind); - expect(error?.message).not.toContain(authenticated.csrfToken); - } - for (const type of ["text/html", "application/x-ndjson", "", "application/jsonp"]) - expect( - await failureOf( - readSession({ - origin, - signal: signal(), - fetcher: async () => - new Response(bootstrapBody(authenticated) as BodyInit, { - headers: type ? { "content-type": type } : {}, - }), - }), - ), - type, - ).toBe("malformed"); - const parameters = await readSession({ - origin, - signal: signal(), - fetcher: async () => - new Response(bootstrapBody(anonymous) as BodyInit, { - headers: { "content-type": "application/json; charset=utf-8" }, - }), - }); - expect(parameters.state).toBe("anonymous"); -}); - -test("the body is bounded before it is parsed", async () => { - const exact = bootstrapBody(anonymous); - const padded = new Uint8Array(16384 + 1).fill(0x20); - padded.set(exact); - // At the schema bound a document is still read (padding is JSON whitespace). - const atBound = new Uint8Array(16384).fill(0x20); - atBound.set(exact); - expect( - ( - await readSession({ - origin, - signal: signal(), - fetcher: async () => jsonResponse(atBound), - }) - ).state, - ).toBe("anonymous"); - expect( - await failureOf( - readSession({ origin, signal: signal(), fetcher: async () => jsonResponse(padded) }), - ), - ).toBe("malformed"); - // A declared length above the bound is refused without reading, and a stream above it is cut off - // after at most one chunk beyond the bound, for the session and the receipt alike. - for (const [bound, call] of [ - [16384, (fetcher: typeof fetch) => readSession({ origin, signal: signal(), fetcher })], - [ - 4096, - (fetcher: typeof fetch) => endSession({ origin, signal: signal(), csrfToken: "t", fetcher }), - ], - ] as const) { - let cancelled = false; - let pulled = 0; - const endless = new Response( - new ReadableStream({ - pull(controller) { - pulled += 1024; - controller.enqueue(new Uint8Array(1024).fill(0x20)); - }, - cancel() { - cancelled = true; - }, - }), - { headers: { "content-type": "application/json" } }, - ); - expect(await failureOf(call(async () => endless)), String(bound)).toBe("malformed"); - expect(cancelled).toBe(true); - expect(pulled).toBeGreaterThan(bound); - expect(pulled).toBeLessThanOrEqual(bound + 4096); - } - expect( - await failureOf( - readSession({ - origin, - signal: signal(), - fetcher: async () => jsonResponse(exact, { headers: { "content-length": "16385" } }), - }), - ), - ).toBe("malformed"); - expect( - await failureOf( - readSession({ - origin, - signal: signal(), - fetcher: async () => jsonResponse(exact, { headers: { "content-length": "12e3" } }), - }), - ), - ).toBe("malformed"); -}); - -test("the stream bound is exact: one byte over it is the first byte refused", async () => { - for (const [bound, call] of [ - [16384, (fetcher: typeof fetch) => readSession({ origin, signal: signal(), fetcher })], - [ - 4096, - (fetcher: typeof fetch) => endSession({ origin, signal: signal(), csrfToken: "t", fetcher }), - ], - ] as const) { - let pulled = 0; - const body = new ReadableStream( - { - pull(controller) { - pulled += 1; - controller.enqueue(Uint8Array.of(0x20)); - }, - }, - { highWaterMark: 0 }, - ); - const answer = new Response(body, { headers: { "content-type": "application/json" } }); - expect(await failureOf(call(async () => answer)), String(bound)).toBe("malformed"); - expect(pulled, String(bound)).toBe(bound + 1); - } -}); - -test("a body split across chunks reads as one document; a failing stream is unavailable", async () => { - const bytes = bootstrapBody(authenticated); - const chunks = [bytes.slice(0, 7), bytes.slice(7, 90), bytes.slice(90)]; - expect( - ( - await readSession({ - origin, - signal: signal(), - fetcher: async () => streamed(chunks), - }) - ).state, - ).toBe("authenticated"); - expect( - await failureOf( - readSession({ - origin, - signal: signal(), - fetcher: async () => streamed([bytes.slice(0, 20)], new TypeError("connection reset")), - }), - ), - ).toBe("unavailable"); -}); - -test("network failure is unavailable and cancellation is distinct from it", async () => { - expect( - await failureOf( - readSession({ - origin, - signal: signal(), - fetcher: async () => { - throw new TypeError("Failed to fetch"); - }, - }), - ), - ).toBe("unavailable"); - const controller = new AbortController(); - const pending = readSession({ - origin, - signal: controller.signal, - fetcher: (_input, init) => - new Promise((_resolve, reject) => { - init?.signal?.addEventListener("abort", () => - reject(new DOMException("aborted", "AbortError")), - ); - }), - }); - controller.abort(); - expect(await failureOf(pending)).toBe("cancelled"); - const already = new AbortController(); - already.abort(); - expect( - await failureOf( - readSession({ - origin, - signal: already.signal, - fetcher: async (_input, init) => { - if (init?.signal?.aborted) throw new DOMException("aborted", "AbortError"); - return jsonResponse(bootstrapBody(anonymous)); - }, - }), - ), - ).toBe("cancelled"); -}); - -test("logout sends the CSRF token as the only credential header, with no body", async () => { - let seen: Request | undefined; - const effect = await endSession({ - origin, - signal: signal(), - csrfToken: authenticated.csrfToken, - fetcher: async (input, init) => { - seen = new Request(input, init); - expect(init?.body).toBeUndefined(); - return jsonResponse(receiptBody("happened")); - }, - }); - expect(effect).toBe("happened"); - expect(seen?.url).toBe(`${origin}/session/v1/logout`); - expect(seen?.method).toBe("POST"); - expect(seen?.credentials).toBe("same-origin"); - expect(seen?.redirect).toBe("error"); - // The literal wire name is pinned here, independent of the module constant. - expect(seen?.headers.get("x-af-csrf")).toBe(authenticated.csrfToken); - expect(csrfHeader.toLowerCase()).toBe("x-af-csrf"); - expect(seen?.headers.get(csrfHeader)).toBe(authenticated.csrfToken); - expect(seen?.headers.has("authorization")).toBe(false); - expect(await seen?.text()).toBe(""); -}); - -test("logout reports an ended session, a refused token and a malformed receipt distinctly", async () => { - const run = (response: () => Response) => - endSession({ origin, signal: signal(), csrfToken: "t", fetcher: async () => response() }); - expect(await failureOf(run(() => new Response("{}", { status: 401 })))).toBe("unauthenticated"); - expect(await failureOf(run(() => new Response("{}", { status: 403 })))).toBe("forbidden"); - expect(await failureOf(run(() => new Response("{}", { status: 400 })))).toBe("rejected"); - expect(await failureOf(run(() => new Response("{}", { status: 503 })))).toBe("unavailable"); - expect(await failureOf(run(() => jsonResponse(receiptBody("didNotHappen"))))).toBe("rejected"); - expect(await failureOf(run(() => jsonResponse(receiptBody("unknown"))))).toBe("unexpected"); - expect(await failureOf(run(() => jsonResponse("{}")))).toBe("malformed"); - expect(await failureOf(run(() => jsonResponse('{"commandId":"x"}')))).toBe("malformed"); - expect(await failureOf(run(() => jsonResponse(new Uint8Array(4097).fill(0x20))))).toBe( - "malformed", - ); -}); - -test("64-bit values stay exact: canonical text in, the same text out, nothing through a number", () => { - for (const value of [ - "0", - "9007199254740991", - "9007199254740993", - "9223372036854775807", - "18446744073709551615", - ]) { - const exact = exactUnsigned(value); - expect(exact.text).toBe(value); - expect(exact.value).toBe(BigInt(value)); - } - // A JavaScript number cannot hold these two values apart; the exact path can. - expect(Number("9007199254740993")).toBe(Number("9007199254740992")); - expect(exactUnsigned("9007199254740993").value).not.toBe(exactUnsigned("9007199254740992").value); - for (const value of [ - "", - "01", - "-1", - "+1", - "1.0", - "1e3", - " 1", - "1 ", - "0x10", - "18446744073709551616", - ]) - expect(tryExactUnsigned(value), value).toBeUndefined(); -}); diff --git a/tests/unit/architecture-policy.test.ts b/tests/unit/architecture-policy.test.ts deleted file mode 100644 index e8d4ea6..0000000 --- a/tests/unit/architecture-policy.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { describe, expect, it } from "vitest"; -import { auditArchitecture, type Sources } from "../../eng/policy/architecture.ts"; - -function baseline(): Sources { - const manifest = { - name: "web", - workspaces: ["apps/site"], - packageManager: "npm@11.19.0", - engines: { node: ">=24.21.0 <25", npm: ">=11.19.0 <12" }, - devDependencies: { typescript: "7.0.2", "@react-router/dev": "8.4.0" }, - }; - return { - "package.json": JSON.stringify(manifest), - "apps/site/package.json": JSON.stringify({ name: "site", license: "AGPL-3.0-only" }), - "package-lock.json": JSON.stringify({ - lockfileVersion: 3, - packages: { "": { devDependencies: manifest.devDependencies }, "apps/site": {} }, - }), - ".node-version": "24.21.0", - "apps/site/app/root.ts": - 'import { Message } from "@arcforges/proto"; document.title = "Web DOM is allowed";', - }; -} -function alterManifest(sources: Sources, update: (manifest: Record) => void) { - const manifest = JSON.parse(sources["package.json"] ?? "{}"); - update(manifest); - sources["package.json"] = JSON.stringify(manifest); -} -describe("GOV.11 architecture refusal fixtures", () => { - const cases: [string, (sources: Sources) => void][] = [ - [ - "wire-source", - (s) => { - s["apps/site/app/root.ts"] = - 'interface LoginPayload { password: string }; JSON.stringify({password:"x"});'; - }, - ], - [ - "wire-source", - (s) => { - s["apps/site/app/root.ts"] = 'fetch("/api/custom", {body:"handwritten"});'; - }, - ], - [ - "wire-source", - (s) => { - s["apps/site/app/root.ts"] = 'import { messageDesc } from "@bufbuild/protobuf/codegenv2";'; - }, - ], - [ - "private-import", - (s) => { - s["apps/site/app/root.ts"] = 'import "../bridge";'; - s["apps/site/bridge.ts"] = 'import "@arcforges/operator-client";'; - }, - ], - [ - "release-fixture", - (s) => { - s["apps/site/app/root.ts"] = 'import "../bridge";'; - s["apps/site/bridge.ts"] = 'import "../../tests/helper";'; - s["tests/helper.ts"] = "export const fake = true;"; - }, - ], - [ - "production-command", - (s) => - alterManifest(s, (m) => { - m.scripts = { build: "npm run dev", dev: "vite dev" }; - }), - ], - [ - "production-command", - (s) => - alterManifest(s, (m) => { - m.scripts = { build: "npm run other", other: "npm run build" }; - }), - ], - [ - "production-command", - (s) => { - s["Web.esproj"] = "true"; - }, - ], - [ - "production-command", - (s) => { - s["Web.esproj"] = "npm run dev"; - }, - ], - [ - "workspace", - (s) => { - s["apps/site/package-lock.json"] = "{}"; - }, - ], - [ - "workspace", - (s) => { - s["yarn.lock"] = "alternate"; - }, - ], - [ - "workspace", - (s) => { - s["hidden/package.json"] = '{"name":"hidden"}'; - }, - ], - [ - "pins", - (s) => - alterManifest(s, (m) => { - m.packageManager = "npm@latest"; - }), - ], - [ - "pins", - (s) => - alterManifest(s, (m) => { - m.devDependencies = { typescript: "^7.0.2" }; - }), - ], - [ - "sdk-ui", - (s) => { - s["packages/sdk/src/index.ts"] = 'export * from "@arcforges/web-ui";'; - }, - ], - [ - "private-import", - (s) => { - s["apps/site/app/root.ts"] = 'export * from "@arcforges/operator-client";'; - }, - ], - [ - "private-import", - (s) => { - s["apps/site/app/root.ts"] = 'import type { X } from "@arcforges/proto/internal/x";'; - }, - ], - [ - "private-import", - (s) => { - s["apps/site/app/root.ts"] = 'const x = import("../server/private.ts");'; - }, - ], - [ - "private-import", - (s) => { - s["apps/site/app/root.ts"] = 'type T = import("@arcforges/local-rpc").T;'; - }, - ], - [ - "private-import", - (s) => { - s["apps/site/app/root.ts"] = 'const x = require("@arcforges/ai-internal");'; - }, - ], - [ - "computed-import", - (s) => { - s["apps/site/app/root.ts"] = "const x = import(variable);"; - }, - ], - [ - "desktop-dom", - (s) => { - s["packages/desktop/src/index.ts"] = "document.createElement('div');"; - }, - ], - [ - "obsolete-target", - (s) => { - s["obsolete.csproj"] = - ''; - }, - ], - [ - "portable-reference", - (s) => { - s["Portable.csproj"] = ''; - }, - ], - [ - "production-command", - (s) => - alterManifest(s, (m) => { - m.scripts = { build: "npm install && vite build" }; - }), - ], - [ - "production-command", - (s) => - alterManifest(s, (m) => { - m.scripts = { start: "vite dev --host" }; - }), - ], - [ - "wire-source", - (s) => { - s["apps/site/app/root.ts"] = "export interface LoginRequest { token: string }"; - }, - ], - [ - "release-fixture", - (s) => { - s["apps/site/app/root.ts"] = 'import "./bridge";'; - s["apps/site/app/bridge.ts"] = 'export * from "./fixtures/secret";'; - s["apps/site/app/fixtures/secret.ts"] = "export const fake = true"; - }, - ], - [ - "unresolved-import", - (s) => { - s["apps/site/app/root.ts"] = 'import "./missing";'; - }, - ], - ]; - for (const [rule, mutation] of cases) - it(`${rule}: accepts valid graph and rejects mutation`, () => { - const sources = baseline(); - expect(auditArchitecture(sources)).toEqual([]); - mutation(sources); - expect(auditArchitecture(sources).some((finding) => finding.rule === rule)).toBe(true); - }); - it("ignores quoted/commented fake imports and permits local development", () => { - const sources = baseline(); - sources["apps/site/app/root.ts"] = - '// import x from "@arcforges/private";\nconst text = "document @arcforges/private";'; - alterManifest(sources, (m) => { - m.scripts = { dev: "vite dev", build: "vite build" }; - }); - expect(auditArchitecture(sources)).toEqual([]); - }); - it("permits UI-local models and aliases of imported generated wire types", () => { - const sources = baseline(); - sources["apps/site/app/root.ts"] = - 'import type { SayHelloRequest } from "@arcforges/proto"; type HelloRequest = SayHelloRequest; type OtherRequest = import("@arcforges/proto").SayHelloRequest; interface ViewState { selected: boolean }'; - expect(auditArchitecture(sources)).toEqual([]); - }); - it("rejects transitive Apache-to-AGPL manifest dependencies without source imports", () => { - const sources = baseline(); - const root = JSON.parse(sources["package.json"] ?? "{}"); - root.workspaces.push("packages/sdk", "packages/bridge", "packages/other"); - sources["package.json"] = JSON.stringify(root); - const lock = JSON.parse(sources["package-lock.json"] ?? "{}"); - for (const [directory, name, license, dependencies] of [ - ["sdk", "@arcforges/web-sdk", "Apache-2.0", { "@arcforges/web-bridge": "1.0.0" }], - ["bridge", "@arcforges/web-bridge", "Apache-2.0", { "@arcforges/web-other": "1.0.0" }], - ["other", "@arcforges/web-other", "Apache-2.0", {}], - ] as const) { - sources[`packages/${directory}/package.json`] = JSON.stringify({ - name, - license, - dependencies, - }); - lock.packages[`packages/${directory}`] = { dependencies }; - } - sources["package-lock.json"] = JSON.stringify(lock); - expect(auditArchitecture(sources)).toEqual([]); - sources["packages/other/package.json"] = (sources["packages/other/package.json"] ?? "").replace( - "Apache-2.0", - "AGPL-3.0-only", - ); - expect(auditArchitecture(sources).some((f) => f.rule === "sdk-ui")).toBe(true); - }); -}); diff --git a/tests/unit/canonical-host.test.ts b/tests/unit/canonical-host.test.ts deleted file mode 100644 index b2fea26..0000000 --- a/tests/unit/canonical-host.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { basename, dirname, join } from "node:path"; -import { pathToFileURL } from "node:url"; -import { expect, test, vi } from "vitest"; -import worker from "../../worker/index.ts"; -import { root, verifyWorkerScript, workerScript } from "../../tooling/project.ts"; - -test.each([ - ["https://www.arcforges.com/", "https://arcforges.com/"], - [ - "https://www.arcforges.com/cloud-hello/?name=a%2Fb&next=%2Fhello%2F", - "https://arcforges.com/cloud-hello/?name=a%2Fb&next=%2Fhello%2F", - ], - [ - "http://www.arcforges.com:8080//other.example/path?x=1&x=2", - "https://arcforges.com//other.example/path?x=1&x=2", - ], -])("www redirects before static assets: %s", async (url, destination) => { - const fetch = vi.fn<(_request: Request) => Promise>(); - const response = await worker.fetch(new Request(url), { ASSETS: { fetch } }); - expect(response.status).toBe(308); - expect(response.headers.get("location")).toBe(destination); - expect(fetch).not.toHaveBeenCalled(); -}); - -test.each(["HEAD", "POST"])("canonical redirect preserves %s semantics", async (method) => { - const fetch = vi.fn<(_request: Request) => Promise>(); - const response = await worker.fetch( - new Request("https://www.arcforges.com/api/example?mode=binary", { method }), - { ASSETS: { fetch } }, - ); - expect(response.status).toBe(308); - expect(response.headers.get("location")).toBe("https://arcforges.com/api/example?mode=binary"); - expect(fetch).not.toHaveBeenCalled(); -}); - -test.each([ - "https://arcforges.com/cloud-hello/", - "http://127.0.0.1:4173/hello/", - "https://www.arcforges.com.other.example/", - "https://other.example/?next=https://www.arcforges.com/", -])("other hosts preserve the original asset request and response: %s", async (url) => { - const request = new Request(url, { headers: { accept: "text/html" } }); - const asset = new Response("asset", { - headers: { "content-security-policy": "default-src 'self'" }, - }); - const fetch = vi.fn(async () => asset); - const response = await worker.fetch(request, { ASSETS: { fetch } }); - expect(fetch).toHaveBeenCalledExactlyOnceWith(request); - expect(response).toBe(asset); -}); - -test("delivery runs the reviewed Worker before assets without a deployment rebuild", async () => { - const config = JSON.parse(await readFile(join(root, "wrangler.json"), "utf8")); - expect(config.main).toBe("./worker/index.js"); - expect(config.no_bundle).toBe(true); - expect(config.assets.binding).toBe("ASSETS"); - expect(config.assets.run_worker_first).toBe(true); -}); - -test("the emitted candidate Worker is importless and carries no type syntax", async () => { - const emitted = await workerScript(); - expect(emitted).not.toMatch(/^\s*import\s/mu); - expect(emitted).not.toMatch(/\brequire\(/u); - expect(emitted).not.toMatch(/\binterface\s|:\s*Env\b|:\s*Request\b/u); - expect(emitted).toMatch(/export default \{/u); -}); - -test.each([ - "https://www.arcforges.com/", - "https://www.arcforges.com/cloud-hello/?name=a%2Fb&next=%2Fhello%2F", - "http://www.arcforges.com:8080//other.example/path?x=1&x=2", - "https://www.arcforges.com/api/example?mode=binary", - "https://arcforges.com/cloud-hello/", - "http://127.0.0.1:4173/hello/", -])("the emitted candidate Worker answers like the TypeScript source: %s", async (url) => { - const directory = await mkdtemp(join(tmpdir(), "arcforges-emitted-worker-")); - try { - const modulePath = join(directory, "index.mjs"); - await writeFile(modulePath, await workerScript()); - const emitted = (await import(pathToFileURL(modulePath).href)).default; - const sourceAsset = vi.fn(async () => new Response("asset")); - const emittedAsset = vi.fn(async () => new Response("asset")); - const sourceResponse = await worker.fetch(new Request(url), { ASSETS: { fetch: sourceAsset } }); - const emittedResponse = await emitted.fetch(new Request(url), { - ASSETS: { fetch: emittedAsset }, - }); - expect(emittedResponse.status).toBe(sourceResponse.status); - expect(emittedResponse.headers.get("location")).toBe(sourceResponse.headers.get("location")); - expect(emittedAsset.mock.calls.length).toBe(sourceAsset.mock.calls.length); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("candidate verification rejects a replaced private Worker independently of its manifest", async () => { - const directory = await mkdtemp(join(tmpdir(), "arcforges-canonical-worker-")); - try { - await mkdir(join(directory, "worker")); - const source = await workerScript(); - await writeFile(join(directory, "worker/index.js"), source); - await expect(verifyWorkerScript(directory)).resolves.toBeUndefined(); - await writeFile( - join(directory, "worker/index.js"), - source.replace('arcforges.com";', 'other.example";'), - ); - await expect(verifyWorkerScript(directory)).rejects.toThrow("Worker script changed"); - } finally { - expect(dirname(directory)).toBe(tmpdir()); - expect(basename(directory)).toMatch(/^arcforges-canonical-worker-/u); - await rm(directory, { recursive: true, force: true }); - } -}); diff --git a/tests/unit/cloud-hello.test.ts b/tests/unit/cloud-hello.test.ts deleted file mode 100644 index c011d55..0000000 --- a/tests/unit/cloud-hello.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { expect, test } from "vitest"; -import { checkServerConnection, helloApiPath } from "../../apps/site/app/cloud-hello"; -import { decodeHelloRequest, helloResponse } from "../fixtures/grpc-web"; - -test("connection uses the published binary client, same-origin API and no credentials", async () => { - const fetcher: typeof fetch = async (input, init) => { - const request = new Request(input, init); - expect(request.url).toBe(`https://arcforges.com${helloApiPath}`); - expect(request.method).toBe("POST"); - expect(request.headers.get("content-type")).toContain("application/grpc-web+proto"); - expect(request.credentials).toBe("omit"); - expect(request.redirect).toBe("error"); - expect(request.headers.has("authorization")).toBe(false); - expect(decodeHelloRequest(new Uint8Array(await request.arrayBuffer())).name).toBe("ArcForges"); - return new Response(helloResponse(), { - headers: { "content-type": "application/grpc-web+proto" }, - }); - }; - await expect( - checkServerConnection("https://arcforges.com", new AbortController().signal, fetcher), - ).resolves.toBe("Hello, ArcForges!"); -}); - -test("missing API and unexpected server output never become a local successful greeting", async () => { - for (const response of [ - new Response("Not found", { status: 404 }), - new Response("Method not allowed", { status: 405 }), - new Response("Not an API", { headers: { "content-type": "text/html" } }), - new Response(helloResponse("different service"), { - headers: { "content-type": "application/grpc-web+proto" }, - }), - ]) - await expect( - checkServerConnection( - "https://arcforges.com", - new AbortController().signal, - async () => response, - ), - ).rejects.toThrow(); -}); diff --git a/tests/unit/contracts.test.ts b/tests/unit/contracts.test.ts deleted file mode 100644 index 5631689..0000000 --- a/tests/unit/contracts.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { createHelloClient } from "@arcforges/api-client"; -import { SayHelloRequestSchema, SayHelloResponseSchema } from "@arcforges/proto"; -import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; -import { expect, test } from "vitest"; -function frame(payload: Uint8Array, flag: number) { - const bytes = new Uint8Array(payload.length + 5); - bytes[0] = flag; - new DataView(bytes.buffer).setUint32(1, payload.length); - bytes.set(payload, 5); - return bytes; -} -// Explicit wire fixture; this does not assert that the C# Cloud service exists. -test("published SDK sends protobuf gRPC-Web and decodes the framed response", async () => { - const client = createHelloClient({ - baseUrl: "https://cloud.example.test", - fetch: async (input, init) => { - const request = new Request(input, init); - expect(new URL(request.url).pathname).toBe("/arcforges.hello.v1.HelloService/SayHello"); - expect(request.headers.get("content-type")).toContain("application/grpc-web+proto"); - const bytes = new Uint8Array(await request.arrayBuffer()); - expect(bytes[0]).toBe(0); - const decoded = fromBinary(SayHelloRequestSchema, bytes.slice(5)); - expect(decoded.name).toBe("ArcForges 世界"); - const message = frame( - toBinary( - SayHelloResponseSchema, - create(SayHelloResponseSchema, { message: `Hello, ${decoded.name}!` }), - ), - 0, - ); - const trailer = frame(new TextEncoder().encode("grpc-status: 0\r\n"), 0x80); - return new Response(new Blob([message, trailer]), { - headers: { "content-type": "application/grpc-web+proto" }, - }); - }, - }); - await expect(client.sayHello({ name: "ArcForges 世界" })).resolves.toMatchObject({ - message: "Hello, ArcForges 世界!", - }); -}); -test("SDK surfaces server failures instead of inventing a successful greeting", async () => { - const client = createHelloClient({ - baseUrl: "https://cloud.example.test", - fetch: async () => - new Response(null, { - headers: { - "content-type": "application/grpc-web+proto", - "grpc-status": "16", - "grpc-message": "Unauthenticated", - }, - }), - }); - await expect(client.sayHello({ name: "World" })).rejects.toThrow(/Unauthenticated/i); -}); diff --git a/tests/unit/delivery.test.ts b/tests/unit/delivery.test.ts deleted file mode 100644 index 70448e6..0000000 --- a/tests/unit/delivery.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { createHash } from "node:crypto"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { expect, test } from "vitest"; -import { contentSecurityPolicy, releaseVersion, seal, verify } from "../../tooling/project.ts"; -import { - deploymentUrl, - requireCustomDomain, - waitForDelivery, - waitForIdentity, -} from "../../tooling/cloudflare.ts"; -test("CSP authorizes exact prerendered scripts without unsafe inline/eval", () => { - const script = "console.log('hello')"; - const csp = contentSecurityPolicy([``]); - expect(csp).toContain(createHash("sha256").update(script).digest("base64")); - expect(csp).not.toMatch(/unsafe-inline|unsafe-eval/); - expect(csp).toContain("form-action 'none'"); - const variant = contentSecurityPolicy([ - ``, - ]); - expect(variant).toContain(createHash("sha256").update(`\n${script}`).digest("base64")); - expect(variant).not.toContain(createHash("sha256").update("not executable").digest("base64")); -}); -test("CI versions are unique across runs and reruns", () => { - expect(releaseVersion("10", "2")).toBe("0.1.0-ci.10.2"); - expect(releaseVersion(undefined, undefined)).toBe("0.1.0-local"); - expect(() => releaseVersion("1", "bad")).toThrow(); -}); -test("candidate verification rejects tampering and added files before deployment", async () => { - const dir = await mkdtemp(join(tmpdir(), "arcforges-web-test-")); - try { - await writeFile(join(dir, "example.txt"), "original"); - await seal(dir, { source: "a".repeat(40), version: "0.1.0-local", dirty: true }); - await writeFile(join(dir, "example.txt"), "changed"); - await expect(verify(dir, "a".repeat(40))).rejects.toThrow("Candidate changed"); - await writeFile(join(dir, "unexpected.txt"), "extra"); - await expect(verify(dir, "a".repeat(40))).rejects.toThrow("Candidate file set changed"); - await expect(verify(dir, "b".repeat(40))).rejects.toThrow("Candidate source"); - } finally { - await rm(dir, { recursive: true, force: true }); - } -}); -test("live verification tolerates propagation with read-only requests", async () => { - const identity = { source: "a".repeat(40), version: "0.1.0-ci.1.1" }; - let calls = 0; - await waitForIdentity(deploymentUrl, identity, { - attempts: 3, - pause: async () => {}, - fetcher: async (_input, init) => { - expect(init?.method).toBeUndefined(); - calls++; - return Response.json(calls < 2 ? { ...identity, version: "old" } : identity); - }, - }); - expect(calls).toBe(2); - await expect( - waitForIdentity(deploymentUrl, identity, { - attempts: 2, - pause: async () => {}, - fetcher: async () => Response.json({ version: "old" }), - }), - ).rejects.toThrow("No redeployment"); - await expect(waitForIdentity("https://other.example.com", identity)).rejects.toThrow( - "Unexpected deployment host", - ); -}); - -test("deployment requires the custom domain to belong to this production Worker", () => { - const domain = { - hostname: "arcforges.com", - service: "arcforges-web", - environment: "production", - }; - expect(() => requireCustomDomain([domain])).not.toThrow(); - for (const domains of [ - [], - [{ ...domain, service: "another-worker" }], - [{ ...domain, hostname: "other.example.com" }], - [{ ...domain, environment: "staging" }], - ]) - expect(() => requireCustomDomain(domains)).toThrow("Attach arcforges.com"); -}); - -test("asset verification waits for headers as well as identity and fails boundedly", async () => { - let reads = 0; - await waitForDelivery( - async (signal) => { - expect(signal.aborted).toBe(false); - reads++; - if (reads === 1) throw new Error("Missing no-transform: /404.css"); - }, - { timeoutMs: 1000, intervalMs: 0 }, - ); - expect(reads).toBe(2); - await expect( - waitForDelivery( - async () => { - throw new Error("Deployed bytes differ: /hello/"); - }, - { timeoutMs: 10, intervalMs: 0 }, - ), - ).rejects.toThrow("no redeployment was attempted. Error: Deployed bytes differ: /hello/"); -}); diff --git a/tests/unit/hello.test.tsx b/tests/unit/hello.test.tsx deleted file mode 100644 index 3d4ac7a..0000000 --- a/tests/unit/hello.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// @vitest-environment jsdom -import "@testing-library/jest-dom/vitest"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; -import { afterEach, expect, test } from "vitest"; -import { greet } from "../../apps/site/app/hello"; -import Hello from "../../apps/site/app/routes/hello"; -afterEach(cleanup); -test("published protobuf round-trip preserves Unicode names", () => { - expect(greet(" 世界 🌍 ")).toBe("Hello, 世界 🌍!"); - expect(greet("🌍".repeat(80))).toBe(`Hello, ${"🌍".repeat(80)}!`); - for (const invalid of [" ", "x".repeat(81), "hi\u0000"]) expect(() => greet(invalid)).toThrow(); -}); -test("form validates, recovers and renders untrusted names as text", () => { - render(); - const input = screen.getByLabelText("Your name"); - fireEvent.change(input, { target: { value: "" } }); - fireEvent.click(screen.getByRole("button", { name: "Say hello" })); - expect(screen.getByRole("alert")).toBeVisible(); - fireEvent.change(input, { target: { value: "" } }); - fireEvent.click(screen.getByRole("button", { name: "Say hello" })); - expect(screen.queryByRole("alert")).not.toBeInTheDocument(); - expect(screen.getByRole("status")).toHaveTextContent("Hello, !"); - expect(document.querySelector("img")).toBeNull(); -}); diff --git a/tests/unit/licence-boundary.test.ts b/tests/unit/licence-boundary.test.ts deleted file mode 100644 index 54191f6..0000000 --- a/tests/unit/licence-boundary.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { test } from "vitest"; -import { auditLicences } from "../../tooling/licence-boundary.ts"; - -function fixture() { - const root = mkdtempSync(path.join(os.tmpdir(), "licence-inventory-")); - execFileSync("git", ["init", "-q"], { cwd: root, windowsHide: true }); - execFileSync( - "git", - [ - "-c", - "user.name=Licence Test", - "-c", - "user.email=licence@example.invalid", - "-c", - "commit.gpgsign=false", - "commit", - "--allow-empty", - "-qm", - "fixture", - ], - { cwd: root, windowsHide: true }, - ); - const write = (file: string, value: unknown) => { - mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); - writeFileSync(path.join(root, file), typeof value === "string" ? value : JSON.stringify(value)); - }; - const policy = { - schemaVersion: 1, - repository: "Web", - spdxLicense: "AGPL-3.0-only", - licenceBoundary: "AGPL", - projects: [{ path: "package.json", kind: "npm" }], - }; - const manifest = { - name: "@arcforges/test", - license: "AGPL-3.0-only", - arcforges: { licenceBoundary: "AGPL" }, - }; - write("eng/policy/licence-boundary.json", policy); - write("package.json", manifest); - const cleanup = () => { - assert.equal(path.dirname(root), os.tmpdir()); - assert(path.basename(root).startsWith("licence-inventory-")); - rmSync(root, { recursive: true }); - }; - return { root, write, policy, manifest, cleanup }; -} -test("Git inventory checks existing and newly added projects", () => { - const f = fixture(); - try { - assert.equal(auditLicences(f.root).projects.length, 1); - f.write("unregistered/package.json", f.manifest); - assert.throws(() => auditLicences(f.root), /inventory drift/u); - } finally { - f.cleanup(); - } -}); -test("a missing boundary and edited owner assignment both fail", () => { - const f = fixture(); - try { - f.write("package.json", { ...f.manifest, arcforges: {} }); - assert.throws(() => auditLicences(f.root), /Incorrect boundary/u); - f.write("package.json", f.manifest); - f.policy.licenceBoundary = "Apache"; - f.write("eng/policy/licence-boundary.json", f.policy); - assert.throws(() => auditLicences(f.root)); - } finally { - f.cleanup(); - } -}); -test("inconsistent managed SPDX, imported overrides and escaped references fail", () => { - const f = fixture(); - try { - f.policy.projects.push({ path: "app.csproj", kind: "msbuild" }); - f.write("eng/policy/licence-boundary.json", f.policy); - const project = - "AGPL-3.0-onlyAGPL"; - f.write("app.csproj", project); - assert.equal(auditLicences(f.root).projects.length, 2); - f.write("app.csproj", project.replace("AGPL-3.0-only", "Apache-2.0")); - assert.throws(() => auditLicences(f.root), /Incorrect SPDX/u); - f.write( - "app.csproj", - project.replace( - "", - '', - ), - ); - assert.throws(() => auditLicences(f.root), /Escaped\/unregistered/u); - f.write("app.csproj", project); - f.write( - "Directory.Build.props", - "Apache", - ); - assert.throws(() => auditLicences(f.root), /Imported licence override/u); - } finally { - f.cleanup(); - } -}); -test("unknown locked transitive packages and unversioned npm aliases fail", () => { - const f = fixture(); - try { - f.write("package-lock.json", { packages: { "node_modules/@arcforges/unknown": {} } }); - assert.throws(() => auditLicences(f.root), /Unknown first-party/u); - f.write("package-lock.json", { packages: {} }); - f.write("package.json", { ...f.manifest, dependencies: { alias: "npm:@arcforges/unknown" } }); - assert.throws(() => auditLicences(f.root), /Unknown first-party/u); - f.write("package.json", f.manifest); - f.write("packages.lock.json", { - dependencies: { "net10.0": { "ArcForges.Contracts.Unknown": { type: "Transitive" } } }, - }); - assert.throws(() => auditLicences(f.root), /Unknown first-party/u); - } finally { - f.cleanup(); - } -}); -test("Gradle declarations and unpublished source inputs are checked", () => { - const f = fixture(); - try { - f.policy.projects.push({ path: "build.gradle.kts", kind: "gradle" }); - f.write("eng/policy/licence-boundary.json", f.policy); - f.write( - "build.gradle.kts", - 'extra["spdxLicense"] = "AGPL-3.0-only"\nextra["licenceBoundary"] = "Apache"\n', - ); - assert.throws(() => auditLicences(f.root), /Incorrect Gradle/u); - f.write( - "build.gradle.kts", - 'extra["spdxLicense"] = "AGPL-3.0-only"\nextra["licenceBoundary"] = "AGPL"\nmavenLocal()\n', - ); - assert.throws(() => auditLicences(f.root), /Unpublished Gradle/u); - } finally { - f.cleanup(); - } -}); - -test("comments cannot join fragments into a licence declaration", () => { - const f = fixture(); - try { - f.policy.projects.push({ path: "app.csproj", kind: "msbuild" }); - f.write("eng/policy/licence-boundary.json", f.policy); - f.write( - "app.csproj", - "AGPL-3.0-onlyoundary>AGPL", - ); - assert.throws(() => auditLicences(f.root), /Incorrect boundary/u); - } finally { - f.cleanup(); - } -}); diff --git a/tests/unit/naming-policy.test.ts b/tests/unit/naming-policy.test.ts deleted file mode 100644 index bfc28d0..0000000 --- a/tests/unit/naming-policy.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import assert from "node:assert/strict"; -import { execFileSync, spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { expect, it } from "vitest"; - -it("published naming scanner accepts current terms and detects every forbidden term", () => { - const packageRoot = path.resolve("node_modules/@arcforges/proto/tools/naming/eng"); - const policy = JSON.parse( - readFileSync(path.join(packageRoot, "policy/product-names.json"), "utf8"), - ); - const root = mkdtempSync(path.join(os.tmpdir(), "web-naming-")); - const git = (...args: string[]) => - execFileSync("git", args, { cwd: root, windowsHide: true, stdio: "pipe" }); - assert.equal(path.dirname(root), path.resolve(os.tmpdir())); - assert(path.basename(root).startsWith("web-naming-")); - try { - git("init", "-q"); - git("remote", "add", "origin", "https://github.com/ArcForges/Web.git"); - git( - "-c", - "user.name=Fixture", - "-c", - "user.email=fixture@example.invalid", - "-c", - "commit.gpgsign=false", - "commit", - "--allow-empty", - "-qm", - "fixture", - ); - const probe = path.join(root, "probe.txt"); - const scan = () => - spawnSync( - "python", - [path.join(packageRoot, "check_naming.py"), "--repository", `Web=${root}`], - { encoding: "utf8", windowsHide: true }, - ); - writeFileSync(probe, "ArcForges ArcScope"); - expect(scan().status).toBe(0); - for (const item of policy.forbiddenNames as { name: string }[]) { - writeFileSync(probe, item.name); - const result = scan(); - expect(result.status, item.name).toBe(1); - expect(result.stdout).toContain('"kind": "forbidden content"'); - } - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); diff --git a/tests/worker/canonical-host.test.ts b/tests/worker/canonical-host.test.ts new file mode 100644 index 0000000..2f8a45f --- /dev/null +++ b/tests/worker/canonical-host.test.ts @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// The canonical-host Worker (worker/index.ts) and its emitted candidate form. Ported one-for-one from the vitest file +// tests/unit/canonical-host.test.ts to node:test; the cases and the expected values are unchanged. +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, dirname, join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { test } from "node:test"; +import worker from "../../worker/index.ts"; +import { root, verifyWorkerScript, workerScript } from "../../tooling/project.ts"; + +type Env = Parameters[1]; + +function recordingAsset(body: string, headers: Record = {}) { + const calls: Request[] = []; + const fetch = async (request: Request) => { + calls.push(request); + return new Response(body, { headers }); + }; + return { calls, env: { ASSETS: { fetch } } as unknown as Env }; +} + +const redirects: [string, string][] = [ + ["https://www.arcforges.com/", "https://arcforges.com/"], + [ + "https://www.arcforges.com/cloud-hello/?name=a%2Fb&next=%2Fhello%2F", + "https://arcforges.com/cloud-hello/?name=a%2Fb&next=%2Fhello%2F", + ], + [ + "http://www.arcforges.com:8080//other.example/path?x=1&x=2", + "https://arcforges.com//other.example/path?x=1&x=2", + ], +]; + +for (const [url, destination] of redirects) + test(`www redirects before static assets: ${url}`, async () => { + const asset = recordingAsset("asset"); + const response = await worker.fetch(new Request(url), asset.env); + assert.equal(response.status, 308); + assert.equal(response.headers.get("location"), destination); + assert.equal(asset.calls.length, 0); + }); + +for (const method of ["HEAD", "POST"]) + test(`canonical redirect preserves ${method} semantics`, async () => { + const asset = recordingAsset("asset"); + const response = await worker.fetch( + new Request("https://www.arcforges.com/api/example?mode=binary", { method }), + asset.env, + ); + assert.equal(response.status, 308); + assert.equal( + response.headers.get("location"), + "https://arcforges.com/api/example?mode=binary", + ); + assert.equal(asset.calls.length, 0); + }); + +for (const url of [ + "https://arcforges.com/cloud-hello/", + "http://127.0.0.1:4173/hello/", + "https://www.arcforges.com.other.example/", + "https://other.example/?next=https://www.arcforges.com/", +]) + test(`other hosts preserve the original asset request and response: ${url}`, async () => { + const request = new Request(url, { headers: { accept: "text/html" } }); + const asset = recordingAsset("asset", { "content-security-policy": "default-src 'self'" }); + const response = await worker.fetch(request, asset.env); + assert.equal(asset.calls.length, 1); + assert.equal(asset.calls[0], request); + assert.equal(response.headers.get("content-security-policy"), "default-src 'self'"); + }); + +test("delivery runs the reviewed Worker before assets without a deployment rebuild", async () => { + const config = JSON.parse(await readFile(join(root, "wrangler.json"), "utf8")); + assert.equal(config.main, "./worker/index.js"); + assert.equal(config.no_bundle, true); + assert.equal(config.assets.binding, "ASSETS"); + assert.equal(config.assets.run_worker_first, true); +}); + +test("the emitted candidate Worker is importless and carries no type syntax", async () => { + const emitted = await workerScript(); + assert.doesNotMatch(emitted, /^\s*import\s/mu); + assert.doesNotMatch(emitted, /\brequire\(/u); + assert.doesNotMatch(emitted, /\binterface\s|:\s*Env\b|:\s*Request\b/u); + assert.match(emitted, /export default \{/u); +}); + +for (const url of [ + "https://www.arcforges.com/", + "https://www.arcforges.com/cloud-hello/?name=a%2Fb&next=%2Fhello%2F", + "http://www.arcforges.com:8080//other.example/path?x=1&x=2", + "https://www.arcforges.com/api/example?mode=binary", + "https://arcforges.com/cloud-hello/", + "http://127.0.0.1:4173/hello/", +]) + test(`the emitted candidate Worker answers like the TypeScript source: ${url}`, async () => { + const directory = await mkdtemp(join(tmpdir(), "arcforges-emitted-worker-")); + try { + const modulePath = join(directory, "index.mjs"); + await writeFile(modulePath, await workerScript()); + const emitted = (await import(pathToFileURL(modulePath).href)).default; + const sourceAsset = recordingAsset("asset"); + const emittedAsset = recordingAsset("asset"); + const sourceResponse = await worker.fetch(new Request(url), sourceAsset.env); + const emittedResponse = await emitted.fetch(new Request(url), emittedAsset.env); + assert.equal(emittedResponse.status, sourceResponse.status); + assert.equal(emittedResponse.headers.get("location"), sourceResponse.headers.get("location")); + assert.equal(emittedAsset.calls.length, sourceAsset.calls.length); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }); + +test("candidate verification rejects a replaced private Worker independently of its manifest", async () => { + const directory = await mkdtemp(join(tmpdir(), "arcforges-canonical-worker-")); + try { + await mkdir(join(directory, "worker")); + const source = await workerScript(); + await writeFile(join(directory, "worker/index.js"), source); + await verifyWorkerScript(directory); + await writeFile( + join(directory, "worker/index.js"), + source.replace('arcforges.com";', 'other.example";'), + ); + await assert.rejects(verifyWorkerScript(directory), /Worker script changed/u); + } finally { + assert.equal(dirname(directory), tmpdir()); + assert.match(basename(directory), /^arcforges-canonical-worker-/u); + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/tooling/browser-provenance.ts b/tooling/browser-provenance.ts deleted file mode 100644 index c209069..0000000 --- a/tooling/browser-provenance.ts +++ /dev/null @@ -1,392 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import assert from "node:assert/strict"; -import { createHash } from "node:crypto"; -import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; -import path from "node:path"; -import type { Plugin } from "vite"; -import { auditProvenance, hash, parseDocument, readOwned, relative, store } from "./provenance.ts"; - -const owner = path.resolve(import.meta.dirname, ".."); -export const profilePath = "eng/provenance/profiles/browser-resources-r11.json"; -const sha = (value: string | Uint8Array) => createHash("sha256").update(value).digest("hex"); -const lf = (value: Buffer) => - new TextDecoder("utf-8", { fatal: true }).decode(value).replaceAll("\r\n", "\n"); -const json = (root: string, file: string) => parseDocument(readOwned(root, file)) as T; -const write = (root: string, file: string, value: unknown) => { - relative(file); - mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); - writeFileSync(path.join(root, file), `${JSON.stringify(value, null, 2)}\n`); -}; -export function members(root: string, prefix = ""): string[] { - return readdirSync(path.join(root, prefix), { withFileTypes: true }) - .flatMap((entry) => { - assert(!entry.isSymbolicLink(), "Linked artifact member"); - const file = `${prefix}${entry.name}`; - if (entry.isDirectory()) return members(root, `${file}/`); - assert(entry.isFile(), "Nonregular artifact member"); - return [relative(file)]; - }) - .sort(); -} -const logical = (name: string) => name.replace(/-[A-Za-z0-9_-]{8}(?=\.(?:js|css)$)/u, ""); -interface Module { - id: string; - imports: string[]; - dynamicImports: string[]; - sourceSha256: string | null; - helperSha256: string | null; -} -interface Chunk { - name: string; - modules: string[]; - imports: string[]; - dynamicImports: string[]; - exports: string[]; -} -interface Graph { - modules: Module[]; - chunks: Chunk[]; - assets: string[]; -} -interface Template { - sha256: string; - revisions: number; - routerVersions: number; - assetReferences: number; -} -interface Component { - name: string; - version: string; - spdx: string; - repository: string; - commit: string; - packagePath: string | null; - emitted: boolean; - role: string; -} -interface Profile { - schemaVersion: 1; - repository: "Web"; - graph: Graph; - templates: Record; - inputs: Record; - contracts: Record; - packageIdentities: Record; - noticeInputs: Record; - additionalNotices: string[]; - components: Component[]; - buildSbomSha256: string; - runtimeSbom: unknown; - evidence: unknown; -} -export function profile(root = owner): Profile { - const value = json(root, profilePath); - assert.equal(value.schemaVersion, 1); - assert.equal(value.repository, "Web"); - return value; -} - -// Observe the actual client compilation, without changing resolution or output bytes. -export function observeBrowser(): Plugin { - const clean = (id: string) => - id.replaceAll("\\", "/").replaceAll(`${owner.replaceAll("\\", "/")}/`, ""); - return { - name: "arcforges-browser-provenance", - enforce: "post", - generateBundle(_options, bundle) { - if (this.environment.name !== "client") return; - const modules = [...this.getModuleIds()] - .map((id): Module => { - const info = this.getModuleInfo(id); - assert(info, "Missing browser module information"); - const file = id.split("?")[0]; - assert(file); - const physical = existsSync(file); - const source = physical ? lf(readFileSync(file)) : null; - return { - id: clean(id), - imports: info.importedIds.map(clean).sort(), - dynamicImports: info.dynamicallyImportedIds.map(clean).sort(), - sourceSha256: source === null ? null : sha(source), - helperSha256: physical ? null : sha(clean(info.code ?? "")), - }; - }) - .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); - const chunks: Chunk[] = []; - const assets: string[] = []; - for (const item of Object.values(bundle)) { - if (item.type === "asset") assets.push(logical(item.fileName)); - else - chunks.push({ - name: logical(item.fileName), - modules: Object.keys(item.modules).map(clean).sort(), - imports: item.imports.map(logical).sort(), - dynamicImports: item.dynamicImports.map(logical).sort(), - exports: [...item.exports].sort(), - }); - } - chunks.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); - const graph = { modules, chunks, assets: assets.sort() }; - assert.deepEqual( - graph, - profile().graph, - "Actual browser module/resource graph changed; review a new profile", - ); - write(owner, "artifacts/evidence/browser-graph.json", graph); - }, - }; -} - -export function browserTemplates(root: string, revision: string): Record { - assert(/^[a-f0-9]{40}$/u.test(revision), "Invalid browser source identity"); - const all = members(root); - const aliases = new Map(); - for (const file of all.filter((file) => file.startsWith("assets/"))) { - assert( - /^assets\/[^/]+-[A-Za-z0-9_-]{8}\.(?:js|css)$/u.test(file), - "Unclassified compiled browser resource", - ); - const name = logical(path.posix.basename(file)); - assert(!aliases.has(name), "Duplicate logical browser resource"); - aliases.set(name, path.posix.basename(file)); - } - const manifestName = aliases.get("manifest.js"); - assert(manifestName, "Missing Router manifest"); - const manifestText = lf(readOwned(root, `assets/${manifestName}`)); - assert( - manifestText.startsWith("window.__reactRouterManifest=") && manifestText.endsWith(";"), - "Changed Router manifest wrapper", - ); - const manifest = parseDocument( - manifestText.slice("window.__reactRouterManifest=".length, -1), - ) as { entry: unknown; routes: unknown; version: string }; - const version = sha(JSON.stringify({ entry: manifest.entry, routes: manifest.routes })).slice( - 0, - 8, - ); - assert.equal(manifest.version, version, "Router manifest fingerprint mismatch"); - assert.equal(manifestName, `manifest-${version}.js`); - const expected = profile().templates; - for (const name of aliases.keys()) - assert(Object.hasOwn(expected, `assets/${name}`), "Unclassified compiled browser resource"); - const result: Record = {}; - for (const file of all) { - const key = file.startsWith("assets/") ? `assets/${logical(path.posix.basename(file))}` : file; - if (!Object.hasOwn(expected, key)) continue; // The complete candidate member gate checks all remaining packaging outputs. - let value = lf(readOwned(root, file)); - assert( - !/|/u.test(value), - "Reserved template marker in input", - ); - for (const [name, actual] of [...aliases].sort((a, b) => b[1].length - a[1].length)) - value = value.replaceAll(actual, ``); - value = value.replaceAll(revision, "").replaceAll(version, ""); - result[key] = { - sha256: sha(value), - revisions: value.split("").length - 1, - routerVersions: value.split("").length - 1, - assetReferences: value.split("; - }>(root, "package-lock.json"); - for (const [file, expected] of Object.entries(approved.packageIdentities)) { - const actual = lock.packages[file]; - assert(actual, `Missing reviewed package: ${file}`); - assert.deepEqual( - { version: actual.version, integrity: actual.integrity, resolved: actual.resolved }, - expected, - `Package identity changed: ${file}`, - ); - } - return approved; -} -export function extraNotices(root = owner): string { - const approved = verifyBrowserInputs(root); - let result = "\n===== Additional generated browser resources and embedded origins =====\n"; - for (const file of approved.additionalNotices) - result += `\n${file}\n${lf(readOwned(root, file))}\n`; - return result; -} - -interface Bom { - serialNumber?: string; - metadata: { timestamp?: string; component: { name: string } }; -} -export function canonicalBuildSbom(value: unknown): unknown { - const copy = structuredClone(value) as Bom; - delete copy.serialNumber; - delete copy.metadata.timestamp; - copy.metadata.component.name = "@arcforges/web-workspace"; - return copy; -} -export function canonicalJson(value: unknown): string { - if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; - if (value !== null && typeof value === "object") - return `{${Object.entries(value) - .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`) - .join(",")}}`; - return JSON.stringify(value); -} -export function runtimeSbom() { - return profile().runtimeSbom; -} - -interface Identity { - source: string; - version: string; -} -function publicNames(root: string) { - return members(path.join(root, "assets")); -} -function receipt(root: string, identity: Identity, active: string[]) { - return { - schemaVersion: 1, - repository: "Web", - source: identity.source, - version: identity.version, - profile: profilePath, - profileSha256: hash(readOwned(owner, profilePath), "lf"), - records: active, - parsedModules: profile().graph.modules.length, - emittedModules: new Set(profile().graph.chunks.flatMap((chunk) => chunk.modules)).size, - members: Object.fromEntries( - members(root) - .filter((file) => !["manifest.json", "provenance/receipt.json"].includes(file)) - .map((file) => [file, hash(readOwned(root, file))]), - ), - }; -} -export function prepareProvenance(root: string, identity: Identity) { - const audit = auditProvenance(owner); - const graph = json(owner, "artifacts/evidence/browser-graph.json"); - assert.deepEqual(graph, profile().graph); - write(root, "provenance/browser-graph.json", graph); - writeFileSync(path.join(root, "provenance/profile.json"), lf(readOwned(owner, profilePath))); - mkdirSync(path.join(root, "provenance/records"), { recursive: true }); - for (const id of audit.activeRecords) - writeFileSync( - path.join(root, `provenance/records/${id}.json`), - lf(readOwned(owner, `${store}${id}.json`)), - ); - writeFileSync( - path.join(root, "assets/source-provenance.txt"), - readOwned(owner, "eng/provenance/NOTICE.txt"), - ); - write(root, "provenance/receipt.json", receipt(root, identity, audit.activeRecords)); -} -export function verifyBrowserCandidate( - root: string, - identity: Identity, - notices: string, - headers: string, -) { - const audit = auditProvenance(owner); - const approved = verifyBrowserInputs(); - assert.deepEqual( - json(root, "provenance/profile.json"), - approved, - "Candidate substituted the reviewed profile", - ); - assert.equal( - hash(readOwned(root, "provenance/profile.json"), "lf"), - hash(readOwned(owner, profilePath), "lf"), - "Candidate profile bytes changed", - ); - assert.deepEqual( - json(root, "provenance/browser-graph.json"), - approved.graph, - "Candidate graph differs from actual reviewed inputs", - ); - for (const id of audit.activeRecords) { - assert.deepEqual( - json(root, `provenance/records/${id}.json`), - json(owner, `${store}${id}.json`), - "Candidate record changed", - ); - assert.equal( - hash(readOwned(root, `provenance/records/${id}.json`), "lf"), - hash(readOwned(owner, `${store}${id}.json`), "lf"), - "Candidate record bytes changed", - ); - } - browserTemplates(path.join(root, "assets"), identity.source); - const expectedPublic = [ - ...Object.keys(approved.templates).filter((name) => !name.startsWith("assets/")), - ...publicNames(root) - .filter((name) => name.startsWith("assets/")) - .map(relative), - "_headers", - "__build.json", - "__build-info.json", - "license.txt", - "third-party-notices.txt", - "source-provenance.txt", - ].sort(); - assert.deepEqual(publicNames(root), expectedPublic, "Unclassified public member"); - for (const [file, expected] of Object.entries(approved.contracts)) - assert.equal( - hash(readOwned(root, file)), - expected, - `Published Contracts member changed: ${file}`, - ); - assert.equal( - lf(readOwned(root, "assets/license.txt")), - lf(readOwned(owner, "LICENSE")), - "Full AGPL legal text changed", - ); - assert.equal( - lf(readOwned(root, "assets/third-party-notices.txt")), - notices.replaceAll("\r\n", "\n"), - "Required full third-party notices changed", - ); - assert.equal( - lf(readOwned(root, "assets/source-provenance.txt")), - lf(readOwned(owner, "eng/provenance/NOTICE.txt")), - "Source notice changed", - ); - assert.equal(lf(readOwned(root, "assets/_headers")), headers, "Security/cache headers changed"); - assert.equal( - sha(canonicalJson(json(root, "sbom.cdx.json"))), - approved.buildSbomSha256, - "Build SBOM identities/edges changed", - ); - assert.deepEqual( - json(root, "runtime-sbom.cdx.json"), - approved.runtimeSbom, - "Emitted browser SBOM identities/edges changed", - ); - const expectedFiles = [ - ...expectedPublic.map((name) => `assets/${name}`), - ...Object.keys(approved.contracts), - ...audit.activeRecords.map((id) => `provenance/records/${id}.json`), - "provenance/profile.json", - "provenance/browser-graph.json", - "provenance/receipt.json", - "sbom.cdx.json", - "runtime-sbom.cdx.json", - "wrangler.json", - "worker/index.js", - "manifest.json", - ].sort(); - assert.deepEqual(members(root), expectedFiles, "Unclassified candidate member"); - assert.deepEqual( - json(root, "provenance/receipt.json"), - receipt(root, identity, audit.activeRecords), - "Source-bound provenance receipt changed", - ); -} diff --git a/tooling/build-identity.ts b/tooling/build-identity.ts index 1376681..77a6b7d 100644 --- a/tooling/build-identity.ts +++ b/tooling/build-identity.ts @@ -234,6 +234,19 @@ export function verifyContractSources( assert.deepEqual([...seen].sort(), Object.keys(schemaSources).sort()); } +export const contractsPackage = "ArcForges.Contracts.PublicApi"; +export const contractsRecord = "eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1"; +/** The one central NuGet pin of the Contracts publication (Directory.Packages.props). */ +export function contractsPackageVersion(props: string) { + const pins = [ + ...props.matchAll( + //gu, + ), + ]; + assert.equal(pins.length, 1, "Exactly one central pin of the Contracts publication"); + return pins[0]?.[1] ?? ""; +} + export function expectedIdentity(version: string) { const git = (...args: string[]) => execFileSync("git", args, { @@ -254,25 +267,15 @@ export function expectedIdentity(version: string) { const read = (name: string) => { if (name === "release/app.json") return JSON.stringify({ versions: [{ subject: "Web", version }] }); - if (name === "packages/contracts/source.json") - return readFileSync( - path.join(root, "node_modules/@arcforges/api-client/source.json"), - "utf8", - ); - if (name === "packages/contracts/build-identity.json") - return readFileSync( - path.join(root, "node_modules/@arcforges/api-client/build-identity.json"), - "utf8", - ); return readFileSync(path.join(root, name), "utf8"); }; - // The consumed producer receipt must match the independently pinned site package. - const contracts = object(JSON.parse(read("packages/contracts/source.json"))); - const pinned = JSON.parse(readFileSync(path.join(root, "apps/site/package.json"), "utf8")) - .dependencies["@arcforges/api-client"]; + // The consumed Contracts publication is the NuGet package the Web profiles reference. Its committed record is the + // CON.07 publication copy, and it must match the exact package version pinned in Directory.Packages.props. + const contracts = object(JSON.parse(read(`${contractsRecord}/source.json`))); + const pinned = contractsPackageVersion(read("Directory.Packages.props")); assert.equal(contracts.version, pinned); - const identity = object(JSON.parse(read("packages/contracts/build-identity.json"))); - assert.equal(object(identity.artifact).id, "@arcforges/api-client"); + const identity = object(JSON.parse(read(`${contractsRecord}/build-identity.json`))); + assert.equal(object(identity.artifact).id, contractsPackage); assert.equal(object(identity.artifact).version, pinned); assert.equal(object(identity.build).sourceCommit, contracts.commit); verifyContractSources(object(identity.axes).ContractSet, contracts); diff --git a/tooling/cloudflare.ts b/tooling/cloudflare.ts index 60f3ace..467e1ef 100644 --- a/tooling/cloudflare.ts +++ b/tooling/cloudflare.ts @@ -1,11 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; +import { spawnSync } from "node:child_process"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { setTimeout as delay } from "node:timers/promises"; import { verifyCandidate } from "./candidate.ts"; -import { candidate, digest, json, npm, root, run, save } from "./project.ts"; +import { candidate, digest, json, root, run, save } from "./project.ts"; const statePath = join(root, "artifacts/deployment.json"); type Identity = { source: string; version: string }; @@ -228,10 +229,24 @@ async function smoke() { ); } }); - console.log("Verifying the real domain in Chromium, Firefox and WebKit."); - process.stdout.write( - npm(["exec", "--no", "--", "playwright", "test", "--config", "playwright.live.config.ts"]), + // The local opt-in C# browser suite (tests/browser) reads the deployed domain. It never runs on CI (P2-017). + console.log("Verifying the real domain in local Chromium through the C# browser suite."); + const browserSuite = spawnSync( + "dotnet", + ["test", join(root, "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj"), "-c", "Release"], + { + cwd: root, + encoding: "utf8", + windowsHide: true, + env: { + ...process.env, + ARCFORGES_LOCAL_BROWSER: "1", + ARCFORGES_BROWSER_BASE_URL: state.url, + }, + }, ); + process.stdout.write(browserSuite.stdout ?? ""); + assert.equal(browserSuite.status, 0, "The local browser suite failed."); await save(statePath, { ...state, verified: true, diff --git a/tooling/dependency-policy.test.ts b/tooling/dependency-policy.test.ts index fd62480..cf39069 100644 --- a/tooling/dependency-policy.test.ts +++ b/tooling/dependency-policy.test.ts @@ -37,10 +37,10 @@ test("same version with altered bytes", () => { packages[firstDependency].integrity = `sha512-${Buffer.alloc(64).toString("base64")}`; assert.throws(() => validateClosure(baseline, packages), /mutable-version/u); }); -test("wrong publisher", () => { +test("retired first-party npm package cannot re-enter the admission", () => { const policy = structuredClone(baseline); - policy.firstParty["@arcforges/proto"] = { publisher: "other/Contracts", visibility: "public" }; - assert.throws(() => validatePolicy(policy), /Wrong publisher/u); + policy.firstParty["@arcforges/proto"] = { publisher: "ArcForges/Contracts", visibility: "public" }; + assert.throws(() => validatePolicy(policy), /Unadmitted internal\/first-party package/u); }); test("untrusted registry", () => { const packages = structuredClone(lock.packages); @@ -77,7 +77,10 @@ test("source cannot escape into a sibling repository", () => { () => validateImports(root, "example.ts", 'import "../Contracts/private.ts";', baseline), /Sibling source/u, ); - validateImports(root, "src/example.ts", 'import type { X } from "@arcforges/proto";', baseline); + assert.throws( + () => validateImports(root, "src/example.ts", 'import type { X } from "@arcforges/proto";', baseline), + /Forbidden internal import/u, + ); }); test("stable closure cannot inherit foundation candidates", () => { const policy = structuredClone(baseline); diff --git a/tooling/dependency-policy.ts b/tooling/dependency-policy.ts index 9a42ec9..430c24e 100644 --- a/tooling/dependency-policy.ts +++ b/tooling/dependency-policy.ts @@ -90,14 +90,14 @@ export function validatePolicy(policy: Policy) { "Native adoption requires AD-01 through AD-08", ); assert(policy.native.evidence.length > 0, "Missing native closure evidence"); - for (const [name, entry] of Object.entries(policy.firstParty)) { - assert( - ["@arcforges/proto", "@arcforges/api-client"].includes(name), - `Unadmitted internal/first-party package: ${name}`, - ); - assert.equal(entry.publisher, "ArcForges/Contracts", `Wrong publisher: ${name}`); - assert.equal(entry.visibility, "public", `Internal package: ${name}`); - } + // The Contracts npm packages (@arcforges/proto and @arcforges/api-client) are retired by WEB.40 U5: the naming + // authority is vendored from its NuGet publication and the identity is the NuGet record. No first-party npm package + // is admitted, so a first-party entry is refused before its publisher is inspected. + assert.deepEqual( + Object.keys(policy.firstParty), + [], + "Unadmitted internal/first-party package: the Contracts npm packages are retired", + ); } export function immutableCoordinates(closure: Record) { const coordinates: Record = {}; diff --git a/tooling/ide.ts b/tooling/ide.ts deleted file mode 100644 index 14add2b..0000000 --- a/tooling/ide.ts +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { digest, root, save, verify } from "./project.ts"; -import { gitEnvironment } from "./provenance.ts"; - -assert.equal(process.platform, "win32", "The IDE adapter is a Windows entry point."); -const evidence = join(root, "artifacts/evidence"); -await mkdir(evidence, { recursive: true }); -async function msbuild(name: string, args: string[]) { - const result = spawnSync("dotnet", ["msbuild", "-nologo", ...args], { - cwd: root, - env: gitEnvironment(), - encoding: "utf8", - windowsHide: true, - maxBuffer: 16 * 1024 * 1024, - timeout: 10 * 60 * 1000, - }); - const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; - await writeFile(join(evidence, `ide-${name}.log`), output); - assert.equal(result.status, 0, `IDE ${name} failed: ${result.error ?? output}`); - return output; -} - -const properties = JSON.parse( - await msbuild("properties", [ - "ArcForges.Web.esproj", - "-getProperty:StartupCommand,BuildCommand,ShouldRunNpmInstall,ShouldRunBuildScript,PublishAot,IsAotCompatible", - "-getItem:ProjectReference", - ]), -); -assert.deepEqual(properties.Items.ProjectReference, []); -assert.equal(properties.Properties.StartupCommand, "npm run dev"); -assert.equal(properties.Properties.BuildCommand, "npm run build && npm run build:profiles"); -assert.equal(properties.Properties.ShouldRunNpmInstall, "false"); -assert.equal(properties.Properties.ShouldRunBuildScript, "true"); -assert.equal(properties.Properties.PublishAot, ""); -assert.equal(properties.Properties.IsAotCompatible, ""); - -const lock = join(root, "package-lock.json"); -const lockHash = digest(await readFile(lock)); -const restore = await msbuild("restore", ["win.slnx", "-t:Restore"]); -assert( - restore.includes("ArcForges explicit locked npm restore"), - "Solution restore skipped npm ci.", -); -assert.equal(digest(await readFile(lock)), lockHash, "IDE restore changed the committed lock."); -const installedLock = join(root, "node_modules/.package-lock.json"); -const installed = { - hash: digest(await readFile(installedLock)), - mtimeMs: (await stat(installedLock)).mtimeMs, -}; -const designTime = await msbuild("design-time", [ - "win.slnx", - "-t:Restore", - "-p:DesignTimeBuild=true", -]); -assert( - !designTime.includes("ArcForges explicit locked npm restore"), - "Design-time evaluation unexpectedly restored npm.", -); -assert.equal((await stat(installedLock)).mtimeMs, installed.mtimeMs); -const build = await msbuild("build", ["win.slnx", "-t:Build", "-p:Configuration=Release"]); -assert(build.includes("Verified candidate "), "Solution build skipped the npm candidate build."); -assert( - build.includes("account: ") && build.includes("chat: "), - "Solution build skipped the production profile builds.", -); -assert( - !build.includes("ArcForges explicit locked npm restore"), - "Build unexpectedly restored npm.", -); -assert.equal(digest(await readFile(lock)), lockHash, "Build changed the committed lock."); -assert.equal(digest(await readFile(installedLock)), installed.hash); -assert.equal( - (await stat(installedLock)).mtimeMs, - installed.mtimeMs, - "Build reinstalled npm dependencies.", -); -const manifest = await verify(); -const startup = JSON.parse( - await msbuild("startup", [ - "ArcForges.Web.esproj", - "-t:ComputeRunArguments", - "-getProperty:RunCommand,RunArguments,RunWorkingDirectory", - ]), -); -assert(startup.Properties.RunArguments.includes("npm run dev")); -assert.equal((await stat(installedLock)).mtimeMs, installed.mtimeMs); -const launch = JSON.parse(await readFile(join(root, ".vscode/launch.json"), "utf8")); -assert.equal(launch.configurations[0].url, "http://127.0.0.1:5173"); -await save(join(evidence, "ide-entrypoints.json"), { - source: manifest.source, - candidateVersion: manifest.version, - properties, - startup, - lockSha256: lockHash, - solutionRestore: "passed", - solutionBuild: "passed", - designTimeRestoredDependencies: false, - buildReinstalledDependencies: false, - actualVisualStudioF5: "separate local IDE verification; CLI dispatch is not F5 evidence", -}); -console.log("IDE solution restore/build and startup dispatch passed."); diff --git a/tooling/project.ts b/tooling/project.ts index c30b3f2..db0c8f9 100644 --- a/tooling/project.ts +++ b/tooling/project.ts @@ -1,28 +1,21 @@ // SPDX-License-Identifier: AGPL-3.0-only +// The Node tooling that remains after the React removal (WEB.40 unit U5). The public Site, the Blazor profiles, the sealed +// candidate and the profile bundle are built and verified by the C# tool (tools/ArcForges.Web.Tooling). What stays here is +// the Worker emission, the reviewed policy checks and the npm lock provenance rule. import assert from "node:assert/strict"; -import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; import { stripTypeScriptTypes } from "node:module"; -import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; -import { dirname, join, relative, resolve, sep } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { type DefaultTreeAdapterTypes, parse } from "parse5"; import { auditLicences, evaluatedManagedLicences } from "./licence-boundary.ts"; -import { expectedIdentity, verifyIdentity } from "./build-identity.ts"; import { auditProvenance, gitEnvironment } from "./provenance.ts"; -import { - canonicalBuildSbom, - extraNotices, - prepareProvenance, - runtimeSbom, - verifyBrowserCandidate, - verifyBrowserInputs, -} from "./browser-provenance.ts"; export const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); export const candidate = join(root, "artifacts/candidate"); -// The canonical-host Worker is authored in TypeScript (worker/index.ts). The candidate ships its -// JavaScript form, produced by Node type stripping: no bundling, no import, no added code. +// The canonical-host Worker is authored in TypeScript (worker/index.ts). The candidate ships its JavaScript form, produced +// by Node type stripping: no bundling, no import, no added code. export const workerSource = join(root, "worker/index.ts"); export async function workerScript(): Promise { const source = (await readFile(workerSource, "utf8")).replaceAll("\r\n", "\n"); @@ -47,8 +40,7 @@ export function npm(args: string[]): string { assert(process.env.npm_execpath, "Run this command through npm run."); return run(process.execPath, [process.env.npm_execpath, ...args]); } -export const digest = (value: string | Uint8Array) => - createHash("sha256").update(value).digest("hex"); +export const digest = (value: string | Uint8Array) => createHash("sha256").update(value).digest("hex"); export async function json(path: string) { return JSON.parse(await readFile(path, "utf8")); } @@ -56,17 +48,6 @@ export async function save(path: string, value: unknown) { await mkdir(dirname(path), { recursive: true }); await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); } -export async function files(directory: string): Promise { - const found: string[] = []; - for (const entry of await readdir(directory, { withFileTypes: true })) { - assert(!entry.isSymbolicLink(), `Symlink not allowed in candidate: ${entry.name}`); - if (entry.isDirectory()) - for (const child of await files(join(directory, entry.name))) - found.push(`${entry.name}/${child}`); - else found.push(entry.name); - } - return found.sort(); -} export function releaseVersion(runNumber: string | undefined, attempt: string | undefined) { if (!runNumber) return "0.1.0-local"; assert( @@ -75,116 +56,7 @@ export function releaseVersion(runNumber: string | undefined, attempt: string | ); return `0.1.0-ci.${runNumber}.${attempt}`; } -export function contentSecurityPolicy(html: string[]) { - const scripts = new Set(); - function visit(node: DefaultTreeAdapterTypes.Node) { - if ( - "tagName" in node && - node.tagName === "script" && - !node.attrs.some((attr) => attr.name === "src") - ) { - const script = node.childNodes.map((child) => ("value" in child ? child.value : "")).join(""); - if (script) scripts.add(`'sha256-${createHash("sha256").update(script).digest("base64")}'`); - } - if ("childNodes" in node) for (const child of node.childNodes) visit(child); - } - for (const page of html) visit(parse(page)); - return `default-src 'self'; script-src 'self' ${[...scripts].sort().join(" ")}; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'none'`; -} -interface Manifest { - schema: 1; - version: string; - source: string; - dirty: boolean; - files: Record; -} -export async function seal(directory: string, identity: Omit) { - const hashes: Record = {}; - for (const path of await files(directory)) - if (path !== "manifest.json") hashes[path] = digest(await readFile(join(directory, path))); - await save(join(directory, "manifest.json"), { schema: 1, ...identity, files: hashes }); -} -export async function verify( - directory = candidate, - expectedSource = process.env.GITHUB_SHA, -): Promise { - const manifest = (await json(join(directory, "manifest.json"))) as Manifest; - assert.deepEqual(Object.keys(manifest).sort(), ["dirty", "files", "schema", "source", "version"]); - assert.equal(manifest.schema, 1); - assert.equal(typeof manifest.dirty, "boolean"); - assert( - /^0\.1\.0-(?:local|ci\.[1-9]\d*\.[1-9]\d*)$/u.test(manifest.version), - "Invalid candidate version", - ); - assert(/^[a-f0-9]{40}$/.test(manifest.source), "Invalid source revision"); - if (expectedSource) - assert.equal(manifest.source, expectedSource, "Candidate source does not match this run"); - assert.deepEqual( - (await files(directory)).filter((path) => path !== "manifest.json"), - Object.keys(manifest.files).sort(), - "Candidate file set changed", - ); - for (const [path, hash] of Object.entries(manifest.files)) { - const absolute = resolve(directory, path); - assert(absolute.startsWith(resolve(directory) + sep), "Invalid candidate path"); - assert.equal(digest(await readFile(absolute)), hash, `Candidate changed: ${path}`); - if (path.startsWith("assets/")) - assert( - !/(?:\.map$|(?:^|\/)(?:\.env|\.dev\.vars|node_modules|server|\.vite|__spa-fallback))/.test( - path, - ), - `Private/build file in public assets: ${path}`, - ); - } - const config = await json(join(directory, "wrangler.json")); - assert.deepEqual(Object.keys(config).sort(), [ - "assets", - "compatibility_date", - "main", - "name", - "no_bundle", - "preview_urls", - "workers_dev", - ]); - assert.equal(config.name, "arcforges-web"); - assert.equal(config.main, "./worker/index.js"); - assert.equal(config.no_bundle, true); - assert.equal(config.preview_urls, false); - assert.equal(config.workers_dev, false); - assert.deepEqual(config.assets, { - directory: "./assets", - binding: "ASSETS", - run_worker_first: true, - html_handling: "auto-trailing-slash", - not_found_handling: "404-page", - }); - await verifyWorkerScript(directory); - const identity = await json(join(directory, "assets/__build.json")); - assert.deepEqual(identity, { source: manifest.source, version: manifest.version }); - verifyIdentity(await json(join(directory, "assets/__build-info.json")), manifest.version); - for (const path of [ - "assets/index.html", - "assets/hello/index.html", - "assets/cloud-hello/index.html", - "assets/404.html", - "assets/_headers", - "sbom.cdx.json", - "runtime-sbom.cdx.json", - "assets/license.txt", - "assets/third-party-notices.txt", - ]) - assert(manifest.files[path], `Missing required file: ${path}`); - const html = await Promise.all( - ["index.html", "hello/index.html", "cloud-hello/index.html"].map((file) => - readFile(join(directory, "assets", file), "utf8"), - ), - ); - verifyBrowserCandidate(directory, manifest, await notices(), securityHeaders(html)); - const { $schema: _schema, ...expectedConfig } = await json(join(root, "wrangler.json")); - expectedConfig.assets.directory = "./assets"; - assert.deepEqual(config, expectedConfig, "Deployment configuration changed"); - return manifest; -} +/** The C# candidate tool emits the Worker's expected bytes; the candidate must carry exactly them. */ export async function verifyWorkerScript(directory: string) { assert.equal( await readFile(join(directory, "worker/index.js"), "utf8"), @@ -192,112 +64,7 @@ export async function verifyWorkerScript(directory: string) { "Worker script changed from the reviewed source", ); } -async function notices() { - const lock = await json(join(root, "package-lock.json")); - let text = - "ArcForges Web third-party notices\nGenerated from the production dependency closure. Some packages are build-time only or tree-shaken.\n"; - for (const [path, entry] of Object.entries(lock.packages) as [ - string, - { dev?: boolean; link?: boolean; version?: string; license?: string }, - ][]) { - if (!path.includes("node_modules/") || entry.dev || entry.link) continue; - const name = path.split("node_modules/").at(-1); - text += `\n===== ${name} ${entry.version} (${entry.license ?? "see included license"}) =====\n`; - const directory = join(root, path); - const names = (await readdir(directory)).filter((name) => - /^(?:license|licence|copying|notice)(?:[.-].*)?$/i.test(name), - ); - if ( - name === "@bufbuild/protobuf" || - name === "@connectrpc/connect" || - name === "@connectrpc/connect-web" - ) { - const group = name === "@bufbuild/protobuf" ? "protobuf-es" : "connect-es"; - text += await readFile(join(root, "third-party", `${group}-LICENSE.txt`), "utf8"); - const sources = [ - "dist/esm/index.js", - ...(name === "@bufbuild/protobuf" ? ["dist/esm/wire/varint.js"] : []), - ]; - for (const source of sources) { - const header = (await readFile(join(directory, source), "utf8")).match( - /^(?:\/\/[^\n]*\n)+/, - )?.[0]; - assert(header?.includes("Copyright"), `Review changed license header in ${name}/${source}`); - text += `\n${header}\n`; - } - } else assert(names.length > 0, `Review missing upstream license: ${name}`); - for (const name of names) - text += `\n${name}\n${await readFile(join(directory, name), "utf8")}\n`; - } - return text + extraNotices(); -} -function securityHeaders(html: string[]) { - const csp = contentSecurityPolicy(html); - assert(csp.length < 1800, "CSP exceeds the Workers header line budget"); - return `/*\n Content-Security-Policy: ${csp}\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()\n X-Frame-Options: DENY\n X-Robots-Tag: noindex, nofollow\n Cache-Control: public, no-cache, no-transform\n/assets/*\n ! Cache-Control\n Cache-Control: public, max-age=31536000, immutable, no-transform\n/__build.json\n ! Cache-Control\n Cache-Control: no-store, no-transform\n/__build-info.json\n ! Cache-Control\n Cache-Control: no-store, no-transform\n`; -} -async function build() { - await save(join(root, "artifacts/evidence/licence-boundary.json"), auditLicences(root)); - await save(join(root, "artifacts/evidence/source-provenance.json"), auditProvenance(root)); - verifyBrowserInputs(); - const source = run("git", ["rev-parse", "HEAD"]).trim(); - const dirty = run("git", ["status", "--porcelain", "--untracked-files=normal"]).trim().length > 0; - const version = releaseVersion(process.env.GITHUB_RUN_NUMBER, process.env.GITHUB_RUN_ATTEMPT); - process.env.VITE_SOURCE_REF = source; - process.stdout.write(npm(["run", "build", "--workspace", "@arcforges/web-site"])); - // Only reset this task's generated candidate, after checking its absolute boundary. - assert.equal(relative(join(root, "artifacts"), candidate), "candidate"); - await rm(candidate, { recursive: true, force: true }); - await mkdir(candidate, { recursive: true }); - const publicRoot = join(candidate, "assets"); - const built = join(root, "apps/site/build/client"); - for (const path of await files(built)) { - if (path.startsWith(".vite/") || path === "__spa-fallback.html") continue; - await mkdir(dirname(join(publicRoot, path)), { recursive: true }); - await cp(join(built, path), join(publicRoot, path)); - } - const html: string[] = []; - for (const path of await files(publicRoot)) - if (path.endsWith(".html")) { - // HTML parsing normalizes CRLF before CSP checks: hash the bytes browsers execute. - const page = (await readFile(join(publicRoot, path), "utf8")).replaceAll("\r\n", "\n"); - await writeFile(join(publicRoot, path), page); - html.push(page); - } - await writeFile(join(publicRoot, "_headers"), securityHeaders(html)); - await cp(join(root, "LICENSE"), join(publicRoot, "license.txt")); - await writeFile(join(publicRoot, "third-party-notices.txt"), await notices()); - await save(join(publicRoot, "__build.json"), { version, source }); - const buildIdentity = expectedIdentity(version); - assert.equal(buildIdentity.build.sourceCommit, source); - await save(join(publicRoot, "__build-info.json"), buildIdentity); - const { $schema: _schema, ...config } = await json(join(root, "wrangler.json")); - config.assets.directory = "./assets"; - await save(join(candidate, "wrangler.json"), config); - await mkdir(join(candidate, "worker"), { recursive: true }); - await writeFile(join(candidate, "worker/index.js"), await workerScript()); - for (const packageName of ["proto", "api-client"]) - for (const path of ["source.json", "LICENSE", "NOTICE", "sbom.cdx.json"]) { - await mkdir(join(candidate, "contracts", packageName), { recursive: true }); - await cp( - join(root, "node_modules/@arcforges", packageName, path), - join(candidate, "contracts", packageName, path), - ); - } - await save( - join(candidate, "sbom.cdx.json"), - canonicalBuildSbom( - JSON.parse(npm(["sbom", "--sbom-format", "cyclonedx", "--package-lock-only"])), - ), - ); - await save(join(candidate, "runtime-sbom.cdx.json"), runtimeSbom()); - prepareProvenance(candidate, { source, version }); - await seal(candidate, { version, source, dirty }); - await verify(); - console.log( - `Verified candidate ${version} at ${source}${dirty ? " (local changes; not deployable)" : ""}`, - ); -} + type LockEntry = { link?: boolean; inBundle?: boolean; integrity?: string; resolved?: string }; function requireRegistryArtifact(path: string, entry: LockEntry) { @@ -308,16 +75,12 @@ function requireRegistryArtifact(path: string, entry: LockEntry) { ); } +/** The lock may hold registry artifacts only. The repository has no npm workspaces, so no link is admitted. */ export function verifyLockProvenance(packages: Record) { for (const [path, entry] of Object.entries(packages)) { if (!path.includes("node_modules/")) continue; if (entry.link) { - assert(!entry.inBundle, `Bundled dependency cannot be a workspace link: ${path}`); - assert( - ["apps/app", "apps/site", "packages/ui"].includes(entry.resolved ?? ""), - "Unexpected workspace link", - ); - continue; + assert.fail(`Unexpected workspace link: ${path}`); } let artifactPath = path; let artifact = entry; @@ -339,13 +102,12 @@ export function verifyLockProvenance(packages: Record) { } } -async function policy() { - const { auditRepository } = await import("../eng/policy/architecture.ts"); - await save(join(root, "artifacts/evidence/architecture.json"), auditRepository(root)); +export async function policy() { + await save(join(root, "artifacts/evidence/licence-boundary.json"), auditLicences(root)); + await save(join(root, "artifacts/evidence/source-provenance.json"), auditProvenance(root)); const naming = await json(join(root, "eng/policy/naming-candidate.json")); - const namingRoot = join(root, "node_modules", naming.package); - assert.equal((await json(join(namingRoot, "package.json"))).version, naming.version); - assert.equal((await json(join(namingRoot, "source.json"))).commit, naming.sourceCommit); + assert.equal(naming.packageSource, "NuGet", "The naming authority is the NuGet Contracts publication"); + const namingRoot = join(root, naming.root); for (const [asset, expected] of Object.entries(naming.assets)) assert.equal( digest(await readFile(join(namingRoot, asset))), @@ -353,58 +115,37 @@ async function policy() { `Naming asset changed: ${asset}`, ); run("python", [ - join(root, "node_modules/@arcforges/proto/tools/naming/eng/check_naming.py"), + "-I", + join(namingRoot, "tools/naming/eng/check_naming.py"), "--repository", `Web=${root}`, "--report", join(root, "artifacts/evidence/naming.json"), ]); - await save(join(root, "artifacts/evidence/licence-boundary.json"), auditLicences(root)); - await save(join(root, "artifacts/evidence/source-provenance.json"), auditProvenance(root)); - verifyBrowserInputs(); assert.equal( process.version, `v${(await readFile(join(root, ".node-version"), "utf8")).trim()}`, "Select the pinned Node version", ); - const packagePaths = [ - "package.json", - "apps/app/package.json", - "apps/site/package.json", - "packages/ui/package.json", - ]; - for (const path of packagePaths) { - const manifest = await json(join(root, path)); - assert.equal(manifest.private, true, `${path} must not be published to npm`); - assert.equal(manifest.license, "AGPL-3.0-only"); - for (const section of ["dependencies", "devDependencies", "peerDependencies"]) - for (const [name, version] of Object.entries(manifest[section] ?? {})) { - assert( - /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(String(version)), - `${path}: pin ${name} exactly`, - ); - } - } + const manifest = await json(join(root, "package.json")); + assert.equal(manifest.private, true, "package.json must not be published to npm"); + assert.equal(manifest.license, "AGPL-3.0-only"); + for (const section of ["dependencies", "devDependencies", "peerDependencies"]) + for (const [name, version] of Object.entries(manifest[section] ?? {})) + assert( + /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(String(version)), + `package.json: pin ${name} exactly`, + ); const lock = await json(join(root, "package-lock.json")); assert.equal(lock.lockfileVersion, 3); verifyLockProvenance(lock.packages); assert.equal((await json(join(root, "wrangler.json"))).assets.not_found_handling, "404-page"); - assert.equal( - (await json(join(root, "package.json"))).packageManager, - `npm@${npm(["--version"]).trim()}`, - ); + assert.equal(manifest.packageManager, `npm@${npm(["--version"]).trim()}`); console.log("Toolchain, exact dependencies, lock provenance and static routing policy passed."); } + async function main() { switch (process.argv[2]) { - case "build": - await build(); - break; - case "verify": { - const manifest = await verify(); - console.log(`Candidate verified: ${manifest.version} ${manifest.source}`); - break; - } case "policy": await policy(); break; @@ -420,7 +161,7 @@ async function main() { console.log("Hooks enabled for this worktree."); break; default: - throw new Error("Use build, verify, policy or hooks."); + throw new Error("Use policy, licence-evaluated or hooks."); } } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main(); diff --git a/tools/ArcForges.Web.Tooling/packages.lock.json b/tools/ArcForges.Web.Tooling/packages.lock.json index 69416d1..a7e5ef9 100644 --- a/tools/ArcForges.Web.Tooling/packages.lock.json +++ b/tools/ArcForges.Web.Tooling/packages.lock.json @@ -51,4 +51,4 @@ } } } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 398b17a..0d6da7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,10 @@ { "compilerOptions": { "target": "ES2023", - "lib": ["ES2023", "DOM", "DOM.Iterable"], + "lib": ["ES2023", "DOM"], "module": "ESNext", "moduleResolution": "Bundler", - "jsx": "react-jsx", - "types": ["node", "vite/client"], + "types": ["node"], "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, @@ -13,8 +12,6 @@ "noUnusedParameters": true, "verbatimModuleSyntax": true, "allowImportingTsExtensions": true, - "allowJs": true, - "checkJs": true, "erasableSyntaxOnly": true, "resolveJsonModule": true, "noEmit": true, @@ -23,9 +20,6 @@ "include": [ "tooling/**/*.ts", "tests/**/*.ts", - "tests/**/*.tsx", - "packages/ui/**/*.ts", - "packages/ui/**/*.tsx", "worker/**/*.ts", "*.ts" ] diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 2f6a224..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -import { defineConfig } from "vitest/config"; -export default defineConfig({ - test: { include: ["tests/unit/**/*.test.{ts,tsx}"], environment: "node", restoreMocks: true }, -}); diff --git a/win.slnx b/win.slnx index 54a9ece..5aca624 100644 --- a/win.slnx +++ b/win.slnx @@ -1,8 +1,4 @@ - - - - diff --git a/wrangler.json b/wrangler.json index 3d03917..821dee8 100644 --- a/wrangler.json +++ b/wrangler.json @@ -7,7 +7,7 @@ "workers_dev": false, "preview_urls": false, "assets": { - "directory": "./apps/site/build/client", + "directory": "./artifacts/site", "binding": "ASSETS", "run_worker_first": true, "html_handling": "auto-trailing-slash", From 3209f54a50c2927b1c64160480bcc38430268259 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 00:43:06 +0100 Subject: [PATCH 16/38] [WEB.40] Blazor migration U5: retire the React and npm test toolchain, the npm Contracts packages and the Prettier and Biome gates (U5 final) The C# static Site reproduces the React prerender (docs/web-40-site-parity.md): element skeletons, visible texts, metadata, the stylesheet bytes and the header lines are equal. Only the hydration scripts and the CSP script hashes differ (TB-01). The public pages carry no JavaScript; the live /hello and /cloud-hello controls are retired by the coordinator adjudication of 2026-10-09 (brief section 10), and ArcForges.Web.App remains the only interactive browser application. Removed: apps/site, apps/app, packages/ui, the Vite, Vitest, React Router, Tailwind and Playwright sources, the Node tests and TypeScript tooling that served them, the Prettier, Biome and ArcForges.Web.esproj configuration. docs/web-40-react-retirement.md maps every retired test to its C# or node:test successor. - package.json reduced to wrangler, TypeScript and @types/node (the Worker build and Node tooling). @arcforges/api-client and @arcforges/proto are retired (package-lock.json regenerated with the pinned npm 11.19.0). The sharp override is kept. - tooling/build-identity.ts reads the Contracts axes from the NuGet CON.07 publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1 (sourceCommit ca45f36, verified against the Plan ledger con-07.md) and pins it to the central Directory.Packages.props entry. - eng/naming holds byte copies of the NuGet ArcForges.Contracts.Validation 1.0.0-ci.287.1 naming tools, pinned in eng/policy/naming-candidate.json. - Formatting gate: CI runs dotnet format --verify-no-changes for every C# project. The policy suite enforces the final-newline and no-trailing-whitespace rules (ArchitecturePolicy.Whitespace.cs). The thin TypeScript Worker is type checked by npm run typecheck. - wrangler.json: the assets directory is the C# Site output (./artifacts/site); main stays ./worker/index.js. - Blazor profile size budgets: eng/policy/profile-budgets.json holds the recorded baseline, enforced in CI before the bundle. The immutable cache rule covers fingerprinted /_framework files (the two unfingerprinted loaders are no-cache), in the Site and in the profile bundle _headers. Receipts (each names reviewer w-deku-20261008-rev-web-40): web-40-admission-r10 (dependency policy rebound from the new lock), cloud-dependency-policy-r3, cloud-build-identity-r5 (the tooling digest changed by the pin move), browser-resources-r12 and contracts-publication-r1. Earlier receipts stay as immutable history. Docs: README, AGENTS.md, CONTRIBUTING, development, provenance, licence-boundary, validation, THIRD_PARTY_NOTICES and the profile bundle layout. Historical docs are marked superseded. Local validation is recorded in the WEB.40 U5 unit report. Node 24.20 is local against the 24.21 pin; the pin assertion is recorded as a local environment gap, and hosted CI is authoritative. Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-site-parity.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/web-40-site-parity.md b/docs/web-40-site-parity.md index 527f8e5..447909b 100644 --- a/docs/web-40-site-parity.md +++ b/docs/web-40-site-parity.md @@ -46,11 +46,12 @@ Those differences change rendering, so the C# Site now embeds the React build's their CSP `script-src` listed one SHA-256 for each inline script body (seven hashes in total). The C# pages carry no script element, so the CSP is `script-src 'self'`. Every other CSP directive and every other header line is identical. The public pages contain no JavaScript (TB-01, P2-021 item 2). -- **Interactive controls.** On the React pages the `/hello` input and button, and the `/cloud-hello` check button, were - rendered disabled and enabled only after hydration. The static Site keeps the same disabled initial markup and cannot - enable it. The live greeting and connection checks have no static-page equivalent. Where they move is an open decision - recorded in the WEB.40 unit report; the coordinator must decide between a Blazor route in `ArcForges.Web.App` and - retirement. The public pages do not regress in content. +- **Interactive controls (retired).** On the React pages the `/hello` input and button, and the `/cloud-hello` check + button, were rendered disabled and enabled only after hydration. The static Site keeps the same disabled initial + markup and cannot enable it, so no live control exists on the public pages. The coordinator adjudication of + 2026-10-09 (brief section 10) retires the live greeting and connection checks rather than moving them to a Blazor route + on the Site: no Blazor route is added to the Site, and `ArcForges.Web.App` is the only interactive browser application + (it already carries the Hello probe). The public pages do not regress in content. - **Output files not published.** The React build also emitted `__spa-fallback.html`, which the candidate never published. It has no C# counterpart. From c6c6a120fbad3f8cbceae180e0bc61077db87fb9 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 01:29:21 +0100 Subject: [PATCH 17/38] [WEB.40] U5 validation fixes: C# editorconfig indent, root npm workspace rule, tooling test visibility and the formatting gate (U5, fix 1) Found by the CI-equivalent run of U5 (dotnet format, restore, build, test and policy): - .editorconfig: the repository-wide indent_size = 2 (the React-era setting) applied to the C# sources, which use the standard 4-space indent. A [*.cs] section sets indent_size and tab_width to 4. The CRLF line endings the formatter reported are normalised (end_of_line = lf) and only indentation changed in the formatter's output (PolicyBaseline.cs, NuGetClosureAdmissionTests.cs). - ArchitecturePolicy: the root package.json may omit the workspaces field. The React workspaces are retired, so the root is the only npm project. A non-array workspaces value is still refused, and the workspace inventory stays exact (negative fixture workspace-root-workspaces-not-array). - ArcForges.Web.Tooling.csproj: InternalsVisibleTo ArcForges.Web.Tooling.Tests, so the archive tests reach the internal TarArchive writer. Co-Authored-By: Claude Haiku 5.5 --- .editorconfig | 4 +++ .../Architecture/ArchitecturePolicy.cs | 7 ++-- .../Fixtures/PolicyBaseline.cs | 32 +++++++++---------- .../Fixtures/PolicyCases.cs | 1 + .../NuGetClosureAdmissionTests.cs | 22 ++++++------- .../ArcForges.Web.Tooling.csproj | 3 ++ 6 files changed, 40 insertions(+), 29 deletions(-) diff --git a/.editorconfig b/.editorconfig index 8c52ff9..7e851f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,9 @@ indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true +[*.cs] +indent_size = 4 +tab_width = 4 + [*.md] trim_trailing_whitespace = false diff --git a/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs index 134a644..46baf40 100644 --- a/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs +++ b/tests/ArcForges.Web.Policy.Tests/Architecture/ArchitecturePolicy.cs @@ -109,11 +109,14 @@ private static void AuditWorkspace(PolicyContext context) context.Report("workspace", "package.json", "The root package.json is missing or malformed."); return; } - if (root["workspaces"] is not JsonArray workspaceArray) + if (root["workspaces"] is not null and not JsonArray) { - context.Report("workspace", "package.json", "One explicit root workspace array is required."); + context.Report("workspace", "package.json", "The root workspaces field must be an array when present."); return; } + // The root is the only npm project when the field is absent: the React workspaces are retired (WEB.40 U5), and + // the root package.json holds wrangler and the TypeScript Worker build only. + var workspaceArray = root["workspaces"] as JsonArray ?? new JsonArray(); var workspaces = workspaceArray.Select(node => node?.GetValue() ?? string.Empty).ToArray(); var expected = new[] { "package.json" }.Concat(workspaces.Select(name => name + "/package.json")) .Order(StringComparer.Ordinal).ToArray(); diff --git a/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs index 4b7db36..48691d3 100644 --- a/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs +++ b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyBaseline.cs @@ -86,22 +86,22 @@ public static Dictionary Create() { var sources = new Dictionary(StringComparer.Ordinal) { - ["win.slnx"] = Solution, - ["global.json"] = """{"sdk":{"version":"10.0.401","rollForward":"disable","allowPrerelease":false},"test":{"runner":"Microsoft.Testing.Platform"}}""", - ["NuGet.config"] = NuGetConfig, - ["Directory.Build.props"] = BuildProps, - ["Directory.Build.targets"] = "", - ["Directory.Packages.props"] = CentralPackages, - [".node-version"] = "24.21.0\n", - ["package.json"] = RootManifest, - ["package-lock.json"] = RootLock, - ["apps/site/package.json"] = SiteManifest, - ["apps/site/app/root.ts"] = "import type { Message } from \"@arcforges/proto\";\n\ndocument.title = \"Web DOM is allowed\";\n", - ["eng/policy/licence-boundary.json"] = Inventory, - ["src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj"] = UiProject, - ["src/ArcForges.Web.Ui/packages.lock.json"] = UiLock, - ["src/ArcForges.Web.Ui/Greeting.cs"] = "namespace ArcForges.Web.Ui;\n\npublic sealed class Greeting\n{\n}\n", - ["src/ArcForges.Web.Ui/Greeting.razor"] = "

Hello

\n", + ["win.slnx"] = Solution, + ["global.json"] = """{"sdk":{"version":"10.0.401","rollForward":"disable","allowPrerelease":false},"test":{"runner":"Microsoft.Testing.Platform"}}""", + ["NuGet.config"] = NuGetConfig, + ["Directory.Build.props"] = BuildProps, + ["Directory.Build.targets"] = "", + ["Directory.Packages.props"] = CentralPackages, + [".node-version"] = "24.21.0\n", + ["package.json"] = RootManifest, + ["package-lock.json"] = RootLock, + ["apps/site/package.json"] = SiteManifest, + ["apps/site/app/root.ts"] = "import type { Message } from \"@arcforges/proto\";\n\ndocument.title = \"Web DOM is allowed\";\n", + ["eng/policy/licence-boundary.json"] = Inventory, + ["src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj"] = UiProject, + ["src/ArcForges.Web.Ui/packages.lock.json"] = UiLock, + ["src/ArcForges.Web.Ui/Greeting.cs"] = "namespace ArcForges.Web.Ui;\n\npublic sealed class Greeting\n{\n}\n", + ["src/ArcForges.Web.Ui/Greeting.razor"] = "

Hello

\n", }; // A file on disk ends with a line feed; C# raw literals omit the final line break of their content. foreach (var file in sources.Keys.ToArray()) diff --git a/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs index fa6a7db..1b66ac1 100644 --- a/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs +++ b/tests/ArcForges.Web.Policy.Tests/Fixtures/PolicyCases.cs @@ -30,6 +30,7 @@ public static class PolicyCases ["workspace-yarn-lock"] = new("workspace", s => s["yarn.lock"] = "alternate"), ["workspace-nested-manifest"] = new("workspace", s => s["hidden/package.json"] = "{\"name\":\"hidden\"}"), ["workspace-nested-workspaces"] = new("workspace", s => s["apps/site/package.json"] = "{\"name\":\"site\",\"workspaces\":[\"x\"],\"license\":\"AGPL-3.0-only\",\"arcforges\":{\"licenceBoundary\":\"AGPL\"}}"), + ["workspace-root-workspaces-not-array"] = new("workspace", s => PolicyBaseline.EditJson(s, Root, o => o["workspaces"] = System.Text.Json.Nodes.JsonValue.Create("apps/site"))), ["workspace-lock-differs"] = new("workspace", s => PolicyBaseline.EditJson(s, Site, o => o["devDependencies"] = new System.Text.Json.Nodes.JsonObject { ["left-pad"] = "1.3.0" })), ["workspace-restore-without-lock"] = new("workspace", s => PolicyBaseline.Replace(s, "Directory.Build.props", "true", "false")), ["workspace-alternate-nuget-source"] = new("workspace", s => PolicyBaseline.Replace(s, "NuGet.config", "", "")), diff --git a/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs b/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs index 8309b7a..56e4710 100644 --- a/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs +++ b/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs @@ -75,17 +75,17 @@ private static IReadOnlyDictionary RestoredClosure(string root) { var document = JsonNode.Parse(File.ReadAllText(lockPath))!.AsObject(); foreach (var framework in document["dependencies"]!.AsObject()) - foreach (var package in framework.Value!.AsObject()) - { - var entry = package.Value!.AsObject(); - if (entry["type"]!.GetValue() == "Project") - continue; - var key = package.Key.ToLowerInvariant() + "/" + entry["resolved"]!.GetValue(); - var hash = entry["contentHash"]!.GetValue(); - if (closure.TryGetValue(key, out var seen)) - Assert.Equal(seen, hash); - closure[key] = hash; - } + foreach (var package in framework.Value!.AsObject()) + { + var entry = package.Value!.AsObject(); + if (entry["type"]!.GetValue() == "Project") + continue; + var key = package.Key.ToLowerInvariant() + "/" + entry["resolved"]!.GetValue(); + var hash = entry["contentHash"]!.GetValue(); + if (closure.TryGetValue(key, out var seen)) + Assert.Equal(seen, hash); + closure[key] = hash; + } } return closure; } diff --git a/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj b/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj index 00dd82d..abcd92e 100644 --- a/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj +++ b/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj @@ -6,6 +6,9 @@ + + + AGPL AGPL-3.0-only From e1e357170e70cac14b40b659f29d62ea18489bc1 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 01:34:27 +0100 Subject: [PATCH 18/38] [WEB.40] U5 validation fix 2: tooling test visibility moves into source, so the reviewed tooling project file is unchanged (U5, fix 2) The CI-equivalent run found that the InternalsVisibleTo item in ArcForges.Web.Tooling.csproj changed a reviewed dependency input (tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj) of the dependency policy, and the change is not a dependency change. The project file is restored to its reviewed content, and the same visibility is declared by one assembly attribute in Profiles/TarArchive.cs, the file that defines the internal writer and reader. No receipt changes. Co-Authored-By: Claude Haiku 5.5 --- tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj | 3 --- tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj b/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj index abcd92e..00dd82d 100644 --- a/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj +++ b/tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj @@ -6,9 +6,6 @@ - - - AGPL AGPL-3.0-only diff --git a/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs b/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs index e8f4a9b..ea8e13c 100644 --- a/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs +++ b/tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs @@ -1,6 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-only +using System.Runtime.CompilerServices; using System.Text; +// The archive tests reach the internal writer and reader. The attribute is in source, not in the project file, so the +// reviewed project inputs of the dependency policy are unchanged. +[assembly: InternalsVisibleTo("ArcForges.Web.Tooling.Tests")] + namespace ArcForges.Web.Tooling.Profiles; /// From d60a406383493b6b04000b3d52f08b0b5e43bfe8 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 02:10:07 +0100 Subject: [PATCH 19/38] [WEB.40] U5 docs: the public Site pages are static and the Blazor App carries the connection check; the retirement is recorded (U5, docs) - docs/cloud-hello.md: the current behaviour states that /hello and /cloud-hello are static C# pages since U5, that the Blazor App is the only interactive browser application and supplies the connection check through the NuGet client ArcForges.Contracts.PublicApi 1.0.0-ci.287.1, and that the 2026-09-18 browser observation predates the retirement. The npm Browser SDK row is replaced. - docs/web-40-site-parity.md: the interactive-control paragraph records the retirement (brief section 10, 2026-10-09) instead of an open decision. - docs/web-40-react-retirement.md: the coordinator adjudication of 2026-10-09 confirms the retirement. Co-Authored-By: Claude Haiku 5.5 --- docs/cloud-hello.md | 6 +++--- docs/web-40-react-retirement.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/cloud-hello.md b/docs/cloud-hello.md index 07f35bf..8d821c1 100644 --- a/docs/cloud-hello.md +++ b/docs/cloud-hello.md @@ -2,15 +2,15 @@ ## Current Web behavior -`/hello/` remains local. `/cloud-hello/` uses the published Contracts client to send one fixed diagnostic greeting after the user clicks **Check connection**. It sends no user-entered name, account cookie, authorization header or Cloudflare management token. Requests have a ten-second deadline, are cancelled on navigation, and are never automatically retried. Only the expected server response is displayed as success. +Since WEB.40 unit U5 (2026-10-09, brief section 10), the public `/hello/` and `/cloud-hello/` pages are static C# pages. They carry no runtime JavaScript, their former controls are disabled and cannot be enabled, and they make no API request. The Blazor WebAssembly Account and Chat profile (`ArcForges.Web.App`) is the only interactive browser application. Its host supplies the connection check to the shared `/cloud-hello` component, and it uses the NuGet Contracts client `ArcForges.Contracts.PublicApi` 1.0.0-ci.287.1 from the central package pins. The check's rules are unchanged: it sends one fixed diagnostic greeting, with no user-entered name, account cookie, authorization header or Cloudflare management token; requests have a ten-second deadline, are cancelled on navigation and are never automatically retried; only the expected server response is displayed as success. -The Cloud repository deploys the API Worker and Native AOT container independently of Web. A public browser check on 2026-09-18 verified that this page sends no API request on load, then sends exactly one POST after the click and displays `Hello, ArcForges!` from the real service. That observation used Web commit `86d6fded796d060eb064125baca7db690bc65419` and Cloud commit `6554400c04817491fe68d5e6319434034c5dc356`; it is a diagnostic integration result, not acceptance of later product APIs. If the Cloud API route is absent, the static Worker rejects the Hello POST with 405 Method Not Allowed; a GET at that missing path returns 404. The page shows an unavailable response. No mock is deployed and no successful response is generated locally. +The Cloud repository deploys the API Worker and Native AOT container independently of Web. A public browser check on 2026-09-18, made before the public control was retired, verified that this page sends no API request on load, then sends exactly one POST after the click and displays `Hello, ArcForges!` from the real service. That observation used Web commit `86d6fded796d060eb064125baca7db690bc65419` and Cloud commit `6554400c04817491fe68d5e6319434034c5dc356`; it is a diagnostic integration result, not acceptance of later product APIs. If the Cloud API route is absent, the static Worker rejects the Hello POST with 405 Method Not Allowed; a GET at that missing path returns 404. The page shows an unavailable response. No mock is deployed and no successful response is generated locally. ## Fixed integration boundary | Item | Value | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Browser SDK | `@arcforges/api-client` and `@arcforges/proto`, both `1.0.0-ci.287.1` | +| Client package | NuGet `ArcForges.Contracts.PublicApi` `1.0.0-ci.287.1` in the Blazor App (the npm `@arcforges/api-client` and `@arcforges/proto` packages are retired, WEB.40 U5) | | Browser base URL | Same origin, `/api` | | Public method | `POST https://arcforges.com/api/arcforges.hello.v1.HelloService/SayHello` | | Container method after prefix removal | `/arcforges.hello.v1.HelloService/SayHello` | diff --git a/docs/web-40-react-retirement.md b/docs/web-40-react-retirement.md index 6926802..964a475 100644 --- a/docs/web-40-react-retirement.md +++ b/docs/web-40-react-retirement.md @@ -32,7 +32,7 @@ Kept Node tests (run by `npm run test` and `npm run test:dependencies`): `tests/ ## Decisions and remaining work -1. **Interactive public pages (retired by the WEB.40 outcome).** The React `/hello/` and `/cloud-hello/` controls ran after hydration and called the API. WEB.40 outcome item 1 requires these public pages with no runtime JavaScript (TB-01), outcome item 2 limits the Blazor App profile to Account and Chat, and brief section 5 item 14 adds nothing outside the brief. The live greeting and connection controls are therefore retired and the static pages keep their texts. The `/cloud-hello/` API call is refused by the no-automatic-call browser check. Coordinator confirmation is requested in the unit report. +1. **Interactive public pages (retired by the WEB.40 outcome).** The React `/hello/` and `/cloud-hello/` controls ran after hydration and called the API. WEB.40 outcome item 1 requires these public pages with no runtime JavaScript (TB-01), outcome item 2 limits the Blazor App profile to Account and Chat, and brief section 5 item 14 adds nothing outside the brief. The live greeting and connection controls are therefore retired and the static pages keep their texts. The `/cloud-hello/` API call is refused by the no-automatic-call browser check. The coordinator adjudication of 2026-10-09 (brief section 10) confirms retirement: no Blazor route is added to the Site, and `ArcForges.Web.App` remains the only interactive browser application. 2. **Identity source (done in U5).** `tooling/build-identity.ts` reads the Contracts axes from the NuGet publication record `eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1` (CON.07; bound by `contracts-publication-r1`) and pins it against the central `Directory.Packages.props` entry. The npm packages `@arcforges/api-client` and `@arcforges/proto` are retired. The naming scanner moves with them: `eng/naming` is a byte copy of the `tools/naming` assets of the NuGet package `ArcForges.Contracts.Validation` 1.0.0-ci.287.1, with its digests pinned in `eng/policy/naming-candidate.json`. 3. **Formatting gates (done in U5).** Prettier and Biome are retired. CI runs `dotnet format --verify-no-changes` for each C# project in the `csharp` job. The policy suite enforces one final newline and no trailing whitespace over the audited C#, MSBuild, npm, lock and Worker inputs (`ArchitecturePolicy.Whitespace.cs`), and `npm run typecheck` types the thin TypeScript Worker. 4. **Bundle refusal tests and the browser identity port (ported).** See the table. From c002eea9bb9ca8325a73fe16c51d558d4b4d3f09 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 02:28:04 +0100 Subject: [PATCH 20/38] [WEB.40] Review fixes: the source job builds the C# tool before the candidate tests, and the Account and Chat suites assert the keyboard focus order (U6, review fixes) - ci.yml: the Linux source job restores and builds tools/ArcForges.Web.Tooling (locked, Release) with the pinned SDK before npm run check, so tests/provenance/csharp-candidate.test.ts never depends on stale bin/ output. - csharp-candidate.test.ts: a missing tool build fails at load with the build command. - FocusOrder: bUnit tab-sequence, reading-order, positive-tabindex and default-button helpers. - Account: anonymous, signed-in and failed-read states have their exact tab sequence; Sign out is a native type=button. - Chat: the tab sequence is Name then Send, Cancel only while pending; Enter in the name field submits through the form's default Send button; the transcript is the polite live region. This is the successor of the retired keyboard greeting case. Co-Authored-By: Claude Haiku 5.5 --- .github/workflows/ci.yml | 13 ++++ .../AccountPageTests.cs | 46 +++++++++++++ .../ArcForges.Web.App.Tests/ChatPageTests.cs | 49 ++++++++++++++ tests/ArcForges.Web.App.Tests/FocusOrder.cs | 67 +++++++++++++++++++ tests/provenance/csharp-candidate.test.ts | 5 ++ 5 files changed, 180 insertions(+) create mode 100644 tests/ArcForges.Web.App.Tests/FocusOrder.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29722a5..4cb1a9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,19 @@ jobs: cache: ${{ matrix.os == 'ubuntu-latest' && 'npm' || '' }} package-manager-cache: false - run: npm ci --ignore-scripts + # The candidate tests (tests/provenance/csharp-candidate.test.ts) run the built C# tool. Its Release build is produced + # here from the locked restore, so this job never depends on stale bin/ output from an earlier local run. + - name: Set up the pinned .NET SDK for the candidate tests + if: runner.os == 'Linux' + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6 + with: + dotnet-version: "10.0.401" + - name: Build the C# tool for the candidate tests + if: runner.os == 'Linux' + run: | + set -euo pipefail + dotnet restore tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj --locked-mode + dotnet build tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj --no-restore -c Release - run: npm run check if: runner.os == 'Linux' - name: Set up the IDE project evaluation toolchain diff --git a/tests/ArcForges.Web.App.Tests/AccountPageTests.cs b/tests/ArcForges.Web.App.Tests/AccountPageTests.cs index 0ad2f50..e07b436 100644 --- a/tests/ArcForges.Web.App.Tests/AccountPageTests.cs +++ b/tests/ArcForges.Web.App.Tests/AccountPageTests.cs @@ -227,6 +227,52 @@ public async Task LeavingThePageCancelsTheReadThatIsStillInFlight() Assert.True(observed.IsCancellationRequested); } + [Fact] + public void AnAnonymousVisitorHasNoControlToTabTo() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AnonymousJson))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Contains("You are not signed in.", cut.Markup, StringComparison.Ordinal)); + Assert.Empty(FocusOrder.TabSequence(cut)); + FocusOrder.AssertNoPositiveTabIndex(cut); + } + + [Fact] + public void ASignedInVisitorTabsToSignOutAfterTheSessionStatusAndSignOutIsANativeButton() + { + var server = ScriptedServer.Always(() => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Sign out"))); + // Reading order: the live status region holds the session facts, then the one control. + Assert.Equal(new[] { "status", "button" }, FocusOrder.ReadingOrder(cut)); + Assert.Equal(new[] { "button:Sign out" }, FocusOrder.TabSequence(cut)); + // A native button gives Enter and Space activation; type="button" keeps it out of any form submission. + var signOut = Button(cut, "Sign out"); + Assert.Equal("button", signOut.LocalName); + Assert.Equal("button", signOut.GetAttribute("type")); + FocusOrder.AssertNoPositiveTabIndex(cut); + } + + [Fact] + public void AFailedReadAnnouncesItsAlertBeforeTheOnlyControlTryAgain() + { + var server = ScriptedServer.Always(() => Responses.Text("down", HttpStatusCode.ServiceUnavailable)); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Equal("The server is unavailable. Try again later.", cut.Find("[role=alert]").TextContent)); + Assert.Equal(new[] { "status", "alert", "button" }, FocusOrder.ReadingOrder(cut)); + Assert.Equal(new[] { "button:Try again" }, FocusOrder.TabSequence(cut)); + FocusOrder.AssertNoPositiveTabIndex(cut); + } + internal static async Task WaitUntilAsync(Func condition) { for (var attempt = 0; attempt < 500 && !condition(); attempt++) diff --git a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs index d9fce88..098a47e 100644 --- a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs +++ b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs @@ -148,6 +148,55 @@ public async Task LeavingTheChatPageCancelsTheMessageThatIsStillPending() Assert.True(observed.IsCancellationRequested); } + [Fact] + public void ChatTabsFromTheNameFieldToSendAndReachesCancelOnlyWhileAMessageIsPending() + { + var server = new ScriptedServer(async (_, token) => + { + await Task.Delay(Timeout.Infinite, token); + return Greeting("unused"); + }); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + Assert.Equal(new[] { "input:Name", "button:Send" }, FocusOrder.TabSequence(cut)); + FocusOrder.AssertNoPositiveTabIndex(cut); + + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); + // Send is disabled while the message is pending, so the reachable control is Cancel. + Assert.Equal(new[] { "input:Name", "button:Cancel" }, FocusOrder.TabSequence(cut)); + FocusOrder.AssertNoPositiveTabIndex(cut); + + CancelButton(cut)!.Click(); + cut.WaitForAssertion(() => Assert.Contains("The request was cancelled.", cut.Markup, StringComparison.Ordinal)); + Assert.Equal(new[] { "input:Name", "button:Send" }, FocusOrder.TabSequence(cut)); + } + + [Fact] + public void EnterInTheNameFieldSendsThroughTheFormsDefaultButtonAndTheReplyIsAnnouncedInTheTranscript() + { + // The keyboard path of the retired Hello greeting: Enter in the name field submits the form, and the reply is + // announced by the live transcript. Enter activates the form's default button, the first submit button in the form. + var server = ScriptedServer.Always(() => Responses.Raw(ProbeFixtures.HelloReply("Hello, ArcForges!"), "application/grpc-web+proto")); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + var form = cut.Find("form"); + Assert.NotNull(form.QuerySelector("input")); + Assert.Equal("Send", FocusOrder.DefaultButtonOf(form)); + + var transcript = cut.Find("ol.transcript"); + Assert.Equal("polite", transcript.GetAttribute("aria-live")); + Assert.Equal("Messages", transcript.GetAttribute("aria-label")); + + cut.Find("form").Submit(); + cut.WaitForAssertion(() => Assert.Contains("Hello, ArcForges!", cut.Find("ol.transcript").TextContent, StringComparison.Ordinal)); + Assert.Equal(1, server.Count); + } + [Fact] public void TheTransportFailureIsUnavailableAndItIsAFixedNotice() { diff --git a/tests/ArcForges.Web.App.Tests/FocusOrder.cs b/tests/ArcForges.Web.App.Tests/FocusOrder.cs new file mode 100644 index 0000000..1ca88d2 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/FocusOrder.cs @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Focus-order and keyboard assertions of the interactive pages (the CI accessibility gate, brief section 6 decision 3). +// bUnit renders the markup but does not move focus, so the keyboard sequence is read from the rendered document: the +// controls a keyboard user can reach, in document order, skipping disabled controls and tabindex -1. Native controls +// (button, a, input) give Enter and Space activation for free, so the tests also pin the element kind and the form's +// default button, which decides what Enter does in a text field. +using System.Globalization; +using AngleSharp.Dom; +using Microsoft.AspNetCore.Components; +using Bunit; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +internal static class FocusOrder +{ + private const string Reachable = "a[href], button, input, select, textarea, [tabindex]"; + + /// The keyboard tab sequence: each reachable control as "tag:name", in document order. + internal static IReadOnlyList TabSequence(IRenderedComponent rendered) + where TComponent : IComponent => + rendered.FindAll(Reachable) + .Where(element => element.GetAttribute("tabindex") != "-1" && !element.HasAttribute("disabled")) + .Select(Describe) + .ToList(); + + /// The document order of the live regions and the controls, for reading-order checks. + internal static IReadOnlyList ReadingOrder(IRenderedComponent rendered) + where TComponent : IComponent => + rendered.FindAll("[role=status], [role=alert], button, input") + .Select(element => element.GetAttribute("role") ?? element.LocalName) + .ToList(); + + /// No element may take a positive tabindex, which would reorder the page away from document order. + internal static void AssertNoPositiveTabIndex(IRenderedComponent rendered) + where TComponent : IComponent + { + foreach (var element in rendered.FindAll("[tabindex]")) + { + var value = element.GetAttribute("tabindex")!; + Assert.True( + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var index) && index <= 0, + $"A positive tabindex reorders the keyboard sequence: {element.OuterHtml}"); + } + } + + /// The first submit button of a form is its default button: Enter in a text field activates it. + internal static string DefaultButtonOf(IElement form) + { + var button = form.QuerySelectorAll("button") + .First(candidate => (candidate.GetAttribute("type") ?? "submit") == "submit"); + return NameOf(button); + } + + private static string Describe(IElement element) => $"{element.LocalName}:{NameOf(element)}"; + + private static string NameOf(IElement element) + { + if (element.LocalName == "input") + { + var label = element.Closest("label")?.TextContent.Trim(); + return element.GetAttribute("aria-label") ?? (string.IsNullOrEmpty(label) ? "(unnamed)" : label); + } + + return element.TextContent.Trim(); + } +} diff --git a/tests/provenance/csharp-candidate.test.ts b/tests/provenance/csharp-candidate.test.ts index 05f1951..caea0dc 100644 --- a/tests/provenance/csharp-candidate.test.ts +++ b/tests/provenance/csharp-candidate.test.ts @@ -13,6 +13,11 @@ import { expectedIdentity } from "../../tooling/build-identity.ts"; import { toolLibrary } from "../../tooling/candidate.ts"; import { root, workerScript } from "../../tooling/project.ts"; +// A missing tool build fails here with the build command, not later with a spawn error for every test. +await access(toolLibrary).catch(() => { + throw new Error(`The C# tool is not built at ${toolLibrary}. Build it first (see the header of this file).`); +}); + const sha = (bytes: Uint8Array | string) => createHash("sha256").update(bytes).digest("hex"); const scratch = await mkdtemp(path.join(tmpdir(), "arcforges-candidate-")); const identityRecord = expectedIdentity("0.1.0-local"); From 1f8f4a7eb2f37595092719e370fbf777e19856a3 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 02:31:26 +0100 Subject: [PATCH 21/38] [WEB.40] Successor admission receipt web-40-admission-r11 for the CI source-job change, and the classified focus-order helper (U6, review fixes) - eng/policy/dependency-reviews/web-40-admission-r11.json: the successor of web-40-admission-r10 (its bytes are unchanged). The only input change is .github/workflows/ci.yml, whose digest is computed by the repository's tooling/dependency-policy.ts digest(). The reviewer field names w-deku-20261008-rev-web-40, the pre-assigned independent reviewer. - eng/policy/dependency-policy.json: reviewRecord, the ci.yml input digest and the review object follow the successor. - eng/provenance/files.json: classifies the successor receipt and tests/ArcForges.Web.App.Tests/FocusOrder.cs. - docs/web-40-react-retirement.md: the Account and Chat row records the focus-order successor of the retired keyboard case. Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-react-retirement.md | 2 +- eng/policy/dependency-policy.json | 10 +- .../web-40-admission-r11.json | 768 ++++++++++++++++++ eng/provenance/files.json | 2 + 4 files changed, 776 insertions(+), 6 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r11.json diff --git a/docs/web-40-react-retirement.md b/docs/web-40-react-retirement.md index 964a475..96ef3f4 100644 --- a/docs/web-40-react-retirement.md +++ b/docs/web-40-react-retirement.md @@ -12,7 +12,7 @@ Parity of the public output is recorded in [web-40-site-parity.md](web-40-site-p | `tests/unit/app-hello.test.ts` (10) | `tests/ArcForges.Web.App.Tests` probe and greeting cases (commit be0d916) | Ported | | `tests/unit/app-profiles.test.ts` (9) | `tests/ArcForges.Web.App.Tests` and `tests/ArcForges.Web.Ui.Tests` `ProfileIdentityTests` (commit be0d916) | Ported | | `tests/unit/app-proof.test.ts` (10) | `tests/ArcForges.Web.Tooling.Tests/ProfileBudgetTests.cs` (budget parse, the ten percent limit, the measure). The interaction ceilings are re-baseline-pending (owner PRF.11). | Budget cases ported. **Remaining:** the Blazor proof structural gates (PRF.11) and the interaction budgets | -| `tests/unit/app-routes.test.tsx` (14) | `tests/ArcForges.Web.App.Tests` Account and Chat bUnit cases (commit be0d916) | Ported | +| `tests/unit/app-routes.test.tsx` (14) | `tests/ArcForges.Web.App.Tests` Account and Chat bUnit cases (commit be0d916). The keyboard and focus order of both pages is asserted by `FocusOrder.cs` (the tab sequence of each state, the reading order, no positive tabindex, the native Sign out button, the Name field before Send, Cancel only while pending, and Enter in the name field submitting through the default Send button, with the transcript as the polite live region). That is the successor of the retired keyboard greeting case in `tests/browser/site.spec.ts` | Ported; focus order asserted in bUnit (review fix). Focus placement after Send is disabled or Sign out is removed is not asserted (see decisions) | | `tests/unit/app-session.test.ts` (13) | `tests/ArcForges.Web.App.Tests` session and CSRF cases (commit be0d916) | Ported | | `tests/unit/architecture-policy.test.ts` (5) | `tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs` (commit 80900a1) | Ported | | `tests/unit/naming-policy.test.ts` (1) | `tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs` (published scanner, every forbidden term) | Ported | diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 02911b3..a5e3d7d 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -6,7 +6,7 @@ "package.json" ], "inputs": { - ".github/workflows/ci.yml": "2546927b2bf8075d90182cf38cb7c0db53dc8e183ee1a571f0d88f33c5098fb8", + ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", @@ -49,10 +49,10 @@ "review": { "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", - "date": "2026-10-08", - "rationale": "WEB.40 successor of web-40-admission-r9 for unit U5, the React removal. apps/site, apps/app and packages/ui are removed with their Vite, React Router, Tailwind and Playwright tooling. The root package.json keeps only wrangler and the TypeScript Worker toolchain (typescript, @types/node). The Contracts npm packages @arcforges/api-client and @arcforges/proto are retired: the Contracts identity is the NuGet ArcForges.Contracts.PublicApi 1.0.0-ci.287.1 publication record (eng/contracts, CON.07), and the naming authority is a byte copy of the NuGet ArcForges.Contracts.Validation 1.0.0-ci.287.1 tools/naming assets (eng/naming, digests pinned in eng/policy/naming-candidate.json). The npm closure is 114 registry entries; the surviving entries are unchanged from the lock of web-40-admission-r9 and the prerelease admissions are the exact versions already reviewed (miniflare, unenv, youch).", + "date": "2026-10-09", + "rationale": "WEB.40 successor of web-40-admission-r10 for the independent review fix of 2026-10-09 (the Linux source job). The only input change is .github/workflows/ci.yml: the source job gains a Linux-only step that restores and builds tools/ArcForges.Web.Tooling (locked, Release) with the pinned .NET 10.0.401 SDK before npm run check, so the candidate tests never run against stale bin/ output. No package, lock, version, owner, class or runtime input changes; every other input hash is unchanged from web-40-admission-r10. The U5 closure rationale of web-40-admission-r10 still applies: WEB.40 successor of web-40-admission-r9 for unit U5, the React removal. apps/site, apps/app and packages/ui are removed with their Vite, React Router, Tailwind and Playwright tooling. The root package.json keeps only wrangler and the TypeScript Worker toolchain (typescript, @types/node). The Contracts npm packages @arcforges/api-client and @arcforges/proto are retired: the Contracts identity is the NuGet ArcForges.Contracts.PublicApi 1.0.0-ci.287.1 publication record (eng/contracts, CON.07), and the naming authority is a byte copy of the NuGet ArcForges.Contracts.Validation 1.0.0-ci.287.1 tools/naming assets (eng/naming, digests pinned in eng/policy/naming-candidate.json). The npm closure is 114 registry entries; the surviving entries are unchanged from the lock of web-40-admission-r9 and the prerelease admissions are the exact versions already reviewed (miniflare, unenv, youch).", "inputs": { - ".github/workflows/ci.yml": "2546927b2bf8075d90182cf38cb7c0db53dc8e183ee1a571f0d88f33c5098fb8", + ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", @@ -811,7 +811,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r10.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r11.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", diff --git a/eng/policy/dependency-reviews/web-40-admission-r11.json b/eng/policy/dependency-reviews/web-40-admission-r11.json new file mode 100644 index 0000000..c75ddd2 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r11.json @@ -0,0 +1,768 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-09", + "rationale": "WEB.40 successor of web-40-admission-r10 for the independent review fix of 2026-10-09 (the Linux source job). The only input change is .github/workflows/ci.yml: the source job gains a Linux-only step that restores and builds tools/ArcForges.Web.Tooling (locked, Release) with the pinned .NET 10.0.401 SDK before npm run check, so the candidate tests never run against stale bin/ output. No package, lock, version, owner, class or runtime input changes; every other input hash is unchanged from web-40-admission-r10. The U5 closure rationale of web-40-admission-r10 still applies: WEB.40 successor of web-40-admission-r9 for unit U5, the React removal. apps/site, apps/app and packages/ui are removed with their Vite, React Router, Tailwind and Playwright tooling. The root package.json keeps only wrangler and the TypeScript Worker toolchain (typescript, @types/node). The Contracts npm packages @arcforges/api-client and @arcforges/proto are retired: the Contracts identity is the NuGet ArcForges.Contracts.PublicApi 1.0.0-ci.287.1 publication record (eng/contracts, CON.07), and the naming authority is a byte copy of the NuGet ArcForges.Contracts.Validation 1.0.0-ci.287.1 tools/naming assets (eng/naming, digests pinned in eng/policy/naming-candidate.json). The npm closure is 114 registry entries; the surviving entries are unchanged from the lock of web-40-admission-r9 and the prerelease admissions are the exact versions already reviewed (miniflare, unenv, youch).", + "inputs": { + ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "eng/provenance/records/browser-resources-r12.json": "6b470604b64d692d0eaa215be133506af065856dde0fde9ee8e702bba883bb88", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "392b8874131b713ac07ab595ff541122fa3c2317f9ba81b34ffe6115f4363825", + "package.json": "60b8b8e853ae2ac0dfec58831d61c717b3e4fb26ed3848745b36e9a2ec370ea5", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "6ae92b481b6a892a9b4952f7e4b8da56b7caac8f6a6206151b017108e2c99229", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "75bd9b2ebbe6ed307c134473be0319fd536a7733ce00fe1237148299e5f59239", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "61d7150f179a77464386591f7a8c38cdbadb95b9c29fff18fe2ef912bef91931", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "ac5bab6c098c95abb7b91c56821d12c3f8e8d6a109d854d7a8cbaba86a8e0823", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "c79f3d29ae3c94cd9e1478cf16514e5223689d47fd6252c6e8aafdee8cc57075", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "c55404228f3ef546808c86c4cfac706e59f5be0b0a8ccb76a944e92cf2220d46", + "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", + "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "af03c905cf03a946129d821708c500812e630358f09ba7ba59cec72b658b3ff8", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "52409f3ea5cd0db52a8fa1320d097d573ddc15543421d9f00b29f7aa6d3207a8", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", + "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site; the deploy job ships the same sealed bytes under the unchanged main-push guards. No hosted browser, device, macOS or live-service check is added.", + "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r9.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r10.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/eng/provenance/files.json b/eng/provenance/files.json index a4ceabb..898124f 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -54,6 +54,7 @@ "eng/policy/dependency-reviews/source-map-js-admission-r1.json", "eng/policy/dependency-reviews/web-40-admission-r1.json", "eng/policy/dependency-reviews/web-40-admission-r10.json", + "eng/policy/dependency-reviews/web-40-admission-r11.json", "eng/policy/dependency-reviews/web-40-admission-r2.json", "eng/policy/dependency-reviews/web-40-admission-r3.json", "eng/policy/dependency-reviews/web-40-admission-r4.json", @@ -181,6 +182,7 @@ "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj", "tests/ArcForges.Web.App.Tests/ChatPageTests.cs", "tests/ArcForges.Web.App.Tests/ExactUnsignedTests.cs", + "tests/ArcForges.Web.App.Tests/FocusOrder.cs", "tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs", "tests/ArcForges.Web.App.Tests/HelloProbeTests.cs", "tests/ArcForges.Web.App.Tests/ProfileIdentityTests.cs", From d436642e2b1be7afade44a8a75bfeda5d2266e1e Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 02:32:51 +0100 Subject: [PATCH 22/38] [WEB.40] Record the open focus-placement gap after Send and Sign out in the retirement decisions (U6, review fixes) bUnit asserts the keyboard sequence but does not move browser focus. The retirement record now states the two focus losses that remain (a focused Send button disabled while pending, and a removed Sign out button after the session ends) as a recorded decision for the coordinator. Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-react-retirement.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/web-40-react-retirement.md b/docs/web-40-react-retirement.md index 96ef3f4..e2c2eab 100644 --- a/docs/web-40-react-retirement.md +++ b/docs/web-40-react-retirement.md @@ -39,3 +39,4 @@ Kept Node tests (run by `npm run test` and `npm run test:dependencies`): `tests/ 5. **Coordination (remaining).** CLOUD.71 and CLOUD.85 bundle-naming note (see [profile-bundle.md](profile-bundle.md)). 6. **Proof gates (owned by PRF.11, not WEB.40 U5).** The Blazor proof structural gates and the interaction budget re-baseline stay with PRF.11, as brief section 10 records. 7. **Local gaps (recorded, not run).** The local Node is 24.20 against the 24.21 pin, so `npm run policy` stops at its Node assertion locally; the rest of that script is covered by the hosted CI job. The Linux locked restore is authoritative on hosted ubuntu CI; a WSL2 run uses an unlocked restore because WSL2 has only SDK 10.0.400. +8. **Focus placement (remaining, recorded).** The bUnit suites assert the keyboard sequence and the native controls, but bUnit does not move browser focus. Two states still lose focus in a browser: a Send button that was focused becomes disabled while its message is pending, and the Sign out button is removed once the session has ended. Restoring focus needs a reviewed decision, because the framework's focus call is a JavaScript interop point that the brief limits to the audited set (section 1, P2-021 item 2). Owner: the WEB.40 coordinator; a local opt-in browser check would assert the placement. From 109ba2fa0f5f62f420c11e4535c1d355d644a03b Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 02:58:15 +0100 Subject: [PATCH 23/38] [WEB.40] Keep the focused Send, Sign out and Try again controls in the document, with an AX-02 focus record and browser check (U6, review fix) Send, Sign out and Try again could lose keyboard focus: a focused control that became disabled or was removed dropped focus to the body (AX-02). Send was disabled while a message was pending, Sign out was disabled while signing out and removed once the session ended, and Try again was removed when its read began. - Chat: Send is aria-disabled while a message is pending, not disabled. The existing send guard refuses a second send, so no message is sent twice. - Account: Sign out and Try again are one action element for the visit. Its label and aria-disabled state change; the element is never removed, so focus stays on it through the sign-out, a failed sign-out and a retry. A click on an aria-disabled control is refused by the handler. - Both pages refresh their state when an operation starts, so the disabled state is rendered while the operation runs. - app.css: aria-disabled buttons look disabled, so the visual state matches the refused action. - bUnit (CI): the controls stay present and reachable at each step, and refused actions do nothing. bUnit re-parses markup on every render, so it cannot prove element identity; that is asserted by the local opt-in LocalFocusBrowserTests (Chromium, keyboard focus identity, same-origin answers fixed by routes). - ArcForges.Web.Browser.Tests: ARCFORGES_CHROMIUM_PATH selects an installed Chromium for the focus check only. Local-only; never built in CI. - docs/web-40-ax-02-focus.md records each control state, the checks and the run result. docs/web-40-react-retirement.md item 8 is updated; the Cancel focus drop in Chat stays open for the coordinator (a persistent aria-disabled Cancel or a combined Send/Cancel control). - eng/provenance/files.json classifies the new record and browser test. Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-ax-02-focus.md | 55 ++++++ docs/web-40-react-retirement.md | 4 +- eng/provenance/files.json | 2 + src/ArcForges.Web.App/Pages/Account.razor | 68 ++++++- src/ArcForges.Web.App/Pages/Chat.razor | 8 +- src/ArcForges.Web.App/wwwroot/app.css | 7 + .../AccountPageTests.cs | 84 +++++++++ .../ArcForges.Web.App.Tests/ChatPageTests.cs | 54 +++++- .../LocalFocusBrowserTests.cs | 170 ++++++++++++++++++ .../ArcForges.Web.Browser.Tests/LocalOptIn.cs | 3 + 10 files changed, 442 insertions(+), 13 deletions(-) create mode 100644 docs/web-40-ax-02-focus.md create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs diff --git a/docs/web-40-ax-02-focus.md b/docs/web-40-ax-02-focus.md new file mode 100644 index 0000000..7d4e73c --- /dev/null +++ b/docs/web-40-ax-02-focus.md @@ -0,0 +1,55 @@ +# WEB.40 AX-02 focus record: Account and Chat + +Status: recorded under WEB.40 for the review fix of 2026-10-09 (focus placement, retirement item 8 of [web-40-react-retirement.md](web-40-react-retirement.md)). + +Requirement: Design `docs/requirements/12-quality-and-compatibility-contract.md`, AX-02 (line 184): "focus is always visible; focus order is logical; ... after an operation, focus returns to a sensible origin". P2-021 item 8 makes this a precondition for retiring each old UI. + +## Why this record exists + +A focused control that becomes `disabled` or is removed from the document drops keyboard focus to the body. bUnit does not move browser focus, and the opt-in axe check does not detect focus loss, so neither proves AX-02. The page therefore never disables or removes a control that has keyboard focus. A control that cannot act is marked `aria-disabled="true"` and refuses the action in its handler. Moving focus to another element would need a new JavaScript interop call, and the brief limits interop to the audited points (P2-021 item 2), so no focus call is used. + +## Control states + +| Screen | State | Control | Element and focus | +| --- | --- | --- | --- | +| Chat | idle | Send (`type=submit`) | Enabled. Enter in the Name field submits through it (the form's default button). | +| Chat | pending | Send | Same element, `aria-disabled="true"`, label "Sending…". A click or Enter is refused by the send guard, so no second message is sent. Focus stays on Send. | +| Chat | pending | Cancel | Added while pending and removed when the pending message ends. See remaining item 1. | +| Chat | after reply or notice | Send | Same element, `aria-disabled="false"`. Focus stays on Send. | +| Account | anonymous first read | none | No control, so the tab sequence is empty. | +| Account | signed in | Sign out | Enabled. | +| Account | sign-out in flight | Sign out | Same element, `aria-disabled="true"`, label "Signing out…". A click is refused, so no second sign-out is posted. Focus stays on it. | +| Account | session ended by sign-out | Try again | The same element, relabelled and enabled. Focus stays on it. | +| Account | sign-out refused or unconfirmed | Try again | The same element, relabelled and enabled. Focus stays on it. | +| Account | read failed | Try again | Enabled. A click makes the same element `aria-disabled="true"` while the read runs. | +| Account | read answers signed in | Sign out | The same element, relabelled and enabled. | + +The Account action is one element for the whole visit, created when the page first shows a session or a failure. Its label and `aria-disabled` state change; the element is never removed. + +## Checks + +bUnit (the CI accessibility gate, brief section 6 decision 3). bUnit re-parses the markup on every render, so these prove that the control is present and reachable at each step, the tab sequence and the refused actions. They do not prove element identity: + +- `tests/ArcForges.Web.App.Tests/ChatPageTests.cs`: `SendStaysPresentAndFocusableThroughAPendingMessage`, `APendingSendIgnoresAClickAndEnterSoItCannotSendTwice`, `ChatTabsFromTheNameFieldToSendAndReachesCancelOnlyWhileAMessageIsPending`. +- `tests/ArcForges.Web.App.Tests/AccountPageTests.cs`: `SignOutStaysPresentAndFocusableWhileWorkingAndAfterTheSessionEnds`, `AFailedSignOutKeepsTheControlAndOffersTryAgainInIt`, `TryAgainStaysPresentAndDisabledWhileItsReadRunsAndBecomesSignOut`. + +Browser (local opt-in only; never CI, see `tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs`). It drives Chromium with the keyboard and asserts `document.activeElement` is the same element before, during and after each operation. The same-origin answers are fixed by Playwright routes: + +- `ChatKeepsFocusOnSendWhileAMessageIsPendingAndAfterTheReply` +- `AccountKeepsFocusOnSignOutWhileSigningOutAndAfterTheSessionEnds` + +Run: set `ARCFORGES_LOCAL_BROWSER=1` and `ARCFORGES_BROWSER_BASE_URL` to a served build of the Account and Chat profiles, then run `dotnet test tests/browser/ArcForges.Web.Browser.Tests`. Result on the recording machine: see the record at the end of this file. + +## Remaining (recorded, not fixed here) + +1. **Cancel in Chat.** Cancel is removed when the pending message ends. A keyboard user who presses Cancel loses focus to the body. The fix needs a decision from the coordinator, because the two options change visible UI: (a) keep Cancel in place, `aria-disabled` and `tabindex="-1"` while idle, so it is visible but not in the tab order; or (b) show one Send/Cancel control whose label changes. Owner: the WEB.40 coordinator. +2. **WCAG 2.2 AA verification per replacement screen.** P2-021 item 8 also requires a recorded WCAG 2.2 AA verification for Account and Chat. This record covers focus only (AX-02). Owner: the WEB.40 coordinator. +3. **Browser run on the recording machine.** The browser check runs only with the local opt-in. It is not part of CI (P2-017), and hosted CI does not run it. + +## Record of the local browser run + +Recorded on the WEB.40 worker machine, 2026-10-09, against the Release publish of the Account and Chat profiles on this branch (`dotnet publish src/ArcForges.Web.App -c Release`), served on a local origin with single-page fallback: + +- `LocalFocusBrowserTests`: 2 passed (`ChatKeepsFocusOnSendWhileAMessageIsPendingAndAfterTheReply`, `AccountKeepsFocusOnSignOutWhileSigningOutAndAfterTheSessionEnds`). The Chromium build was the installed one, named by `ARCFORGES_CHROMIUM_PATH`. +- The other browser tests in the project (`LocalBrowserTests`, `LocalSiteBrowserTests`, `LocalBuildIdentityBrowserTests`) did not launch on this machine: the Playwright package expects Chromium build 1234, which is not installed (Chromium 1243 is). That is an environment gap, not a result of this change, and it is recorded here rather than worked around. +- A negative control (the pre-fix `disabled` markup in the browser) was not run. The check fails on any lost focus by identity, so it cannot pass with a disabled control, but the control itself was not executed. diff --git a/docs/web-40-react-retirement.md b/docs/web-40-react-retirement.md index e2c2eab..87595a5 100644 --- a/docs/web-40-react-retirement.md +++ b/docs/web-40-react-retirement.md @@ -12,7 +12,7 @@ Parity of the public output is recorded in [web-40-site-parity.md](web-40-site-p | `tests/unit/app-hello.test.ts` (10) | `tests/ArcForges.Web.App.Tests` probe and greeting cases (commit be0d916) | Ported | | `tests/unit/app-profiles.test.ts` (9) | `tests/ArcForges.Web.App.Tests` and `tests/ArcForges.Web.Ui.Tests` `ProfileIdentityTests` (commit be0d916) | Ported | | `tests/unit/app-proof.test.ts` (10) | `tests/ArcForges.Web.Tooling.Tests/ProfileBudgetTests.cs` (budget parse, the ten percent limit, the measure). The interaction ceilings are re-baseline-pending (owner PRF.11). | Budget cases ported. **Remaining:** the Blazor proof structural gates (PRF.11) and the interaction budgets | -| `tests/unit/app-routes.test.tsx` (14) | `tests/ArcForges.Web.App.Tests` Account and Chat bUnit cases (commit be0d916). The keyboard and focus order of both pages is asserted by `FocusOrder.cs` (the tab sequence of each state, the reading order, no positive tabindex, the native Sign out button, the Name field before Send, Cancel only while pending, and Enter in the name field submitting through the default Send button, with the transcript as the polite live region). That is the successor of the retired keyboard greeting case in `tests/browser/site.spec.ts` | Ported; focus order asserted in bUnit (review fix). Focus placement after Send is disabled or Sign out is removed is not asserted (see decisions) | +| `tests/unit/app-routes.test.tsx` (14) | `tests/ArcForges.Web.App.Tests` Account and Chat bUnit cases (commit be0d916). The keyboard and focus order of both pages is asserted by `FocusOrder.cs` (the tab sequence of each state, the reading order, no positive tabindex, the native Sign out button, the Name field before Send, Cancel only while pending, and Enter in the name field submitting through the default Send button, with the transcript as the polite live region). That is the successor of the retired keyboard greeting case in `tests/browser/site.spec.ts` | Ported; focus order asserted in bUnit. Focus placement: Send, Sign out and Try again keep their control in the document (aria-disabled, never disabled or removed), recorded in [web-40-ax-02-focus.md](web-40-ax-02-focus.md) and checked in the local opt-in `LocalFocusBrowserTests`. Cancel in Chat is still open (decision 8) | | `tests/unit/app-session.test.ts` (13) | `tests/ArcForges.Web.App.Tests` session and CSRF cases (commit be0d916) | Ported | | `tests/unit/architecture-policy.test.ts` (5) | `tests/ArcForges.Web.Policy.Tests/ArchitecturePolicyTests.cs` (commit 80900a1) | Ported | | `tests/unit/naming-policy.test.ts` (1) | `tests/ArcForges.Web.Policy.Tests/NamingScannerTests.cs` (published scanner, every forbidden term) | Ported | @@ -39,4 +39,4 @@ Kept Node tests (run by `npm run test` and `npm run test:dependencies`): `tests/ 5. **Coordination (remaining).** CLOUD.71 and CLOUD.85 bundle-naming note (see [profile-bundle.md](profile-bundle.md)). 6. **Proof gates (owned by PRF.11, not WEB.40 U5).** The Blazor proof structural gates and the interaction budget re-baseline stay with PRF.11, as brief section 10 records. 7. **Local gaps (recorded, not run).** The local Node is 24.20 against the 24.21 pin, so `npm run policy` stops at its Node assertion locally; the rest of that script is covered by the hosted CI job. The Linux locked restore is authoritative on hosted ubuntu CI; a WSL2 run uses an unlocked restore because WSL2 has only SDK 10.0.400. -8. **Focus placement (remaining, recorded).** The bUnit suites assert the keyboard sequence and the native controls, but bUnit does not move browser focus. Two states still lose focus in a browser: a Send button that was focused becomes disabled while its message is pending, and the Sign out button is removed once the session has ended. Restoring focus needs a reviewed decision, because the framework's focus call is a JavaScript interop point that the brief limits to the audited set (section 1, P2-021 item 2). Owner: the WEB.40 coordinator; a local opt-in browser check would assert the placement. +8. **Focus placement (fixed in part, recorded).** A focused control that becomes disabled or is removed drops keyboard focus to the body (AX-02). The page no longer does that for its operation controls: Send (Chat), and Sign out and Try again (Account) stay in the document. A control that cannot act is `aria-disabled="true"`, and its handler refuses the action. No focus call is used, so no new JavaScript interop is added (P2-021 item 2). The bUnit suites assert that the control stays present and reachable at each step, because bUnit re-parses the markup on every render. Element identity and browser focus are asserted by the local opt-in `LocalFocusBrowserTests`, which is never run in CI. Still open: the Cancel control in Chat is removed when its message ends, so a keyboard user who presses it loses focus. Choosing between a persistent `aria-disabled` Cancel and one combined Send/Cancel control is a coordinator decision. The per-screen record is [web-40-ax-02-focus.md](web-40-ax-02-focus.md). Owner: the WEB.40 coordinator. diff --git a/eng/provenance/files.json b/eng/provenance/files.json index 898124f..17e0532 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -40,6 +40,7 @@ "docs/provenance.md", "docs/validation-policy.md", "docs/validation.md", + "docs/web-40-ax-02-focus.md", "docs/web-40-react-retirement.md", "docs/web-40-site-parity.md", "eng/policy/architecture.md", @@ -239,6 +240,7 @@ "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj", "tests/browser/ArcForges.Web.Browser.Tests/LocalBrowserTests.cs", "tests/browser/ArcForges.Web.Browser.Tests/LocalBuildIdentityBrowserTests.cs", + "tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs", "tests/browser/ArcForges.Web.Browser.Tests/LocalOptIn.cs", "tests/browser/ArcForges.Web.Browser.Tests/LocalOptInTests.cs", "tests/browser/ArcForges.Web.Browser.Tests/LocalSiteBrowserTests.cs", diff --git a/src/ArcForges.Web.App/Pages/Account.razor b/src/ArcForges.Web.App/Pages/Account.razor index 144de90..a281314 100644 --- a/src/ArcForges.Web.App/Pages/Account.razor +++ b/src/ArcForges.Web.App/Pages/Account.razor @@ -48,13 +48,11 @@ }
- @if (_session is AuthenticatedSession signedIn) + @* One action element for the whole visit: its label and aria-disabled state change, but the element is not removed, + so keyboard focus on it is never dropped by the page (AX-02). See docs/web-40-ax-02-focus.md. *@ + @if (_action != ActionKind.None) { - - } - @if (_phase == Phase.Failed) - { - + }
@@ -68,24 +66,63 @@ Ended, } + /// + /// The one action the page offers: Sign out once a session is shown, Try again after a failed or ended read. None until + /// the page has shown a session or a failure, so an anonymous first visit has no control. + /// + private enum ActionKind + { + None, + SignOut, + TryAgain, + } + private Phase _phase = Phase.Loading; private SessionSnapshot? _session; private FailureKind _failure; private bool _working; private bool _disposed; + private ActionKind _action = ActionKind.None; private CancellationTokenSource? _active; + private string ActionLabel => _action == ActionKind.SignOut + ? (_working ? "Signing out…" : "Sign out") + : "Try again"; + + /// A control that cannot act is aria-disabled and keeps its place: a disabled control drops keyboard focus. + private bool ActionDisabled => _action == ActionKind.SignOut + ? _working || _session is not AuthenticatedSession + : _phase == Phase.Loading; + /// The session is read once when the page opens. protected override async Task OnInitializedAsync() { await CheckAsync(); } + /// Runs the offered action. A click on an aria-disabled control does nothing. + private async Task OnActionAsync() + { + if (ActionDisabled) + { + return; + } + if (_action == ActionKind.SignOut && _session is AuthenticatedSession signedIn) + { + await SignOutAsync(signedIn.CsrfToken); + } + else + { + await CheckAsync(); + } + } + /// Reads the session again. A new read replaces a read that is still in flight. private async Task CheckAsync() { _phase = Phase.Loading; _session = null; + Refresh(); SessionSnapshot? session = null; var completed = await RunAsync(async token => { @@ -96,12 +133,14 @@ _session = session; _phase = Phase.Ready; } + SettleAction(); Refresh(); } private async Task SignOutAsync(string csrfToken) { _working = true; + Refresh(); var completed = await RunAsync(token => Session.EndSessionAsync(csrfToken, token)); _working = false; if (completed) @@ -109,9 +148,26 @@ _session = null; _phase = Phase.Ended; } + SettleAction(); Refresh(); } + /// + /// Chooses the action after an operation. A signed-in session offers Sign out. Once an action has been offered, a result + /// without a session offers Try again in the same element; a failure with no earlier action offers Try again too. + /// + private void SettleAction() + { + if (_session is AuthenticatedSession) + { + _action = ActionKind.SignOut; + } + else if (_action != ActionKind.None || _phase == Phase.Failed) + { + _action = ActionKind.TryAgain; + } + } + /// /// Runs one call and reports whether it completed. A cancelled call changes nothing. A typed failure shows the page's /// failure state; an ended session is reported as ended, never as a transient fault. Any other exception is an diff --git a/src/ArcForges.Web.App/Pages/Chat.razor b/src/ArcForges.Web.App/Pages/Chat.razor index 69cfe65..b8f28e1 100644 --- a/src/ArcForges.Web.App/Pages/Chat.razor +++ b/src/ArcForges.Web.App/Pages/Chat.razor @@ -15,7 +15,7 @@ - + @if (_pending) { @@ -50,7 +50,10 @@ } } - /// Sends one greeting. A second send while one is pending does nothing. + /// + /// Sends one greeting. A second send while one is pending does nothing. Send is aria-disabled, not disabled, while a + /// message is pending: a disabled control drops keyboard focus, and this guard is what stops a second send. + /// private async Task SendAsync() { if (_active is not null) @@ -61,6 +64,7 @@ _active = controller; _pending = true; Add("you", _name); + StateHasChanged(); try { var reply = await Hello.SayHelloAsync(_name, controller.Token); diff --git a/src/ArcForges.Web.App/wwwroot/app.css b/src/ArcForges.Web.App/wwwroot/app.css index 92ff8a0..2430f21 100644 --- a/src/ArcForges.Web.App/wwwroot/app.css +++ b/src/ArcForges.Web.App/wwwroot/app.css @@ -157,6 +157,13 @@ h1 em { .button:focus-visible { outline-offset: 4px; } +/* aria-disabled keeps the control focusable (AX-02); it looks disabled and its action is refused by the page. */ +.button[aria-disabled="true"], +.button[aria-disabled="true"]:hover { + background: #6b7a72; + border-color: #6b7a72; + cursor: not-allowed; +} .hello-art { position: relative; display: grid; diff --git a/tests/ArcForges.Web.App.Tests/AccountPageTests.cs b/tests/ArcForges.Web.App.Tests/AccountPageTests.cs index e07b436..270eaa6 100644 --- a/tests/ArcForges.Web.App.Tests/AccountPageTests.cs +++ b/tests/ArcForges.Web.App.Tests/AccountPageTests.cs @@ -273,6 +273,90 @@ public void AFailedReadAnnouncesItsAlertBeforeTheOnlyControlTryAgain() FocusOrder.AssertNoPositiveTabIndex(cut); } + [Fact] + public async Task SignOutStaysPresentAndFocusableWhileWorkingAndAfterTheSessionEnds() + { + // Focus on Sign out must survive the sign-out (AX-02): the control is aria-disabled while working, never removed. + // Element identity is asserted in a real browser (LocalFocusBrowserTests); bUnit re-parses the markup on each render. + var logout = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new ScriptedServer((request, _) => request.Url.AbsolutePath.EndsWith("/logout", StringComparison.Ordinal) + ? logout.Task + : Task.FromResult(Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson)))); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Sign out"))); + Button(cut, "Sign out").Click(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Signing out…"))); + Assert.Single(cut.FindAll("button")); + Assert.Equal("true", Button(cut, "Signing out…").GetAttribute("aria-disabled")); + Assert.False(Button(cut, "Signing out…").HasAttribute("disabled")); + Assert.Equal(new[] { "button:Signing out…" }, FocusOrder.TabSequence(cut)); + + // A click on the refused control does not post a second sign-out. + await WaitUntilAsync(() => server.Count == 2); + Button(cut, "Signing out…").Click(); + await Task.Delay(50, Xunit.TestContext.Current.CancellationToken); + Assert.Equal(2, server.Count); + + logout.SetResult(Responses.Json(ProbeFixtures.ReceiptBody("happened"))); + cut.WaitForAssertion(() => Assert.Contains(Ended, cut.Markup, StringComparison.Ordinal)); + // The ended state keeps the one control, now offering Try again: a fresh read is the only action left. + Assert.Single(cut.FindAll("button")); + Assert.Equal("false", Button(cut, "Try again").GetAttribute("aria-disabled")); + Assert.Equal(new[] { "button:Try again" }, FocusOrder.TabSequence(cut)); + } + + [Fact] + public void AFailedSignOutKeepsTheControlAndOffersTryAgainInIt() + { + var answers = new Queue>( + [ + () => Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson)), + () => Responses.Text("{}", HttpStatusCode.Forbidden), + ]); + using var context = new BunitContext(); + RegisterProbes(context, ScriptedServer.Always(() => answers.Dequeue()())); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.True(HasButton(cut, "Sign out"))); + Button(cut, "Sign out").Click(); + cut.WaitForAssertion(() => Assert.Equal("The server refused this request.", cut.Find("[role=alert]").TextContent)); + Assert.Single(cut.FindAll("button")); + Assert.Equal("false", Button(cut, "Try again").GetAttribute("aria-disabled")); + Assert.Equal(new[] { "button:Try again" }, FocusOrder.TabSequence(cut)); + } + + [Fact] + public async Task TryAgainStaysPresentAndDisabledWhileItsReadRunsAndBecomesSignOut() + { + var read = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var calls = 0; + var server = new ScriptedServer((_, _) => + { + calls++; + return calls == 1 + ? Task.FromResult(Responses.Text("down", HttpStatusCode.ServiceUnavailable)) + : read.Task; + }); + using var context = new BunitContext(); + RegisterProbes(context, server); + + var cut = context.Render(); + cut.WaitForAssertion(() => Assert.Equal("The server is unavailable. Try again later.", cut.Find("[role=alert]").TextContent)); + Button(cut, "Try again").Click(); + await WaitUntilAsync(() => server.Count == 2); + cut.WaitForAssertion(() => Assert.Equal("true", Button(cut, "Try again").GetAttribute("aria-disabled"))); + Assert.Single(cut.FindAll("button")); + Assert.Equal(new[] { "button:Try again" }, FocusOrder.TabSequence(cut)); + + read.SetResult(Responses.Json(Encoding.UTF8.GetBytes(ProbeFixtures.AuthenticatedJson))); + cut.WaitForAssertion(() => Assert.Contains("Ada Lovelace", cut.Markup, StringComparison.Ordinal)); + Assert.Single(cut.FindAll("button")); + Assert.Equal("false", Button(cut, "Sign out").GetAttribute("aria-disabled")); + } + internal static async Task WaitUntilAsync(Func condition) { for (var attempt = 0; attempt < 500 && !condition(); attempt++) diff --git a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs index 098a47e..b824aca 100644 --- a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs +++ b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs @@ -90,7 +90,7 @@ public void ChatCanCancelAPendingMessageReportsItAsCancelledAndDoesNotDoubleSend var cut = context.Render(); SendButton(cut).Click(); cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); - Assert.True(SendButton(cut).HasAttribute("disabled")); + Assert.Equal("true", SendButton(cut).GetAttribute("aria-disabled")); // A second submit while one is pending does not send again. cut.Find("form").Submit(); Assert.Equal(1, server.Count); @@ -98,7 +98,55 @@ public void ChatCanCancelAPendingMessageReportsItAsCancelledAndDoesNotDoubleSend CancelButton(cut)!.Click(); cut.WaitForAssertion(() => Assert.Contains("The request was cancelled.", cut.Markup, StringComparison.Ordinal)); Assert.Null(CancelButton(cut)); + Assert.Equal("false", SendButton(cut).GetAttribute("aria-disabled")); + } + + [Fact] + public void SendStaysPresentAndFocusableThroughAPendingMessage() + { + // Focus on Send survives a send: the control is never disabled or removed (AX-02). bUnit re-parses the markup on + // every render, so element identity is asserted in a real browser (tests/browser LocalFocusBrowserTests); here the + // control must be present and reachable at each step. + var answer = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var server = new ScriptedServer((_, _) => answer.Task); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + Assert.Equal("false", SendButton(cut).GetAttribute("aria-disabled")); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); + Assert.Single(cut.FindAll("button[type=submit]")); Assert.False(SendButton(cut).HasAttribute("disabled")); + Assert.Equal("true", SendButton(cut).GetAttribute("aria-disabled")); + Assert.Equal(new[] { "input:Name", "button:Sending…", "button:Cancel" }, FocusOrder.TabSequence(cut)); + + answer.SetResult(Greeting("ArcForges")); + cut.WaitForAssertion(() => Assert.Contains("Hello, ArcForges!", cut.Markup, StringComparison.Ordinal)); + Assert.Single(cut.FindAll("button[type=submit]")); + Assert.Equal("false", SendButton(cut).GetAttribute("aria-disabled")); + Assert.Equal(new[] { "input:Name", "button:Send" }, FocusOrder.TabSequence(cut)); + } + + [Fact] + public void APendingSendIgnoresAClickAndEnterSoItCannotSendTwice() + { + var server = new ScriptedServer(async (_, token) => + { + await Task.Delay(Timeout.Infinite, token); + return Greeting("unused"); + }); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render(); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); + // The control stays focusable, so a click on it reaches the handler, which refuses the second send. + SendButton(cut).Click(); + cut.Find("form").Submit(); + Assert.Equal(1, server.Count); + Assert.Single(cut.FindAll("li")); } [Fact] @@ -165,8 +213,8 @@ public void ChatTabsFromTheNameFieldToSendAndReachesCancelOnlyWhileAMessageIsPen SendButton(cut).Click(); cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); - // Send is disabled while the message is pending, so the reachable control is Cancel. - Assert.Equal(new[] { "input:Name", "button:Cancel" }, FocusOrder.TabSequence(cut)); + // Send stays reachable while the message is pending (aria-disabled, not disabled); Cancel joins it. + Assert.Equal(new[] { "input:Name", "button:Sending…", "button:Cancel" }, FocusOrder.TabSequence(cut)); FocusOrder.AssertNoPositiveTabIndex(cut); CancelButton(cut)!.Click(); diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs new file mode 100644 index 0000000..5b261ac --- /dev/null +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Local opt-in AX-02 focus check (P2-021 item 8; docs/web-40-ax-02-focus.md). bUnit does not move browser focus and axe does +// not detect focus loss, so this check drives a real page with the keyboard and asserts that the same element keeps focus +// through each operation. The same-origin answers are fixed by Playwright routes, so no live service is needed. It skips +// unless LocalOptIn is enabled; CI never builds this project. +using System.Text; +using Microsoft.Playwright; +using Xunit; + +namespace ArcForges.Web.Browser.Tests; + +public sealed class LocalFocusBrowserTests +{ + private const string HelloPath = "**/api/arcforges.hello.v1.HelloService/SayHello"; + private const string BootstrapPath = "**/session/v1/bootstrap"; + private const string LogoutPath = "**/session/v1/logout"; + private const string Ended = "You are signed out. Reload to check again."; + + // The same wire fixtures as the bUnit suite (tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs), in browser form. + private const string AuthenticatedJson = + "{\"csrfToken\":\"cccccccccccccccccccccccc\",\"authenticated\":true," + + "\"session\":{\"sessionId\":\"6d1d4c2a-62a0-4b86-9d3f-0c6a3d0b5c11\",\"expiresAt\":\"2026-10-03T08:00:00.000000Z\"," + + "\"idleExpiresAt\":\"2026-10-02T20:30:00.000000Z\",\"userId\":\"0f0e6a30-5d1c-4c7e-8b53-5b6b7f9a4a10\"," + + "\"deviceId\":\"8f2a1d77-1c1b-4b0a-9a55-2f1d0e5c7b21\",\"workspaceIds\":[\"3f1d3b1e-2a8c-4c44-a1c8-77a1e8f0b9a1\"," + + "\"a2b84f55-0c2d-4b0d-8e6a-5d6e44a6c7d3\"],\"recoveryGeneration\":\"18446744073709551615\",\"purpose\":\"authenticate\"}," + + "\"profile\":{\"displayName\":\"Ada Lovelace\",\"locale\":\"en-GB\",\"timezone\":\"Europe/London\"," + + "\"revision\":\"9007199254740993\"}}"; + + private const string ReceiptJson = + "{\"commandId\":\"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\"effect\":\"happened\"}"; + + /// One gRPC-Web frame: a flag byte, a big-endian 32-bit length and the payload. + private static byte[] Frame(byte flag, byte[] payload) + { + var frame = new byte[payload.Length + 5]; + frame[0] = flag; + frame[1] = (byte)(payload.Length >> 24); + frame[2] = (byte)(payload.Length >> 16); + frame[3] = (byte)(payload.Length >> 8); + frame[4] = (byte)payload.Length; + payload.CopyTo(frame, 5); + return frame; + } + + /// A binary gRPC-Web hello answer: the SayHelloResponse message (field 1) and an OK trailer. + private static byte[] HelloReply(string message) + { + var text = Encoding.UTF8.GetBytes(message); + var payload = new byte[text.Length + 2]; + payload[0] = 0x0A; + payload[1] = (byte)text.Length; + text.CopyTo(payload, 2); + var trailer = Encoding.UTF8.GetBytes("grpc-status: 0\r\n"); + var data = Frame(0x00, payload); + var end = Frame(0x80, trailer); + var reply = new byte[data.Length + end.Length]; + data.CopyTo(reply, 0); + end.CopyTo(reply, data.Length); + return reply; + } + + /// True when the element is the one holding keyboard focus right now (identity, not a look-alike). + private static Task HasFocusAsync(IPage page, IElementHandle element) => + page.EvaluateAsync("el => el === document.activeElement", element); + + /// + /// Launches the browser the check drives. ARCFORGES_CHROMIUM_PATH, when set, names an installed Chromium for a machine whose + /// Playwright browser build differs from the package's; otherwise the Playwright-managed build is used. + /// + private static Task LaunchAsync(IPlaywright playwright, IReadOnlyDictionary environment) + { + var executable = environment.GetValueOrDefault(LocalOptIn.ChromiumPathVariable); + return playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + ExecutablePath = string.IsNullOrWhiteSpace(executable) ? null : executable, + }); + } + + [Fact] + public async Task ChatKeepsFocusOnSendWhileAMessageIsPendingAndAfterTheReply() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await LaunchAsync(playwright, environment); + var page = await browser.NewPageAsync(); + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await page.RouteAsync(HelloPath, async route => + { + // Hold the answer so that the pending state is observable, then answer with a real gRPC-Web reply. + await release.Task; + await route.FulfillAsync(new RouteFulfillOptions + { + Status = 200, + ContentType = "application/grpc-web+proto", + BodyBytes = HelloReply("Hello, ArcForges!"), + }); + }); + + await page.GotoAsync(new Uri(baseUrl, "/chat").ToString()); + var sendLocator = page.Locator("form button[type=submit]"); + await sendLocator.FocusAsync(); + var send = await sendLocator.ElementHandleAsync() + ?? throw new InvalidOperationException("The Chat page has no Send button."); + Assert.True(await HasFocusAsync(page, send), "Send did not take keyboard focus."); + + await page.Keyboard.PressAsync("Enter"); + await page.WaitForSelectorAsync("form button[type=submit][aria-disabled=true]"); + // Pending: Send is aria-disabled, not disabled, so it keeps keyboard focus (AX-02). + Assert.True(await HasFocusAsync(page, send), "Focus left Send while the message was pending."); + Assert.False(await page.EvaluateAsync("el => el.disabled", send)); + + release.SetResult(); + await page.WaitForSelectorAsync("ol.transcript li:nth-child(2)"); + Assert.True(await HasFocusAsync(page, send), "Focus left Send after the reply."); + Assert.Equal("false", await page.EvaluateAsync("el => el.getAttribute('aria-disabled')", send)); + } + + [Fact] + public async Task AccountKeepsFocusOnSignOutWhileSigningOutAndAfterTheSessionEnds() + { + var environment = LocalOptIn.Current(); + if (!LocalOptIn.IsEnabled(environment)) + Assert.Skip("Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1 and ARCFORGES_BROWSER_BASE_URL on a developer machine; never on CI."); + var baseUrl = new Uri(environment[LocalOptIn.BaseUrlVariable]!); + + using var playwright = await Playwright.CreateAsync(); + await using var browser = await LaunchAsync(playwright, environment); + var page = await browser.NewPageAsync(); + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await page.RouteAsync(BootstrapPath, route => route.FulfillAsync(new RouteFulfillOptions + { + Status = 200, + ContentType = "application/json", + Body = AuthenticatedJson, + })); + await page.RouteAsync(LogoutPath, async route => + { + await release.Task; + await route.FulfillAsync(new RouteFulfillOptions + { + Status = 200, + ContentType = "application/json", + Body = ReceiptJson, + }); + }); + + await page.GotoAsync(new Uri(baseUrl, "/account").ToString()); + var signOutLocator = page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "Sign out" }); + await signOutLocator.WaitForAsync(); + await signOutLocator.FocusAsync(); + var element = await signOutLocator.ElementHandleAsync() + ?? throw new InvalidOperationException("The Account page has no Sign out button."); + Assert.True(await HasFocusAsync(page, element), "Sign out did not take keyboard focus."); + + await page.Keyboard.PressAsync("Enter"); + await page.WaitForSelectorAsync("button[aria-disabled=true]"); + // Working: Sign out is aria-disabled and keeps focus instead of being disabled (AX-02). + Assert.True(await HasFocusAsync(page, element), "Focus left Sign out while the sign-out was in flight."); + + release.SetResult(); + await page.WaitForSelectorAsync($"text={Ended}"); + // Ended: the same element stays and offers the one remaining action, so focus is not dropped to the body. + Assert.True(await HasFocusAsync(page, element), "Focus was dropped when the session ended."); + Assert.Equal("Try again", (await element.TextContentAsync())?.Trim()); + } +} diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalOptIn.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalOptIn.cs index 6ebfe32..e3a7f40 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/LocalOptIn.cs +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalOptIn.cs @@ -15,6 +15,9 @@ public static class LocalOptIn /// The environment variable that names a local axe-core script, injected only into the page under test. public const string AxeScriptVariable = "ARCFORGES_AXE_CORE_PATH"; + /// The environment variable that names an installed Chromium executable, for a machine whose Playwright build differs. + public const string ChromiumPathVariable = "ARCFORGES_CHROMIUM_PATH"; + /// True only for a deliberate local run with a base URL: never on CI. public static bool IsEnabled(IReadOnlyDictionary environment) { From d25adcd2454928ff38018fea97df17e14b67a685 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 03:15:53 +0100 Subject: [PATCH 24/38] [WEB.40] Record the byte parity gate result against the React prerender: byte parity is not shown after removing the hydration nodes, and a coordinator waiver is needed (review fix, not complete) Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-site-parity.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/web-40-site-parity.md b/docs/web-40-site-parity.md index 447909b..7667c27 100644 --- a/docs/web-40-site-parity.md +++ b/docs/web-40-site-parity.md @@ -60,3 +60,15 @@ Those differences change rendering, so the C# Site now embeds the React build's After this record, `apps/site`, `apps/app` and `packages/ui` may be removed. The C# Site output is the only public output. Byte parity with the React output is not claimed for the HTML pages, because their markup differs by the hydration nodes listed above. Content parity is claimed for every public page as described. + +## Byte parity gate (review fix, 2026-10-09) + +Brief section 10 says: keep apps/site until byte parity is shown, then remove it. This section records what the comparison shows against the React prerender in `artifacts/parity/react` (local output, not committed) and the C# output in `artifacts/parity/csharp2` (site build at this branch). + +Measured on `index.html`, `hello/index.html` and `cloud-hello/index.html`: + +- Removed hydration nodes (the five or six inline scripts, the React Router context, the `` and `` markers and the `modulepreload` links) account for the largest part of the difference. Their removal is the TB-01 change. +- Serialization differs after those nodes are removed, so the files are not byte equal. The remaining differences are: void-element syntax (`` against ``), `charSet` against `charset`, boolean attribute spelling (`disabled=""` against `disabled`), attribute casing (`autoComplete` against `autocomplete`), the em dash in `` (`—` against the literal character), the space before `/>` on the stylesheet and description links, and the `<title>` text encoding of the React file. +- `404.html`, `404.css`, `robots.txt` and `favicon.svg` are byte identical. + +Result: byte parity is not shown for the HTML pages. The content and element-skeleton parity recorded above still holds. Closing the byte gate needs either a recorded coordinator waiver or a serializer change that makes the C# output match React's serializer, which is not a hydration removal and is outside this normalisation. No waiver is recorded in this file. The apps/site deletion is open to the coordinator until one of the two is recorded. From 33c57dff2fd38183bc10590dc03377a1bb9744a8 Mon Sep 17 00:00:00 2001 From: sammiller <dekueon@gmail.com> Date: Fri, 9 Oct 2026 03:46:01 +0100 Subject: [PATCH 25/38] [WEB.40] Persistent aria-disabled Chat Cancel (option a) with bUnit tests, the normalised-DOM site parity gate, the local axe accessibility suite (Deque.AxeCore.Playwright) and successor admission web-40-admission-r12 with policy and provenance updates (U6, captured as found; full local validation and the accessibility/parity runs not yet passed) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- Directory.Packages.props | 2 + docs/licence-boundary.md | 7 + docs/web-40-ax-02-focus.md | 8 +- docs/web-40-react-retirement.md | 2 +- eng/policy/dependency-policy.json | 57 +- .../web-40-admission-r12.json | 797 ++++++++++++++++++ eng/provenance/files.json | 5 + src/ArcForges.Web.App/Pages/Chat.razor | 13 +- .../ArcForges.Web.App.Tests/ChatPageTests.cs | 61 +- .../NuGetAdmission.cs | 10 +- .../NuGetClosureAdmissionTests.cs | 45 + .../ArcForges.Web.Site.Tests.csproj | 1 + .../SiteParity/HtmlNormaliser.cs | 171 ++++ .../SiteParityTests.cs | 227 +++++ .../packages.lock.json | 6 + .../ArcForges.Web.Browser.Tests.csproj | 1 + .../LocalAccessibilityBrowserTests.cs | 316 +++++++ .../LocalFocusBrowserTests.cs | 12 +- .../packages.lock.json | 29 + 19 files changed, 1736 insertions(+), 34 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r12.json create mode 100644 tests/ArcForges.Web.Site.Tests/SiteParity/HtmlNormaliser.cs create mode 100644 tests/ArcForges.Web.Site.Tests/SiteParityTests.cs create mode 100644 tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 7dbcb6d..a01c7eb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,8 +5,10 @@ <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> </PropertyGroup> <ItemGroup> + <PackageVersion Include="AngleSharp" Version="1.7.0" /> <PackageVersion Include="ArcForges.Build.Policy" Version="1.0.0-ci.94.1" /> <PackageVersion Include="ArcForges.Contracts.PublicApi" Version="1.0.0-ci.287.1" /> + <PackageVersion Include="Deque.AxeCore.Playwright" Version="4.13.0" /> <PackageVersion Include="Grpc.Net.Client" Version="2.84.0" /> <PackageVersion Include="Grpc.Net.Client.Web" Version="2.84.0" /> <PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.10" /> diff --git a/docs/licence-boundary.md b/docs/licence-boundary.md index 84db80c..ea00088 100644 --- a/docs/licence-boundary.md +++ b/docs/licence-boundary.md @@ -20,6 +20,13 @@ targets reject wrong effective values before build/pack. Source and evaluated reports include the exact commit, dirty state, inventory and findings under `artifacts/evidence/licence-*.json`; CI uploads the reports. +The local opt-in accessibility re-proof (WEB.40 U6, `tests/browser`, P2-021 item 8) admits four test-only NuGet packages through the +dependency policy: Deque.AxeCore.Playwright 4.13.0 (MIT) and its dependencies Newtonsoft.Json 13.0.1 (MIT) and +System.IO.Abstractions 17.0.24 (MIT), and Deque.AxeCore.Commons 4.13.0, which carries the axe-core script under MPL-2.0. The +MPL-2.0 row is admitted only with `testOnly: true`; no product project restores it, it is never shipped or compiled into a product +output, and the policy suite fails if a product project references a testOnly row. The axe-core script is injected only into the +page under test. The AngleSharp 1.7.0 parser (MIT) is a direct reference of the Site parity test, which is also test-only. + The npm inventory covers the root manifest only (wrangler, TypeScript and `@types/node`). The former first-party Contracts npm packages `@arcforges/proto` and `@arcforges/api-client` are retired (WEB.40 unit U5); the Contracts naming and identity bytes are recorded by `contracts-publication-r1`. The React workspaces and the JavaScript IDE adapter are retired (WEB.40 unit U5). Windows CI evaluates the owned C# projects with .NET SDK 10.0.401. The final static candidate retains its licence/provenance checks. Browser and public-download verification are removed from CI under [validation policy](validation-policy.md). These source/build-policy results do not establish product functionality or close diff --git a/docs/web-40-ax-02-focus.md b/docs/web-40-ax-02-focus.md index 7d4e73c..6746039 100644 --- a/docs/web-40-ax-02-focus.md +++ b/docs/web-40-ax-02-focus.md @@ -14,7 +14,9 @@ A focused control that becomes `disabled` or is removed from the document drops | --- | --- | --- | --- | | Chat | idle | Send (`type=submit`) | Enabled. Enter in the Name field submits through it (the form's default button). | | Chat | pending | Send | Same element, `aria-disabled="true"`, label "Sending…". A click or Enter is refused by the send guard, so no second message is sent. Focus stays on Send. | -| Chat | pending | Cancel | Added while pending and removed when the pending message ends. See remaining item 1. | +| Chat | idle | Cancel (`type=button`) | Always rendered, so it is never removed: `aria-disabled="true"` and `tabindex="-1"`. It is not a tab stop, and a click is refused by the handler (option a of the 2026-10-09 adjudication). | +| Chat | pending | Cancel | Same element, `aria-disabled="false"` and no tabindex, so it joins the tab order after Send. A click cancels the pending message. | +| Chat | after the message ends | Cancel | Same element, back to idle (`aria-disabled="true"`, `tabindex="-1"`). Focus that was on Cancel stays on it, because an aria-disabled control is never removed. | | Chat | after reply or notice | Send | Same element, `aria-disabled="false"`. Focus stays on Send. | | Account | anonymous first read | none | No control, so the tab sequence is empty. | | Account | signed in | Sign out | Enabled. | @@ -42,8 +44,8 @@ Run: set `ARCFORGES_LOCAL_BROWSER=1` and `ARCFORGES_BROWSER_BASE_URL` to a serve ## Remaining (recorded, not fixed here) -1. **Cancel in Chat.** Cancel is removed when the pending message ends. A keyboard user who presses Cancel loses focus to the body. The fix needs a decision from the coordinator, because the two options change visible UI: (a) keep Cancel in place, `aria-disabled` and `tabindex="-1"` while idle, so it is visible but not in the tab order; or (b) show one Send/Cancel control whose label changes. Owner: the WEB.40 coordinator. -2. **WCAG 2.2 AA verification per replacement screen.** P2-021 item 8 also requires a recorded WCAG 2.2 AA verification for Account and Chat. This record covers focus only (AX-02). Owner: the WEB.40 coordinator. +1. **Cancel in Chat (resolved, option a).** Cancel is always rendered. While idle it is `aria-disabled="true"` and `tabindex="-1"`, so it is never a tab stop and never removed, and focus cannot fall to the body when a message ends. The bUnit tests `CancelStaysInTheDocumentWhileIdleAndAClickOnItIsRefused` and `CancelBecomesTabbableWhileAMessageIsPendingAndReturnsToIdleWithoutBeingRemoved` assert the states; the browser focus identity is asserted by `LocalFocusBrowserTests` for Send and Sign out, and the Cancel states are part of the axe re-proof in [web-40-accessibility.md](web-40-accessibility.md). Decision: 2026-10-09 coordinator adjudication (brief section 10); option (b), a combined Send/Cancel control, was not chosen. +2. **WCAG 2.2 AA verification per replacement screen.** P2-021 item 8 also requires a recorded WCAG 2.2 AA verification for Account, Chat and the Site pages. That record is [web-40-accessibility.md](web-40-accessibility.md). This record covers focus only (AX-02). 3. **Browser run on the recording machine.** The browser check runs only with the local opt-in. It is not part of CI (P2-017), and hosted CI does not run it. ## Record of the local browser run diff --git a/docs/web-40-react-retirement.md b/docs/web-40-react-retirement.md index 87595a5..6d0f3f2 100644 --- a/docs/web-40-react-retirement.md +++ b/docs/web-40-react-retirement.md @@ -39,4 +39,4 @@ Kept Node tests (run by `npm run test` and `npm run test:dependencies`): `tests/ 5. **Coordination (remaining).** CLOUD.71 and CLOUD.85 bundle-naming note (see [profile-bundle.md](profile-bundle.md)). 6. **Proof gates (owned by PRF.11, not WEB.40 U5).** The Blazor proof structural gates and the interaction budget re-baseline stay with PRF.11, as brief section 10 records. 7. **Local gaps (recorded, not run).** The local Node is 24.20 against the 24.21 pin, so `npm run policy` stops at its Node assertion locally; the rest of that script is covered by the hosted CI job. The Linux locked restore is authoritative on hosted ubuntu CI; a WSL2 run uses an unlocked restore because WSL2 has only SDK 10.0.400. -8. **Focus placement (fixed in part, recorded).** A focused control that becomes disabled or is removed drops keyboard focus to the body (AX-02). The page no longer does that for its operation controls: Send (Chat), and Sign out and Try again (Account) stay in the document. A control that cannot act is `aria-disabled="true"`, and its handler refuses the action. No focus call is used, so no new JavaScript interop is added (P2-021 item 2). The bUnit suites assert that the control stays present and reachable at each step, because bUnit re-parses the markup on every render. Element identity and browser focus are asserted by the local opt-in `LocalFocusBrowserTests`, which is never run in CI. Still open: the Cancel control in Chat is removed when its message ends, so a keyboard user who presses it loses focus. Choosing between a persistent `aria-disabled` Cancel and one combined Send/Cancel control is a coordinator decision. The per-screen record is [web-40-ax-02-focus.md](web-40-ax-02-focus.md). Owner: the WEB.40 coordinator. +8. **Focus placement (fixed in part, recorded).** A focused control that becomes disabled or is removed drops keyboard focus to the body (AX-02). The page no longer does that for its operation controls: Send (Chat), and Sign out and Try again (Account) stay in the document. A control that cannot act is `aria-disabled="true"`, and its handler refuses the action. No focus call is used, so no new JavaScript interop is added (P2-021 item 2). The bUnit suites assert that the control stays present and reachable at each step, because bUnit re-parses the markup on every render. Element identity and browser focus are asserted by the local opt-in `LocalFocusBrowserTests`, which is never run in CI. Cancel in Chat is resolved by option (a) of the 2026-10-09 adjudication: it is always rendered, `aria-disabled="true"` and `tabindex="-1"` while idle, and it joins the tab order only while a message is pending. The per-screen record is [web-40-ax-02-focus.md](web-40-ax-02-focus.md). The WCAG 2.2 AA re-proof of every replacement screen is [web-40-accessibility.md](web-40-accessibility.md). diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index a5e3d7d..71ad156 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -11,7 +11,7 @@ ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", - "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "Directory.Packages.props": "95e73db26cdb9ba15ccd3c752d804be4ee083f85fdf589acf837121a2c3975b7", "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", @@ -34,14 +34,14 @@ "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "c79f3d29ae3c94cd9e1478cf16514e5223689d47fd6252c6e8aafdee8cc57075", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", - "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", - "tests/ArcForges.Web.Site.Tests/packages.lock.json": "c55404228f3ef546808c86c4cfac706e59f5be0b0a8ccb76a944e92cf2220d46", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "15eb52424732fb8bf0a7479acf0c323b9a36c7e1503c3369bf92f402319b55e0", "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "af03c905cf03a946129d821708c500812e630358f09ba7ba59cec72b658b3ff8", - "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", - "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "52409f3ea5cd0db52a8fa1320d097d573ddc15543421d9f00b29f7aa6d3207a8", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" @@ -50,14 +50,14 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-09", - "rationale": "WEB.40 successor of web-40-admission-r10 for the independent review fix of 2026-10-09 (the Linux source job). The only input change is .github/workflows/ci.yml: the source job gains a Linux-only step that restores and builds tools/ArcForges.Web.Tooling (locked, Release) with the pinned .NET 10.0.401 SDK before npm run check, so the candidate tests never run against stale bin/ output. No package, lock, version, owner, class or runtime input changes; every other input hash is unchanged from web-40-admission-r10. The U5 closure rationale of web-40-admission-r10 still applies: WEB.40 successor of web-40-admission-r9 for unit U5, the React removal. apps/site, apps/app and packages/ui are removed with their Vite, React Router, Tailwind and Playwright tooling. The root package.json keeps only wrangler and the TypeScript Worker toolchain (typescript, @types/node). The Contracts npm packages @arcforges/api-client and @arcforges/proto are retired: the Contracts identity is the NuGet ArcForges.Contracts.PublicApi 1.0.0-ci.287.1 publication record (eng/contracts, CON.07), and the naming authority is a byte copy of the NuGet ArcForges.Contracts.Validation 1.0.0-ci.287.1 tools/naming assets (eng/naming, digests pinned in eng/policy/naming-candidate.json). The npm closure is 114 registry entries; the surviving entries are unchanged from the lock of web-40-admission-r9 and the prerelease admissions are the exact versions already reviewed (miniflare, unenv, youch).", + "rationale": "WEB.40 successor of web-40-admission-r11 for the parity, focus and accessibility re-proof unit U6 (2026-10-09 adjudication). Test-only additions: Deque.AxeCore.Playwright 4.13.0 (MIT) with its dependencies Deque.AxeCore.Commons 4.13.0 (MPL-2.0, the axe-core script carrier, admitted only as testOnly), Newtonsoft.Json 13.0.1 (MIT) and System.IO.Abstractions 17.0.24 (MIT). They are referenced only by tests/browser (local opt-in Microsoft.Playwright suite, never built or restored in CI); axe-core is injected only into the page under test. AngleSharp 1.7.0 (already admitted, MIT) is now also a direct reference of tests/ArcForges.Web.Site.Tests for the normalised-DOM parity gate. The Chat Cancel change and the parity tooling add no package. Inputs re-bound: Directory.Packages.props, tests/ArcForges.Web.Site.Tests csproj and packages.lock.json, tests/browser/ArcForges.Web.Browser.Tests csproj and packages.lock.json. Every npm coordinate and every other input is unchanged from web-40-admission-r11. The U5 closure rationale of web-40-admission-r11 still applies.", "inputs": { ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", - "Directory.Packages.props": "0e38d67479b9a3e45d0c3cf3ae4b1b96477dcbf57ef285d7ca4bff66b42f4408", + "Directory.Packages.props": "95e73db26cdb9ba15ccd3c752d804be4ee083f85fdf589acf837121a2c3975b7", "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", @@ -80,14 +80,14 @@ "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "c79f3d29ae3c94cd9e1478cf16514e5223689d47fd6252c6e8aafdee8cc57075", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", - "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "77d795a2a58b2b043814f5b03dd826034348c8772905afe59ccbb2df7c59878e", - "tests/ArcForges.Web.Site.Tests/packages.lock.json": "c55404228f3ef546808c86c4cfac706e59f5be0b0a8ccb76a944e92cf2220d46", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "15eb52424732fb8bf0a7479acf0c323b9a36c7e1503c3369bf92f402319b55e0", "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "af03c905cf03a946129d821708c500812e630358f09ba7ba59cec72b658b3ff8", - "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "86aff01e8a6376feb3d0eeba1ffddf6a5252f00a5b09eb5368c3fbae98f7137e", - "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "52409f3ea5cd0db52a8fa1320d097d573ddc15543421d9f00b29f7aa6d3207a8", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" @@ -103,7 +103,7 @@ ], "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site; the deploy job ships the same sealed bytes under the unchanged main-push guards. No hosted browser, device, macOS or live-service check is added.", "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", - "previous": "eng/policy/dependency-reviews/web-40-admission-r9.json", + "previous": "eng/policy/dependency-reviews/web-40-admission-r11.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -811,7 +811,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r11.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r12.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", @@ -846,7 +846,7 @@ "licence": "MIT", "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", - "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + "classification": "Test-only HTML5 parser: a direct reference of tests/ArcForges.Web.Site.Tests (the WEB.40 normalised-DOM parity gate, 2026-10-09 adjudication) and a transitive dependency of the bUnit closure. Reached only from tests/**; never in a published output." }, "arcforges.build.policy/1.0.0-ci.94.1": { "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", @@ -876,6 +876,21 @@ "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." }, + "deque.axecore.commons/4.13.0": { + "contentHash": "s+pmv/W8wJ9EXIJTbqqxFDjzsH78JC545k3J0n8Bdb7/dG8naE1Hi5MZWCU/p/0yHaDJWP6FFFVk2I6/frttVg==", + "licence": "MPL-2.0", + "testOnly": true, + "nuspecSha256": "99c3d40cc4f4e0bf3cbcc7c272810116663adc8e0da00c6d3db038f8c09c8ce3", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.commons/4.13.0/deque.axecore.commons.nuspec", + "classification": "Test-only axe-core script carrier (MPL-2.0 is recorded by the WEB.40 2026-10-09 adjudication). File-level weak copyleft, accepted only for a row marked testOnly: no product project restores it, and it is never shipped or compiled into a product output. Referenced only through Deque.AxeCore.Playwright by tests/browser." + }, + "deque.axecore.playwright/4.13.0": { + "contentHash": "Ltn/cHlelruijCvoKxYMKYpd58uZFeMsYNEdvTEWt+nEkMfbfNboPVhOPj0MZ0XoLpzLOhUutHIM/oPjRKJnFw==", + "licence": "MIT", + "nuspecSha256": "a57f77f0d8101e573f4dc7194387e9e8356a05445e403ac5558bd556332e2c99", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.playwright/4.13.0/deque.axecore.playwright.nuspec", + "classification": "Test-only accessibility package (WEB.40 P2-021 item 8): axe-core run through Microsoft.Playwright for .NET. Referenced only by tests/browser (local opt-in, never built in CI). axe-core is injected only into the page under test. Its Microsoft.Playwright 1.20.2 dependency resolves to the centrally pinned 1.62.0." + }, "google.protobuf/3.36.1": { "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", "licence": "BSD-3-Clause", @@ -1212,6 +1227,13 @@ "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." }, + "newtonsoft.json/13.0.1": { + "contentHash": "g3MbZi6vBTeaI/hEbvR7vBETSd1DWLe9i1E4P+nPY34v5i94zqUqDXvdWC3G+7tYN9SnsdU9zzegrnRz4h7nsQ==", + "licence": "MIT", + "nuspecSha256": "86ae2dca2b50badbad00c2f115cc268c8c4a3f42d0018d0576a0b4c20be6eb11", + "source": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", + "classification": "Transitive MIT JSON library of Deque.AxeCore.Commons, test-only: reached only from tests/browser (local opt-in). No product project references it; the architecture policy refuses its use in product C# source." + }, "system.componentmodel.annotations/5.0.0": { "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", "licence": "MIT", @@ -1219,6 +1241,13 @@ "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." }, + "system.io.abstractions/17.0.24": { + "contentHash": "O0ASsrpvPWW3bBT3v7HbauwiD/0KliKnXkt+T5pWR7/vZad2+EOWX+klsbJubvCgmpuBkVZgr5ayI0FRkgPPJw==", + "licence": "MIT", + "nuspecSha256": "5de77d739386b194c179ed5c4fc8e39e1362552a560f34ac6fb9daa4bafadf22", + "source": "https://api.nuget.org/v3-flatcontainer/system.io.abstractions/17.0.24/system.io.abstractions.nuspec", + "classification": "Transitive MIT file-system abstraction of Deque.AxeCore.Playwright, test-only: reached only from tests/browser (local opt-in). Its net6.0 dependency group is empty, so no further package is restored for net10.0." + }, "system.security.accesscontrol/6.0.1": { "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", "licence": "MIT", diff --git a/eng/policy/dependency-reviews/web-40-admission-r12.json b/eng/policy/dependency-reviews/web-40-admission-r12.json new file mode 100644 index 0000000..2793956 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r12.json @@ -0,0 +1,797 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-09", + "rationale": "WEB.40 successor of web-40-admission-r11 for the parity, focus and accessibility re-proof unit U6 (2026-10-09 adjudication). Test-only additions: Deque.AxeCore.Playwright 4.13.0 (MIT) with its dependencies Deque.AxeCore.Commons 4.13.0 (MPL-2.0, the axe-core script carrier, admitted only as testOnly), Newtonsoft.Json 13.0.1 (MIT) and System.IO.Abstractions 17.0.24 (MIT). They are referenced only by tests/browser (local opt-in Microsoft.Playwright suite, never built or restored in CI); axe-core is injected only into the page under test. AngleSharp 1.7.0 (already admitted, MIT) is now also a direct reference of tests/ArcForges.Web.Site.Tests for the normalised-DOM parity gate. The Chat Cancel change and the parity tooling add no package. Inputs re-bound: Directory.Packages.props, tests/ArcForges.Web.Site.Tests csproj and packages.lock.json, tests/browser/ArcForges.Web.Browser.Tests csproj and packages.lock.json. Every npm coordinate and every other input is unchanged from web-40-admission-r11. The U5 closure rationale of web-40-admission-r11 still applies.", + "inputs": { + ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "95e73db26cdb9ba15ccd3c752d804be4ee083f85fdf589acf837121a2c3975b7", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "eng/provenance/records/browser-resources-r12.json": "6b470604b64d692d0eaa215be133506af065856dde0fde9ee8e702bba883bb88", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "392b8874131b713ac07ab595ff541122fa3c2317f9ba81b34ffe6115f4363825", + "package.json": "60b8b8e853ae2ac0dfec58831d61c717b3e4fb26ed3848745b36e9a2ec370ea5", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "6ae92b481b6a892a9b4952f7e4b8da56b7caac8f6a6206151b017108e2c99229", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "75bd9b2ebbe6ed307c134473be0319fd536a7733ce00fe1237148299e5f59239", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "61d7150f179a77464386591f7a8c38cdbadb95b9c29fff18fe2ef912bef91931", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "ac5bab6c098c95abb7b91c56821d12c3f8e8d6a109d854d7a8cbaba86a8e0823", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "c79f3d29ae3c94cd9e1478cf16514e5223689d47fd6252c6e8aafdee8cc57075", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "15eb52424732fb8bf0a7479acf0c323b9a36c7e1503c3369bf92f402319b55e0", + "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", + "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "af03c905cf03a946129d821708c500812e630358f09ba7ba59cec72b658b3ff8", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", + "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site; the deploy job ships the same sealed bytes under the unchanged main-push guards. No hosted browser, device, macOS or live-service check is added.", + "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r11.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r11.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Test-only HTML5 parser: a direct reference of tests/ArcForges.Web.Site.Tests (the WEB.40 normalised-DOM parity gate, 2026-10-09 adjudication) and a transitive dependency of the bUnit closure. Reached only from tests/**; never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "deque.axecore.commons/4.13.0": { + "contentHash": "s+pmv/W8wJ9EXIJTbqqxFDjzsH78JC545k3J0n8Bdb7/dG8naE1Hi5MZWCU/p/0yHaDJWP6FFFVk2I6/frttVg==", + "licence": "MPL-2.0", + "testOnly": true, + "nuspecSha256": "99c3d40cc4f4e0bf3cbcc7c272810116663adc8e0da00c6d3db038f8c09c8ce3", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.commons/4.13.0/deque.axecore.commons.nuspec", + "classification": "Test-only axe-core script carrier (MPL-2.0 is recorded by the WEB.40 2026-10-09 adjudication). File-level weak copyleft, accepted only for a row marked testOnly: no product project restores it, and it is never shipped or compiled into a product output. Referenced only through Deque.AxeCore.Playwright by tests/browser." + }, + "deque.axecore.playwright/4.13.0": { + "contentHash": "Ltn/cHlelruijCvoKxYMKYpd58uZFeMsYNEdvTEWt+nEkMfbfNboPVhOPj0MZ0XoLpzLOhUutHIM/oPjRKJnFw==", + "licence": "MIT", + "nuspecSha256": "a57f77f0d8101e573f4dc7194387e9e8356a05445e403ac5558bd556332e2c99", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.playwright/4.13.0/deque.axecore.playwright.nuspec", + "classification": "Test-only accessibility package (WEB.40 P2-021 item 8): axe-core run through Microsoft.Playwright for .NET. Referenced only by tests/browser (local opt-in, never built in CI). axe-core is injected only into the page under test. Its Microsoft.Playwright 1.20.2 dependency resolves to the centrally pinned 1.62.0." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "newtonsoft.json/13.0.1": { + "contentHash": "g3MbZi6vBTeaI/hEbvR7vBETSd1DWLe9i1E4P+nPY34v5i94zqUqDXvdWC3G+7tYN9SnsdU9zzegrnRz4h7nsQ==", + "licence": "MIT", + "nuspecSha256": "86ae2dca2b50badbad00c2f115cc268c8c4a3f42d0018d0576a0b4c20be6eb11", + "source": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", + "classification": "Transitive MIT JSON library of Deque.AxeCore.Commons, test-only: reached only from tests/browser (local opt-in). No product project references it; the architecture policy refuses its use in product C# source." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.io.abstractions/17.0.24": { + "contentHash": "O0ASsrpvPWW3bBT3v7HbauwiD/0KliKnXkt+T5pWR7/vZad2+EOWX+klsbJubvCgmpuBkVZgr5ayI0FRkgPPJw==", + "licence": "MIT", + "nuspecSha256": "5de77d739386b194c179ed5c4fc8e39e1362552a560f34ac6fb9daa4bafadf22", + "source": "https://api.nuget.org/v3-flatcontainer/system.io.abstractions/17.0.24/system.io.abstractions.nuspec", + "classification": "Transitive MIT file-system abstraction of Deque.AxeCore.Playwright, test-only: reached only from tests/browser (local opt-in). Its net6.0 dependency group is empty, so no further package is restored for net10.0." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/eng/provenance/files.json b/eng/provenance/files.json index 17e0532..9affae6 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -23,9 +23,14 @@ "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", + "docs/web-40-accessibility.md", + "eng/policy/dependency-reviews/web-40-admission-r12.json", "NuGet.config", "README.md", "SECURITY.md", + "tests/ArcForges.Web.Site.Tests/SiteParity/HtmlNormaliser.cs", + "tests/ArcForges.Web.Site.Tests/SiteParityTests.cs", + "tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs", "THIRD_PARTY_NOTICES.md", "docs/bootstrap-plan.md", "docs/build-identity.md", diff --git a/src/ArcForges.Web.App/Pages/Chat.razor b/src/ArcForges.Web.App/Pages/Chat.razor index b8f28e1..2f76afe 100644 --- a/src/ArcForges.Web.App/Pages/Chat.razor +++ b/src/ArcForges.Web.App/Pages/Chat.razor @@ -16,10 +16,7 @@ Name <input @bind="_name" @bind:event="oninput" /> </label> <Button type="submit" aria-disabled="@(_pending ? "true" : "false")">@(_pending ? "Sending…" : "Send")</Button> - @if (_pending) - { - <button type="button" class="button" @onclick="Cancel">Cancel</button> - } + <button type="button" class="button" aria-disabled="@(_pending ? "false" : "true")" tabindex="@(_pending ? null : "-1")" @onclick="Cancel">Cancel</button> </form> <ol class="transcript" aria-live="polite" aria-label="Messages"> @foreach (var entry in _entries) @@ -89,8 +86,16 @@ } } + /// <summary> + /// Cancel is always rendered (option a of the 2026-10-09 focus adjudication). While idle it is aria-disabled and takes no + /// tab stop, so focus never falls to the body when a message ends; a click is refused. + /// </summary> private void Cancel() { + if (!_pending) + { + return; + } _active?.Cancel(); } diff --git a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs index b824aca..32dbba3 100644 --- a/tests/ArcForges.Web.App.Tests/ChatPageTests.cs +++ b/tests/ArcForges.Web.App.Tests/ChatPageTests.cs @@ -19,8 +19,8 @@ public sealed class ChatPageTests private static IElement SendButton(IRenderedComponent<Chat> cut) => cut.FindAll("button").Single(button => button.TextContent.Trim() is "Send" or "Sending…"); - private static IElement? CancelButton(IRenderedComponent<Chat> cut) => - cut.FindAll("button").SingleOrDefault(button => button.TextContent.Trim() == "Cancel"); + private static IElement CancelButton(IRenderedComponent<Chat> cut) => + cut.FindAll("button").Single(button => button.TextContent.Trim() == "Cancel"); private static HttpResponseMessage Greeting(string name) => Responses.Raw(ProbeFixtures.HelloReply($"Hello, {name}!"), "application/grpc-web+proto"); @@ -95,9 +95,11 @@ public void ChatCanCancelAPendingMessageReportsItAsCancelledAndDoesNotDoubleSend cut.Find("form").Submit(); Assert.Equal(1, server.Count); - CancelButton(cut)!.Click(); + CancelButton(cut).Click(); cut.WaitForAssertion(() => Assert.Contains("The request was cancelled.", cut.Markup, StringComparison.Ordinal)); - Assert.Null(CancelButton(cut)); + // Cancel stays in the document after the message ends, idle again: aria-disabled and out of the tab order. + Assert.Equal("true", CancelButton(cut).GetAttribute("aria-disabled")); + Assert.Equal("-1", CancelButton(cut).GetAttribute("tabindex")); Assert.Equal("false", SendButton(cut).GetAttribute("aria-disabled")); } @@ -217,11 +219,60 @@ public void ChatTabsFromTheNameFieldToSendAndReachesCancelOnlyWhileAMessageIsPen Assert.Equal(new[] { "input:Name", "button:Sending…", "button:Cancel" }, FocusOrder.TabSequence(cut)); FocusOrder.AssertNoPositiveTabIndex(cut); - CancelButton(cut)!.Click(); + CancelButton(cut).Click(); cut.WaitForAssertion(() => Assert.Contains("The request was cancelled.", cut.Markup, StringComparison.Ordinal)); Assert.Equal(new[] { "input:Name", "button:Send" }, FocusOrder.TabSequence(cut)); } + [Fact] + public void CancelStaysInTheDocumentWhileIdleAndAClickOnItIsRefused() + { + // Option (a) of the 2026-10-09 adjudication: Cancel is persistent. While idle it is aria-disabled and tabindex -1, so + // it is never a tab stop and never removed; focus cannot fall to the body when a message ends. + var server = ScriptedServer.Always(() => Responses.Raw(ProbeFixtures.HelloReply("Hello, ArcForges!"), "application/grpc-web+proto")); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render<Chat>(); + var cancel = CancelButton(cut); + Assert.Equal("true", cancel.GetAttribute("aria-disabled")); + Assert.Equal("-1", cancel.GetAttribute("tabindex")); + Assert.False(cancel.HasAttribute("disabled")); + + cancel.Click(); + Assert.Equal(0, server.Count); + Assert.Empty(cut.FindAll("li")); + Assert.Equal("true", CancelButton(cut).GetAttribute("aria-disabled")); + Assert.Equal(new[] { "input:Name", "button:Send" }, FocusOrder.TabSequence(cut)); + } + + [Fact] + public void CancelBecomesTabbableWhileAMessageIsPendingAndReturnsToIdleWithoutBeingRemoved() + { + var server = new ScriptedServer(async (_, token) => + { + await Task.Delay(Timeout.Infinite, token); + return Greeting("unused"); + }); + using var context = new BunitContext(); + AccountPageTests.RegisterProbes(context, server); + + var cut = context.Render<Chat>(); + SendButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("Sending…", cut.Markup, StringComparison.Ordinal)); + Assert.Equal("false", CancelButton(cut).GetAttribute("aria-disabled")); + Assert.False(CancelButton(cut).HasAttribute("tabindex")); + Assert.Equal(new[] { "input:Name", "button:Sending…", "button:Cancel" }, FocusOrder.TabSequence(cut)); + + CancelButton(cut).Click(); + cut.WaitForAssertion(() => Assert.Contains("The request was cancelled.", cut.Markup, StringComparison.Ordinal)); + Assert.Equal("true", CancelButton(cut).GetAttribute("aria-disabled")); + Assert.Equal("-1", CancelButton(cut).GetAttribute("tabindex")); + Assert.Equal(new[] { "input:Name", "button:Send" }, FocusOrder.TabSequence(cut)); + FocusOrder.AssertNoPositiveTabIndex(cut); + Assert.Equal(1, server.Count); + } + [Fact] public void EnterInTheNameFieldSendsThroughTheFormsDefaultButtonAndTheReplyIsAnnouncedInTheTranscript() { diff --git a/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs b/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs index 568197e..e0c70e2 100644 --- a/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs +++ b/tests/ArcForges.Web.Policy.Tests/NuGetAdmission.cs @@ -8,6 +8,12 @@ public static class NuGetAdmission { private static readonly string[] PermissiveLicences = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC"]; + /// <summary> + /// File-level copyleft licences that are admitted only for a row marked testOnly (WEB.40 2026-10-09 adjudication: axe-core, + /// the accessibility re-proof). A testOnly row must not be restored by any product project (see the policy tests). + /// </summary> + private static readonly string[] TestOnlyLicences = ["MPL-2.0"]; + /// <summary>Returns every refusal reason; an empty list means the closure is admitted.</summary> public static IReadOnlyList<string> Violations(JsonObject admitted, IReadOnlyDictionary<string, string> restored) { @@ -22,8 +28,10 @@ public static IReadOnlyList<string> Violations(JsonObject admitted, IReadOnlyDic violations.Add("Content hash differs from the admitted record: " + key); var licence = row["licence"]?.GetValue<string>() ?? string.Empty; var firstParty = key.StartsWith("arcforges.", StringComparison.Ordinal); + var testOnly = row["testOnly"]?.GetValue<bool>() == true; if (!PermissiveLicences.Contains(licence, StringComparer.Ordinal) - && !(firstParty && licence == "AGPL-3.0-only")) + && !(firstParty && licence == "AGPL-3.0-only") + && !(testOnly && TestOnlyLicences.Contains(licence, StringComparer.Ordinal))) violations.Add("Forbidden or unreviewed licence " + licence + ": " + key); } return violations; diff --git a/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs b/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs index 56e4710..7ba6f3f 100644 --- a/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs +++ b/tests/ArcForges.Web.Policy.Tests/NuGetClosureAdmissionTests.cs @@ -60,6 +60,51 @@ public void CopyleftThirdPartyLicenceIsRefused() Assert.Contains(NuGetAdmission.Violations(admitted, restored), message => message.StartsWith("Forbidden or unreviewed licence", StringComparison.Ordinal)); } + [Fact] + public void TestOnlyCopyleftIsAdmittedOnlyForATestOnlyRow() + { + var restored = new Dictionary<string, string> { ["axe.test/1.0.0"] = "h" }; + var testOnly = new JsonObject + { + ["axe.test/1.0.0"] = new JsonObject { ["contentHash"] = "h", ["licence"] = "MPL-2.0", ["testOnly"] = true }, + }; + Assert.Empty(NuGetAdmission.Violations(testOnly, restored)); + + var productionCopyleft = new JsonObject + { + ["axe.test/1.0.0"] = new JsonObject { ["contentHash"] = "h", ["licence"] = "MPL-2.0" }, + }; + Assert.Contains(NuGetAdmission.Violations(productionCopyleft, restored), message => message.StartsWith("Forbidden or unreviewed licence", StringComparison.Ordinal)); + + var testOnlyGpl = new JsonObject + { + ["axe.test/1.0.0"] = new JsonObject { ["contentHash"] = "h", ["licence"] = "GPL-3.0-only", ["testOnly"] = true }, + }; + Assert.Contains(NuGetAdmission.Violations(testOnlyGpl, restored), message => message.StartsWith("Forbidden or unreviewed licence", StringComparison.Ordinal)); + } + + [Fact] + public void TestOnlyPackagesAreRestoredByNoProductProject() + { + var root = RepositoryRoot.Find(); + var testOnly = AdmittedClosure(root) + .Where(entry => entry.Value!.AsObject()["testOnly"]?.GetValue<bool>() == true) + .Select(entry => entry.Key.Split('/')[0]) + .ToList(); + Assert.NotEmpty(testOnly); + + var productLocks = Directory.EnumerateFiles(root, "packages.lock.json", SearchOption.AllDirectories) + .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + && !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)) + .Where(path => !Path.GetRelativePath(root, path).StartsWith($"tests{Path.DirectorySeparatorChar}", StringComparison.Ordinal)); + foreach (var lockPath in productLocks) + { + var text = File.ReadAllText(lockPath); + foreach (var id in testOnly) + Assert.DoesNotContain($"\"{id}\": {{", text, StringComparison.OrdinalIgnoreCase); + } + } + private static JsonObject AdmittedClosure(string root) { var policy = JsonNode.Parse(File.ReadAllText(Path.Combine(root, "eng", "policy", "dependency-policy.json")))!.AsObject(); diff --git a/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj b/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj index cb04d26..a46b9a7 100644 --- a/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj +++ b/tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj @@ -5,6 +5,7 @@ <TestingPlatformDotnetTestSupport>true</TestingPlatformDotnetTestSupport> </PropertyGroup> <ItemGroup> + <PackageReference Include="AngleSharp" /> <PackageReference Include="xunit.v3.mtp-v2" /> <ProjectReference Include="../../src/ArcForges.Web.Site/ArcForges.Web.Site.csproj" /> </ItemGroup> diff --git a/tests/ArcForges.Web.Site.Tests/SiteParity/HtmlNormaliser.cs b/tests/ArcForges.Web.Site.Tests/SiteParity/HtmlNormaliser.cs new file mode 100644 index 0000000..b43620f --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/SiteParity/HtmlNormaliser.cs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Normalised-DOM comparison of the public pages (WEB.40 parity gate, 2026-10-09 adjudication). Both sides are parsed with +// the HTML5 parser (AngleSharp), so void-element syntax, attribute-name case, entity form, boolean attribute spelling and +// text-node splitting do not count as differences. Scripts, module preloads and comments are not part of the rendered +// structure and are removed, which is the TB-01 change; every other difference is reported. +using System.Text; +using AngleSharp; +using AngleSharp.Dom; +using AngleSharp.Html.Parser; + +namespace ArcForges.Web.Site.Tests.SiteParity; + +/// <summary>The normalised structure of one HTML document, one line per start tag, end tag or text run.</summary> +public sealed record NormalisedHtml(IReadOnlyList<string> Lines, int RemovedScripts, int RemovedModulePreloads, int RemovedComments); + +internal static class HtmlNormaliser +{ + /// <summary>The placeholder that replaces a content-hashed stylesheet name. Its bytes are compared separately.</summary> + internal const string StylesheetPlaceholder = "/assets/<stylesheet>.css"; + + private static readonly HtmlParser Parser = new(); + + internal static NormalisedHtml Normalise(string html) + { + ArgumentNullException.ThrowIfNull(html); + var document = Parser.ParseDocument(html); + var state = new Walk(); + if (document.DocumentElement is { } root) + { + state.Element(root); + } + return new NormalisedHtml(state.Lines, state.RemovedScripts, state.RemovedModulePreloads, state.RemovedComments); + } + + /// <summary>The line-level difference between two normalised documents, in an LCS order; empty when they are equal.</summary> + internal static IReadOnlyList<string> Difference(IReadOnlyList<string> reference, IReadOnlyList<string> candidate) + { + ArgumentNullException.ThrowIfNull(reference); + ArgumentNullException.ThrowIfNull(candidate); + var n = reference.Count; + var m = candidate.Count; + var common = new int[n + 1, m + 1]; + for (var i = n - 1; i >= 0; i--) + { + for (var j = m - 1; j >= 0; j--) + { + common[i, j] = string.Equals(reference[i], candidate[j], StringComparison.Ordinal) + ? common[i + 1, j + 1] + 1 + : Math.Max(common[i + 1, j], common[i, j + 1]); + } + } + var result = new List<string>(); + int a = 0, b = 0; + while (a < n && b < m) + { + if (string.Equals(reference[a], candidate[b], StringComparison.Ordinal)) + { + a++; + b++; + } + else if (common[a + 1, b] >= common[a, b + 1]) + { + result.Add("- " + reference[a++]); + } + else + { + result.Add("+ " + candidate[b++]); + } + } + while (a < n) + { + result.Add("- " + reference[a++]); + } + while (b < m) + { + result.Add("+ " + candidate[b++]); + } + return result; + } + + private sealed class Walk + { + private static readonly char[] Whitespace = [' ', '\t', '\n', '\r', '\f']; + + /// <summary>The void elements have no end tag and no children, so their syntax (with or without the slash) is not compared.</summary> + private static readonly HashSet<string> VoidElements = new(StringComparer.Ordinal) + { + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr", + }; + + internal List<string> Lines { get; } = []; + internal int RemovedScripts { get; private set; } + internal int RemovedModulePreloads { get; private set; } + internal int RemovedComments { get; private set; } + + internal void Element(IElement element) + { + if (element.LocalName == "script") + { + RemovedScripts++; + return; + } + if (element.LocalName == "link" && HasToken(element.GetAttribute("rel"), "modulepreload")) + { + RemovedModulePreloads++; + return; + } + + Lines.Add("<" + element.LocalName + Attributes(element) + ">"); + if (VoidElements.Contains(element.LocalName)) + { + return; + } + var text = new StringBuilder(); + foreach (var child in element.ChildNodes) + { + switch (child) + { + case IText: + text.Append(child.TextContent); + break; + case IElement childElement: + Flush(text); + Element(childElement); + break; + case IComment: + RemovedComments++; + break; + } + } + Flush(text); + Lines.Add("</" + element.LocalName + ">"); + } + + private void Flush(StringBuilder text) + { + var value = string.Join(' ', text.ToString().Split(Whitespace, StringSplitOptions.RemoveEmptyEntries)); + text.Clear(); + if (value.Length > 0) + { + Lines.Add("#text " + value); + } + } + + private static string Attributes(IElement element) + { + var isStylesheet = HasToken(element.GetAttribute("rel"), "stylesheet"); + var builder = new StringBuilder(); + foreach (var attribute in element.Attributes.OrderBy(attribute => attribute.Name, StringComparer.Ordinal)) + { + var value = attribute.Value; + if (attribute.Name == "class") + { + value = string.Join(' ', value.Split(Whitespace, StringSplitOptions.RemoveEmptyEntries)); + } + else if (isStylesheet && attribute.Name == "href" && IsHashedStylesheet(value)) + { + value = StylesheetPlaceholder; + } + builder.Append(' ').Append(attribute.Name).Append("=\"").Append(value).Append('"'); + } + return builder.ToString(); + } + + private static bool IsHashedStylesheet(string href) => + href.StartsWith("/assets/", StringComparison.Ordinal) && href.EndsWith(".css", StringComparison.Ordinal); + + private static bool HasToken(string? list, string token) => + list is not null && list.Split(Whitespace, StringSplitOptions.RemoveEmptyEntries).Contains(token, StringComparer.OrdinalIgnoreCase); + } +} diff --git a/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs b/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs new file mode 100644 index 0000000..806449f --- /dev/null +++ b/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// WEB.40 parity gate for the public Site (brief section 10, 2026-10-09 adjudication). The byte gate is replaced by a +// normalised-DOM gate: the recorded React prerender and the C# Site are parsed as HTML and compared structurally, and every +// non-HTML asset stays byte-identical. The normaliser is tested on fixtures in CI. The comparison against the recorded React +// prerender needs that local output, so it runs only when ARCFORGES_SITE_PARITY_REFERENCE names it (a local opt-in run that +// never happens in CI). The result is recorded in docs/web-40-site-parity.md. +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using ArcForges.Web.Site.Tests.SiteParity; +using Xunit; + +namespace ArcForges.Web.Site.Tests; + +public sealed class SiteParityTests +{ + /// <summary>The directory holding the recorded React prerender (for example artifacts/parity/react).</summary> + internal const string ReferenceVariable = "ARCFORGES_SITE_PARITY_REFERENCE"; + + /// <summary>An optional file that receives the parity report of a local run.</summary> + internal const string ReportVariable = "ARCFORGES_SITE_PARITY_REPORT"; + + /// <summary>The source commit that the React prerender was built from (VITE_SOURCE_REF), so the source links match.</summary> + internal const string ReferenceSourceRef = "80900a1430c0fbb483e4a220e20980e3c18728b3"; + + private static readonly string[] HtmlPaths = ["index.html", "hello/index.html", "cloud-hello/index.html", "404.html"]; + + private static readonly string[] ByteIdenticalPaths = ["404.css", "favicon.svg", "robots.txt"]; + + /// <summary>One SHA-256 source of the React policy, with its leading space (the policy lists them after 'self').</summary> + private static readonly Regex ScriptHash = new(@" 'sha256-[A-Za-z0-9+/=]+'", RegexOptions.CultureInvariant); + + private static readonly Regex ReferenceStylesheet = new(@"href=""/(assets/[^""]+\.css)""", RegexOptions.CultureInvariant); + + [Fact] + public void NormalisationAcceptsEquivalentSerialisationsAndRemovesHydrationNodes() + { + // The React serialiser and the Razor HtmlRenderer spell the same page differently; none of these may be a difference. + var react = """ + <!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Your hello — ArcForges + +

Hello, World!

+ + """; + var csharp = """ + + + + + Your hello — ArcForges + + + +

Hello, World!

+ + + + + """; + + var reference = HtmlNormaliser.Normalise(react); + var candidate = HtmlNormaliser.Normalise(csharp); + + Assert.Equal(reference.Lines, candidate.Lines); + Assert.Empty(HtmlNormaliser.Difference(reference.Lines, candidate.Lines)); + Assert.Equal(1, reference.RemovedScripts); + Assert.Equal(1, reference.RemovedModulePreloads); + Assert.True(reference.RemovedComments >= 3); + Assert.Equal(0, candidate.RemovedScripts + candidate.RemovedModulePreloads + candidate.RemovedComments); + } + + [Fact] + public void NormalisationReportsTextAttributeAndStructuralDifferences() + { + var reference = HtmlNormaliser.Normalise("

Hello

Hello example
"); + + var text = HtmlNormaliser.Normalise("

Hi

Hello example
"); + var textDifference = HtmlNormaliser.Difference(reference.Lines, text.Lines); + Assert.Contains("- #text Hello", textDifference); + Assert.Contains("+ #text Hi", textDifference); + + var attribute = HtmlNormaliser.Normalise("

Hello

Hello example
"); + Assert.Contains("+ ", HtmlNormaliser.Difference(reference.Lines, attribute.Lines)); + + var structure = HtmlNormaliser.Normalise("

Hello

"); + Assert.NotEmpty(HtmlNormaliser.Difference(reference.Lines, structure.Lines)); + } + + [Fact] + public void AScriptThatEnablesAControlIsRemovedAndTheStaticControlStaysDisabled() + { + // The retired live control: the React page disabled the button until a script enabled it. Removing the script leaves + // the control disabled in the static markup, so the only difference is the removed script (the retirement). + var react = "
"; + var csharp = "
"; + + var reference = HtmlNormaliser.Normalise(react); + var candidate = HtmlNormaliser.Normalise(csharp); + + Assert.Empty(HtmlNormaliser.Difference(reference.Lines, candidate.Lines)); + Assert.Equal(1, reference.RemovedScripts); + } + + [Fact] + public void OnlyAHashedStylesheetHrefIsAPlaceholderAndOtherHrefsAreCompared() + { + var reference = HtmlNormaliser.Normalise(""); + var candidate = HtmlNormaliser.Normalise(""); + Assert.Equal(reference.Lines, candidate.Lines); + Assert.Contains(reference.Lines, line => line.Contains(HtmlNormaliser.StylesheetPlaceholder, StringComparison.Ordinal)); + + var anchor = HtmlNormaliser.Normalise("
download"); + Assert.Contains("href=\"/assets/root-BPtceTrQ.css\"", anchor.Lines[0], StringComparison.Ordinal); + } + + [Fact] + public void TheOnlyContentSecurityPolicyDifferenceIsTheReactScriptHashes() + { + // The React policy listed one SHA-256 per inline script body; the C# Site has no inline script, so script-src is 'self'. + var reference = " Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-aaaa=' 'sha256-bbbb/+='; style-src 'self'"; + var candidate = " Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'"; + + Assert.Equal(candidate, WithoutScriptHashes(reference)); + Assert.Empty(HeaderDifferences([reference], [candidate])); + Assert.NotEmpty(HeaderDifferences([" Referrer-Policy: no-referrer"], [" Referrer-Policy: origin"])); + Assert.NotEmpty(HeaderDifferences([reference], [" Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'"])); + } + + [Fact] + public async Task TheCSharpSiteAgreesWithTheRecordedReactPrerenderWhenItIsNamed() + { + var reference = Environment.GetEnvironmentVariable(ReferenceVariable); + if (string.IsNullOrWhiteSpace(reference) || Environment.GetEnvironmentVariable("CI") is not null) + Assert.Skip($"Local parity run only: set {ReferenceVariable} to the recorded React prerender directory; never on CI."); + var root = Path.GetFullPath(reference); + Assert.True(Directory.Exists(root), $"The React prerender directory does not exist: {root}"); + + var site = await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = ReferenceSourceRef }, TestContext.Current.CancellationToken); + var report = new StringBuilder(); + var failures = new List(); + + // File inventory. The reference-only files are the React runtime (hydration bundles and the SPA fallback). The candidate + // has no file the reference lacks; its content-hashed stylesheet is paired with the reference stylesheet below. + var referenceFiles = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories) + .Select(path => Path.GetRelativePath(root, path).Replace('\\', '/')) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + var candidateFiles = site.Files.Select(file => file.Path).OrderBy(path => path, StringComparer.Ordinal).ToList(); + var stylesheet = ReferenceStylesheet.Match(File.ReadAllText(Path.Combine(root, "index.html"), Encoding.UTF8)).Groups[1].Value; + var candidateStylesheet = Assert.Single(candidateFiles, path => path.StartsWith("assets/site.", StringComparison.Ordinal)); + var referenceOnly = referenceFiles.Except(candidateFiles, StringComparer.Ordinal).ToList(); + var candidateOnly = candidateFiles.Except(referenceFiles, StringComparer.Ordinal).ToList(); + var expectedReferenceOnly = referenceFiles + .Where(path => path == "__spa-fallback.html" || (path.StartsWith("assets/", StringComparison.Ordinal) && path.EndsWith(".js", StringComparison.Ordinal))) + .ToList(); + report.AppendLine($"Reference files: {referenceFiles.Count}. Candidate files: {candidateFiles.Count}."); + report.AppendLine($"Reference-only (React runtime, intended): {string.Join(", ", referenceOnly)}"); + report.AppendLine($"Candidate-only: {(candidateOnly.Count == 0 ? "none" : string.Join(", ", candidateOnly))}"); + report.AppendLine($"Stylesheet pair: {stylesheet} and {candidateStylesheet}."); + if (!referenceOnly.SequenceEqual(expectedReferenceOnly, StringComparer.Ordinal)) + failures.Add("The reference-only files are not exactly the React runtime files."); + if (candidateOnly.Count > 0) + failures.Add("The candidate has files the reference does not."); + + // HTML pages: each page is a document, and its normalised structure must be equal. + foreach (var path in HtmlPaths) + { + var candidateFile = site.Find(path) ?? throw new InvalidOperationException($"The Site has no {path}."); + var referenceHtml = File.ReadAllText(Path.Combine(root, path), Encoding.UTF8); + var candidateHtml = Encoding.UTF8.GetString(candidateFile.Content); + if (!referenceHtml.StartsWith("", StringComparison.OrdinalIgnoreCase) || !candidateHtml.StartsWith("", StringComparison.OrdinalIgnoreCase)) + failures.Add($"{path} is not an HTML5 document in both outputs."); + var normalisedReference = HtmlNormaliser.Normalise(referenceHtml); + var normalisedCandidate = HtmlNormaliser.Normalise(candidateHtml); + var difference = HtmlNormaliser.Difference(normalisedReference.Lines, normalisedCandidate.Lines); + report.AppendLine($"{path}: {normalisedCandidate.Lines.Count} normalised lines; React removed {normalisedReference.RemovedScripts} scripts, {normalisedReference.RemovedModulePreloads} module preloads, {normalisedReference.RemovedComments} comments; differences {difference.Count}."); + foreach (var line in difference) + report.AppendLine($" {line}"); + if (difference.Count > 0) + failures.Add($"{path} differs after normalisation."); + } + + // Non-HTML assets: byte-identical, including the stylesheet pair whose name is content-hashed. + foreach (var path in ByteIdenticalPaths) + { + var same = SameBytes(File.ReadAllBytes(Path.Combine(root, path)), site.Find(path)?.Content); + report.AppendLine($"{path}: {(same ? "byte identical" : "DIFFERENT BYTES")}"); + if (!same) + failures.Add($"{path} is not byte-identical."); + } + var stylesheetSame = SameBytes(File.ReadAllBytes(Path.Combine(root, stylesheet)), site.Find(candidateStylesheet)?.Content); + report.AppendLine($"{stylesheet} against {candidateStylesheet}: {(stylesheetSame ? "byte identical" : "DIFFERENT BYTES")}"); + if (!stylesheetSame) + failures.Add("The stylesheet bytes are not identical."); + + // Security headers: every line equal, except the Content-Security-Policy, whose only difference is the script hashes. + var referenceHeaders = File.ReadAllLines(Path.Combine(root, "_headers"), Encoding.UTF8); + var candidateHeaders = Encoding.UTF8.GetString(site.Find("_headers")!.Content).TrimEnd('\n').Split('\n'); + var headerDifferences = HeaderDifferences(referenceHeaders, candidateHeaders); + report.AppendLine($"_headers: {candidateHeaders.Length} lines; differences {headerDifferences.Count}."); + foreach (var line in headerDifferences) + report.AppendLine($" {line}"); + if (headerDifferences.Count > 0) + failures.Add("The security headers differ beyond the script hashes."); + + var text = report.ToString(); + var reportPath = Environment.GetEnvironmentVariable(ReportVariable); + if (!string.IsNullOrWhiteSpace(reportPath)) + File.WriteAllText(reportPath, text, new UTF8Encoding(false)); + Assert.True(failures.Count == 0, text + string.Join(Environment.NewLine, failures)); + } + + private static bool SameBytes(byte[] reference, byte[]? candidate) => + candidate is not null && SHA256.HashData(reference).AsSpan().SequenceEqual(SHA256.HashData(candidate)); + + /// The header lines that differ, after the React script hashes are removed from its policy. + internal static IReadOnlyList HeaderDifferences(IReadOnlyList reference, IReadOnlyList candidate) + { + var normalisedReference = reference.Select(WithoutScriptHashes).ToList(); + var normalisedCandidate = candidate.Select(WithoutScriptHashes).ToList(); + return HtmlNormaliser.Difference(normalisedReference, normalisedCandidate); + } + + /// A header line with each script hash removed. Only the Content-Security-Policy line can carry one. + internal static string WithoutScriptHashes(string line) => + line.StartsWith(" Content-Security-Policy:", StringComparison.Ordinal) ? ScriptHash.Replace(line, string.Empty) : line; +} diff --git a/tests/ArcForges.Web.Site.Tests/packages.lock.json b/tests/ArcForges.Web.Site.Tests/packages.lock.json index 1c3c2ee..4883249 100644 --- a/tests/ArcForges.Web.Site.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Site.Tests/packages.lock.json @@ -2,6 +2,12 @@ "version": 2, "dependencies": { "net10.0": { + "AngleSharp": { + "type": "Direct", + "requested": "[1.7.0, )", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, "ArcForges.Build.Policy": { "type": "Direct", "requested": "[1.0.0-ci.94.1, )", diff --git a/tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj b/tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj index 782a887..1dab143 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj +++ b/tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj @@ -8,6 +8,7 @@ + false diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs new file mode 100644 index 0000000..5750da9 --- /dev/null +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Local opt-in accessibility re-proof of every replacement screen and state (P2-021 item 8; WEB.40 2026-10-09 adjudication; +// docs/web-40-accessibility.md). axe-core (Deque.AxeCore.Playwright, a test-only package) is injected only into the page +// under test, and each screen is run through the installed Edge or Chrome (ARCFORGES_CHROMIUM_PATH); no browser is +// downloaded. The pages are served from the local build outputs through Playwright routes, so no server and no live +// service is needed. The same-origin answers are fixed by routes. The suite skips unless ARCFORGES_LOCAL_BROWSER=1 and +// the build directories are named, and it is never built or run in CI. +using System.Text; +using Deque.AxeCore.Commons; +using Deque.AxeCore.Playwright; +using Microsoft.Playwright; +using Xunit; + +namespace ArcForges.Web.Browser.Tests; + +public sealed class LocalAccessibilityBrowserTests +{ + /// The wwwroot of the published Account and Chat profiles (artifacts/publish/app/wwwroot). + internal const string ProfileDirVariable = "ARCFORGES_PROFILE_DIR"; + + /// The static public Site output (artifacts/site). + internal const string SiteDirVariable = "ARCFORGES_SITE_DIR"; + + /// An optional file that receives one line per screen and state with the axe result. + internal const string ReportVariable = "ARCFORGES_A11Y_REPORT"; + + private const string Origin = "http://arcforges.test"; + private const string AnonymousJson = "{\"csrfToken\":\"aaaaaaaaaaaaaaaaaaaaaaaa\",\"authenticated\":false}"; + private static readonly object ReportLock = new(); + + [Fact] + public async Task AccountAnonymousPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + session.Route = async route => await route.FulfillAsync(new RouteFulfillOptions { Status = 200, ContentType = "application/json", Body = AnonymousJson }); + await session.GotoAsync("/account"); + await session.Page.WaitForSelectorAsync("text=You are not signed in."); + await session.AssertAxeAsync("Account", "anonymous first read"); + } + + [Fact] + public async Task AccountSignedInPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + session.Route = async route => await route.FulfillAsync(new RouteFulfillOptions { Status = 200, ContentType = "application/json", Body = LocalFocusBrowserTests.AuthenticatedJson }); + await session.GotoAsync("/account"); + await session.Page.WaitForSelectorAsync("button:has-text('Sign out')"); + await session.AssertAxeAsync("Account", "signed in"); + } + + [Fact] + public async Task AccountSignOutInFlightPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await session.Page.RouteAsync("**/session/v1/bootstrap", route => route.FulfillAsync(new RouteFulfillOptions { Status = 200, ContentType = "application/json", Body = LocalFocusBrowserTests.AuthenticatedJson })); + await session.Page.RouteAsync("**/session/v1/logout", async route => + { + await release.Task; + await route.FulfillAsync(new RouteFulfillOptions { Status = 200, ContentType = "application/json", Body = LocalFocusBrowserTests.ReceiptJson }); + }); + await session.GotoAsync("/account"); + await session.Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "Sign out" }).ClickAsync(); + await session.Page.WaitForSelectorAsync("button[aria-disabled=true]:has-text('Signing out')"); + await session.AssertAxeAsync("Account", "sign-out in flight"); + release.SetResult(); + await session.Page.WaitForSelectorAsync("text=You are signed out. Reload to check again."); + await session.AssertAxeAsync("Account", "session ended by sign-out (after the sign-out)"); + } + + [Fact] + public async Task AccountFailedReadPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + await session.Page.RouteAsync("**/session/v1/bootstrap", route => route.FulfillAsync(new RouteFulfillOptions { Status = 503, ContentType = "application/json", Body = "{}" })); + await session.GotoAsync("/account"); + await session.Page.WaitForSelectorAsync("p[role=alert]"); + await session.AssertAxeAsync("Account", "failed read"); + } + + [Fact] + public async Task ChatIdlePassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + await session.GotoAsync("/chat"); + await session.Page.WaitForSelectorAsync("form button[aria-disabled=false]"); + await session.AssertAxeAsync("Chat", "idle"); + } + + [Fact] + public async Task ChatPendingPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await session.Page.RouteAsync("**/api/arcforges.hello.v1.HelloService/SayHello", async route => + { + await release.Task; + await route.FulfillAsync(new RouteFulfillOptions { Status = 200, ContentType = "application/grpc-web+proto", BodyBytes = LocalFocusBrowserTests.HelloReply("Hello, ArcForges!") }); + }); + await session.GotoAsync("/chat"); + await session.Page.WaitForSelectorAsync("form button[aria-disabled=false]"); + await session.Page.Locator("form button[type=submit]").ClickAsync(); + await session.Page.WaitForSelectorAsync("form button[type=submit][aria-disabled=true]"); + await session.AssertAxeAsync("Chat", "pending"); + release.SetResult(); + await session.Page.WaitForSelectorAsync("ol.transcript li:nth-child(2)"); + } + + [Fact] + public async Task ChatAfterReplyPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + await session.Page.RouteAsync("**/api/arcforges.hello.v1.HelloService/SayHello", route => route.FulfillAsync(new RouteFulfillOptions { Status = 200, ContentType = "application/grpc-web+proto", BodyBytes = LocalFocusBrowserTests.HelloReply("Hello, ArcForges!") })); + await session.GotoAsync("/chat"); + await session.Page.WaitForSelectorAsync("form button[aria-disabled=false]"); + await session.Page.Locator("form button[type=submit]").ClickAsync(); + await session.Page.WaitForSelectorAsync("ol.transcript li:nth-child(2)"); + await session.AssertAxeAsync("Chat", "after reply"); + } + + [Fact] + public async Task SitePagesPassAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: false); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + foreach (var path in new[] { "/", "/hello/", "/cloud-hello/", "/404.html" }) + { + await session.GotoAsync(path); + await session.AssertAxeAsync("Site", $"page {path}"); + } + } + + private const string SkipMessage = + "Local opt-in only: set ARCFORGES_LOCAL_BROWSER=1, ARCFORGES_PROFILE_DIR and ARCFORGES_SITE_DIR on a developer machine; never on CI."; + + /// Serves a build directory from routes, with the single-page fallback for the profiles. + private sealed class Harness + { + private readonly IReadOnlyDictionary _environment; + private readonly string _root; + private readonly bool _spa; + + private Harness(IReadOnlyDictionary environment, string root, bool spa) + { + _environment = environment; + _root = root; + _spa = spa; + } + + /// The harness for a screen group, or null when the local gate or the named directory is missing. + internal static Harness? FromEnvironment(IReadOnlyDictionary environment, bool profile) + { + if (LocalOptIn.IsCi(environment) || !environment.TryGetValue(LocalOptIn.OptInVariable, out var optIn) || optIn != "1") + return null; + var variable = profile ? ProfileDirVariable : SiteDirVariable; + if (!environment.TryGetValue(variable, out var root) || string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) + return null; + return new Harness(environment, Path.GetFullPath(root), profile); + } + + internal async Task OpenAsync() + { + var playwright = await Playwright.CreateAsync(); + var executable = _environment.GetValueOrDefault(LocalOptIn.ChromiumPathVariable); + var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions + { + ExecutablePath = string.IsNullOrWhiteSpace(executable) ? null : executable, + }); + var page = await browser.NewPageAsync(); + var session = new Session(playwright, browser, page, _root, _spa); + await page.RouteAsync(Origin + "/**", session.ServeAsync); + return session; + } + } + + /// One browser page served from a build directory. Routes not set by the screen answer from the directory. + private sealed class Session : IAsyncDisposable + { + private readonly IPlaywright _playwright; + private readonly IBrowser _browser; + private readonly string _root; + private readonly bool _spa; + + internal Session(IPlaywright playwright, IBrowser browser, IPage page, string root, bool spa) + { + _playwright = playwright; + _browser = browser; + Page = page; + _root = root; + _spa = spa; + } + + internal IPage Page { get; } + + /// A route the screen installs for the same-origin answer of its own request; null serves the directory. + internal Func? Route { get; set; } + + internal Task GotoAsync(string path) => Page.GotoAsync(Origin + path); + + internal async Task ServeAsync(IRoute route) + { + var path = new Uri(route.Request.Url).AbsolutePath; + if (Route is not null && (path.StartsWith("/api/", StringComparison.Ordinal) || path.StartsWith("/session/", StringComparison.Ordinal))) + { + await Route(route); + return; + } + var file = Resolve(path); + if (file is null) + { + await route.FulfillAsync(new RouteFulfillOptions { Status = 404, ContentType = "text/plain", Body = "not found" }); + return; + } + await route.FulfillAsync(new RouteFulfillOptions + { + Status = 200, + ContentType = ContentType(file), + BodyBytes = await File.ReadAllBytesAsync(file), + }); + } + + /// Maps a request path to a file of the build directory; the profiles fall back to their shell. + private string? Resolve(string path) + { + var relative = Uri.UnescapeDataString(path).TrimStart('/'); + var candidate = Path.GetFullPath(Path.Combine(_root, relative.Replace('/', Path.DirectorySeparatorChar))); + if (!candidate.StartsWith(_root, StringComparison.OrdinalIgnoreCase)) + return null; + if (Directory.Exists(candidate)) + candidate = Path.Combine(candidate, "index.html"); + if (File.Exists(candidate)) + return candidate; + if (!_spa) + { + return null; + } + if (Path.HasExtension(candidate)) + return null; + var shell = Path.Combine(_root, "index.html"); + return File.Exists(shell) ? shell : null; + } + + private static string ContentType(string file) => Path.GetExtension(file).ToLowerInvariant() switch + { + ".html" => "text/html; charset=utf-8", + ".js" or ".mjs" => "text/javascript", + ".css" => "text/css", + ".json" => "application/json", + ".wasm" => "application/wasm", + ".svg" => "image/svg+xml", + ".txt" => "text/plain; charset=utf-8", + ".png" => "image/png", + ".ico" => "image/x-icon", + _ => "application/octet-stream", + }; + + /// Runs axe-core on the page under test and records the result. Every violation fails the screen. + internal async Task AssertAxeAsync(string screen, string state) + { + var result = await Page.RunAxe(); + var violations = result.Violations ?? []; + var line = new StringBuilder() + .Append(screen).Append(" | ").Append(state) + .Append(" | url ").Append(new Uri(result.Url).AbsolutePath) + .Append(" | violations ").Append(violations.Count) + .Append(" | passes ").Append(result.Passes?.Count ?? 0) + .Append(" | incomplete ").Append(result.Incomplete?.Count ?? 0) + .Append(" | inapplicable ").Append(result.Inapplicable?.Count ?? 0); + foreach (var violation in violations) + line.Append(" | ").Append(violation.Id).Append(" (").Append(violation.Impact).Append(", ").Append(violation.Nodes.Count).Append(" nodes)"); + Record(line.ToString()); + Assert.True(violations.Count == 0, $"axe found violations on {screen} ({state}): {line}"); + } + + private static void Record(string line) + { + var report = Environment.GetEnvironmentVariable(ReportVariable); + if (string.IsNullOrWhiteSpace(report)) + return; + lock (ReportLock) + { + File.AppendAllText(report, line + Environment.NewLine, new UTF8Encoding(false)); + } + } + + public async ValueTask DisposeAsync() + { + await Page.CloseAsync(); + await _browser.CloseAsync(); + _playwright.Dispose(); + } + } +} diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs index 5b261ac..f0e3e86 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalFocusBrowserTests.cs @@ -11,13 +11,13 @@ namespace ArcForges.Web.Browser.Tests; public sealed class LocalFocusBrowserTests { - private const string HelloPath = "**/api/arcforges.hello.v1.HelloService/SayHello"; - private const string BootstrapPath = "**/session/v1/bootstrap"; - private const string LogoutPath = "**/session/v1/logout"; + internal const string HelloPath = "**/api/arcforges.hello.v1.HelloService/SayHello"; + internal const string BootstrapPath = "**/session/v1/bootstrap"; + internal const string LogoutPath = "**/session/v1/logout"; private const string Ended = "You are signed out. Reload to check again."; // The same wire fixtures as the bUnit suite (tests/ArcForges.Web.App.Tests/Fixtures/ProbeFixtures.cs), in browser form. - private const string AuthenticatedJson = + internal const string AuthenticatedJson = "{\"csrfToken\":\"cccccccccccccccccccccccc\",\"authenticated\":true," + "\"session\":{\"sessionId\":\"6d1d4c2a-62a0-4b86-9d3f-0c6a3d0b5c11\",\"expiresAt\":\"2026-10-03T08:00:00.000000Z\"," + "\"idleExpiresAt\":\"2026-10-02T20:30:00.000000Z\",\"userId\":\"0f0e6a30-5d1c-4c7e-8b53-5b6b7f9a4a10\"," @@ -26,7 +26,7 @@ public sealed class LocalFocusBrowserTests + "\"profile\":{\"displayName\":\"Ada Lovelace\",\"locale\":\"en-GB\",\"timezone\":\"Europe/London\"," + "\"revision\":\"9007199254740993\"}}"; - private const string ReceiptJson = + internal const string ReceiptJson = "{\"commandId\":\"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\"effect\":\"happened\"}"; /// One gRPC-Web frame: a flag byte, a big-endian 32-bit length and the payload. @@ -43,7 +43,7 @@ private static byte[] Frame(byte flag, byte[] payload) } /// A binary gRPC-Web hello answer: the SayHelloResponse message (field 1) and an OK trailer. - private static byte[] HelloReply(string message) + internal static byte[] HelloReply(string message) { var text = Encoding.UTF8.GetBytes(message); var payload = new byte[text.Length + 2]; diff --git a/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json b/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json index 0d36a3a..d779706 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json +++ b/tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json @@ -8,6 +8,17 @@ "resolved": "1.0.0-ci.94.1", "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==" }, + "Deque.AxeCore.Playwright": { + "type": "Direct", + "requested": "[4.13.0, )", + "resolved": "4.13.0", + "contentHash": "tqzqVEsQfVg4fmx9/AkbgYjVMYHHOGI7WkTznKuP8ayi0uaBcd+2skOfxnVdUBlEhpHdu9stzxOZ6uPVhIS2JA==", + "dependencies": { + "Deque.AxeCore.Commons": "4.13.0", + "Microsoft.Playwright": "1.20.2", + "System.IO.Abstractions": "17.0.24" + } + }, "Microsoft.Playwright": { "type": "Direct", "requested": "[1.62.0, )", @@ -29,6 +40,14 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, + "Deque.AxeCore.Commons": { + "type": "Transitive", + "resolved": "4.13.0", + "contentHash": "roq2z6OmKY4KVXGj1lSigUbjE7DnIcYLqPOVcnWrusS6CI7oB6/He8Oo3ZO/TCOlnaTS1hkJOuKPwL+PyhyC6w==", + "dependencies": { + "NewtonSoft.Json": "13.0.1" + } + }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", @@ -74,11 +93,21 @@ "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, "System.ComponentModel.Annotations": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" }, + "System.IO.Abstractions": { + "type": "Transitive", + "resolved": "17.0.24", + "contentHash": "hA7bacntMiZv1Yf9xtjwl/GP3GT1mG84QxhAk7ijAUD0pJhJaVVwXScE13vMpXnNtlaRDW6SeyZdWg2j2qrh4w==" + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "6.0.1", From 01246e88f2758be796eee2c70220f71069cf436e Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 03:48:06 +0100 Subject: [PATCH 26/38] [WEB.40] Refresh the Ui.Tests, App.Tests and Operations.Tests lock files for the centrally pinned AngleSharp (CentralTransitive, same version 1.7.0, no new admission; locked restore fails without it) (U6, fix) Co-Authored-By: Claude Haiku 5.5 --- tests/ArcForges.Web.App.Tests/packages.lock.json | 11 ++++++----- .../ArcForges.Web.Operations.Tests/packages.lock.json | 11 ++++++----- tests/ArcForges.Web.Ui.Tests/packages.lock.json | 11 ++++++----- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/ArcForges.Web.App.Tests/packages.lock.json b/tests/ArcForges.Web.App.Tests/packages.lock.json index 5db8752..8f0c9b6 100644 --- a/tests/ArcForges.Web.App.Tests/packages.lock.json +++ b/tests/ArcForges.Web.App.Tests/packages.lock.json @@ -39,11 +39,6 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, - "AngleSharp": { - "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" - }, "AngleSharp.Css": { "type": "Transitive", "resolved": "1.0.1", @@ -474,6 +469,12 @@ "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" } }, + "AngleSharp": { + "type": "CentralTransitive", + "requested": "[1.7.0, )", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, "ArcForges.Contracts.PublicApi": { "type": "CentralTransitive", "requested": "[1.0.0-ci.287.1, )", diff --git a/tests/ArcForges.Web.Operations.Tests/packages.lock.json b/tests/ArcForges.Web.Operations.Tests/packages.lock.json index 0319c08..0145148 100644 --- a/tests/ArcForges.Web.Operations.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Operations.Tests/packages.lock.json @@ -39,11 +39,6 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, - "AngleSharp": { - "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" - }, "AngleSharp.Css": { "type": "Transitive", "resolved": "1.0.1", @@ -464,6 +459,12 @@ "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" } }, + "AngleSharp": { + "type": "CentralTransitive", + "requested": "[1.7.0, )", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, "ArcForges.Contracts.PublicApi": { "type": "CentralTransitive", "requested": "[1.0.0-ci.287.1, )", diff --git a/tests/ArcForges.Web.Ui.Tests/packages.lock.json b/tests/ArcForges.Web.Ui.Tests/packages.lock.json index 9e40c11..5fea4f9 100644 --- a/tests/ArcForges.Web.Ui.Tests/packages.lock.json +++ b/tests/ArcForges.Web.Ui.Tests/packages.lock.json @@ -39,11 +39,6 @@ "xunit.v3.core.mtp-v2": "[4.0.1]" } }, - "AngleSharp": { - "type": "Transitive", - "resolved": "1.7.0", - "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" - }, "AngleSharp.Css": { "type": "Transitive", "resolved": "1.0.1", @@ -457,6 +452,12 @@ "ArcForges.Contracts.PublicApi": "[1.0.0-ci.287.1, )" } }, + "AngleSharp": { + "type": "CentralTransitive", + "requested": "[1.7.0, )", + "resolved": "1.7.0", + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==" + }, "ArcForges.Contracts.PublicApi": { "type": "CentralTransitive", "requested": "[1.0.0-ci.287.1, )", From f6c26108c5905ef261ff2c52ce57db7d2d7a2672 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 06:11:48 +0100 Subject: [PATCH 27/38] [WEB.40] Fix the parity anchor check and the axe result counts in the browser tests (U6, fix) - SiteParityTests: the anchor href check read Lines[0], which is the html element because the parser wraps the anchor in html, head and body. It now checks the anchor's own line for the verbatim href and that no stylesheet placeholder applies to a non-stylesheet href. The parity rule is unchanged. - LocalAccessibilityBrowserTests: Deque.AxeCore.Commons 4.13.0 exposes AxeResult.Violations, Passes, Incomplete, Inapplicable and AxeResultNode.Nodes as arrays, so .Count bound to the LINQ method group (CS1503, CS8978, CS0019). These now use .Length. Co-Authored-By: Claude Haiku 5.5 --- tests/ArcForges.Web.Site.Tests/SiteParityTests.cs | 4 +++- .../LocalAccessibilityBrowserTests.cs | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs b/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs index 806449f..d4dcca7 100644 --- a/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs +++ b/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs @@ -109,8 +109,10 @@ public void OnlyAHashedStylesheetHrefIsAPlaceholderAndOtherHrefsAreCompared() Assert.Equal(reference.Lines, candidate.Lines); Assert.Contains(reference.Lines, line => line.Contains(HtmlNormaliser.StylesheetPlaceholder, StringComparison.Ordinal)); + // The anchor is nested under , so it is not Lines[0]; its own line must keep the href verbatim. var anchor = HtmlNormaliser.Normalise("download"); - Assert.Contains("href=\"/assets/root-BPtceTrQ.css\"", anchor.Lines[0], StringComparison.Ordinal); + Assert.Contains("", anchor.Lines); + Assert.DoesNotContain(anchor.Lines, line => line.Contains(HtmlNormaliser.StylesheetPlaceholder, StringComparison.Ordinal)); } [Fact] diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs index 5750da9..4f24a90 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs @@ -280,19 +280,20 @@ await route.FulfillAsync(new RouteFulfillOptions /// Runs axe-core on the page under test and records the result. Every violation fails the screen. internal async Task AssertAxeAsync(string screen, string state) { + // Deque.AxeCore.Commons.AxeResult exposes Violations, Passes, Incomplete, Inapplicable and AxeResultNode.Nodes as arrays. var result = await Page.RunAxe(); var violations = result.Violations ?? []; var line = new StringBuilder() .Append(screen).Append(" | ").Append(state) .Append(" | url ").Append(new Uri(result.Url).AbsolutePath) - .Append(" | violations ").Append(violations.Count) - .Append(" | passes ").Append(result.Passes?.Count ?? 0) - .Append(" | incomplete ").Append(result.Incomplete?.Count ?? 0) - .Append(" | inapplicable ").Append(result.Inapplicable?.Count ?? 0); + .Append(" | violations ").Append(violations.Length) + .Append(" | passes ").Append(result.Passes?.Length ?? 0) + .Append(" | incomplete ").Append(result.Incomplete?.Length ?? 0) + .Append(" | inapplicable ").Append(result.Inapplicable?.Length ?? 0); foreach (var violation in violations) - line.Append(" | ").Append(violation.Id).Append(" (").Append(violation.Impact).Append(", ").Append(violation.Nodes.Count).Append(" nodes)"); + line.Append(" | ").Append(violation.Id).Append(" (").Append(violation.Impact).Append(", ").Append(violation.Nodes.Length).Append(" nodes)"); Record(line.ToString()); - Assert.True(violations.Count == 0, $"axe found violations on {screen} ({state}): {line}"); + Assert.True(violations.Length == 0, $"axe found violations on {screen} ({state}): {line}"); } private static void Record(string line) From 0781769041c7382c811c915a69ba04547719ee8b Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 06:28:49 +0100 Subject: [PATCH 28/38] [WEB.40] Match the React stylesheet pair in the parity file-set gate and repair the r12 NuGet contentHash rows (successor admission web-40-admission-r13) - SiteParityTests: the React stylesheet named by the prerender and the candidate's content-hashed stylesheet are excused on their own side only, through CompareFileSets. Every other file-set rule stays strict: the reference-only files must be exactly the React runtime files and the candidate may have no other file. An unmatched pair fails, and the pair bytes are still compared. Four unit tests cover the pair handling. - web-40-admission-r13 (successor of r12, reviewer w-deku-20261008-rev-web-40): the contentHash of Deque.AxeCore.Commons, Deque.AxeCore.Playwright, Newtonsoft.Json and System.IO.Abstractions is the restored lock value (confirmed against .nupkg.metadata), not the .nupkg.sha512 value r12 recorded. Three stale bound lock digests (App.Tests, Operations.Tests, Ui.Tests) are re-bound. dependency-policy.json points at r13. - eng/provenance/files.json: r13 is classified; the entry for docs/web-40-accessibility.md is removed because the file was never committed (the WCAG 2.2 AA per-screen record is still to be written and must be re-listed with it). Checks run: SiteParityTests (10 passed, parity reference set, 0 skipped); NuGetClosureAdmissionTests.RestoredClosureMatchesTheAdmittedRecords (passed); node tooling/dependency-policy.ts (passed); provenance audit (passed); node --test tooling/dependency-policy.test.ts (15 passed). Co-Authored-By: Claude Haiku 5.5 --- eng/policy/dependency-policy.json | 26 +- .../web-40-admission-r13.json | 797 ++++++++++++++++++ eng/provenance/files.json | 2 +- .../SiteParityTests.cs | 113 ++- 4 files changed, 911 insertions(+), 27 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r13.json diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 71ad156..dbc0866 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -29,9 +29,9 @@ "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", - "tests/ArcForges.Web.App.Tests/packages.lock.json": "ac5bab6c098c95abb7b91c56821d12c3f8e8d6a109d854d7a8cbaba86a8e0823", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "5939781e73182199cd09e6bf6baf91a74bf2111e67a2379c073afdcb53ba75ff", "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", - "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "c79f3d29ae3c94cd9e1478cf16514e5223689d47fd6252c6e8aafdee8cc57075", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "e7446b75088edc5a49877ec5b4927c0d7bf65e523a1cc76a8c626a547bc7917b", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", @@ -39,7 +39,7 @@ "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", - "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "af03c905cf03a946129d821708c500812e630358f09ba7ba59cec72b658b3ff8", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "482a8749cf737188e1106d77c1d657ed5c1c9626317be6e337c59e2b3e46793b", "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", @@ -50,7 +50,7 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-09", - "rationale": "WEB.40 successor of web-40-admission-r11 for the parity, focus and accessibility re-proof unit U6 (2026-10-09 adjudication). Test-only additions: Deque.AxeCore.Playwright 4.13.0 (MIT) with its dependencies Deque.AxeCore.Commons 4.13.0 (MPL-2.0, the axe-core script carrier, admitted only as testOnly), Newtonsoft.Json 13.0.1 (MIT) and System.IO.Abstractions 17.0.24 (MIT). They are referenced only by tests/browser (local opt-in Microsoft.Playwright suite, never built or restored in CI); axe-core is injected only into the page under test. AngleSharp 1.7.0 (already admitted, MIT) is now also a direct reference of tests/ArcForges.Web.Site.Tests for the normalised-DOM parity gate. The Chat Cancel change and the parity tooling add no package. Inputs re-bound: Directory.Packages.props, tests/ArcForges.Web.Site.Tests csproj and packages.lock.json, tests/browser/ArcForges.Web.Browser.Tests csproj and packages.lock.json. Every npm coordinate and every other input is unchanged from web-40-admission-r11. The U5 closure rationale of web-40-admission-r11 still applies.", + "rationale": "WEB.40 successor of web-40-admission-r12, repairing the U6 admission after the coordinator's validation at f6c2610. (1) Four r12 rows recorded the SHA-512 of the .nupkg file (the .nupkg.sha512 value) as contentHash: Deque.AxeCore.Commons 4.13.0, Deque.AxeCore.Playwright 4.13.0, Newtonsoft.Json 13.0.1 and System.IO.Abstractions 17.0.24. The packages.lock.json of tests/browser/ArcForges.Web.Browser.Tests records a different value for each, every restored package's .nupkg.metadata repeats it, and every row admitted before r12 uses that lock and metadata value; NuGetClosureAdmissionTests compares the admitted record with the lock. Each of the four is now the restored lock value, read by script and cross-checked against .nupkg.metadata. No package, version, source, licence, nuspecSha256 or test-only class changed. Newtonsoft.Json 13.0.1 was restored from the machine-wide SDK package folder (C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages), not from api.nuget.org, so the hosted locked restore against nuget.org is the authority that confirms its value. (2) The packages.lock.json files of tests/ArcForges.Web.App.Tests, tests/ArcForges.Web.Operations.Tests and tests/ArcForges.Web.Ui.Tests were refreshed by 01246e8 (the centrally pinned AngleSharp 1.7.0, no new package) after r12 was committed, so their recorded digests were stale; each is re-bound to its current digest. Every other bound input digest was recomputed and is current. No npm coordinate changed.", "inputs": { ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", @@ -75,9 +75,9 @@ "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", - "tests/ArcForges.Web.App.Tests/packages.lock.json": "ac5bab6c098c95abb7b91c56821d12c3f8e8d6a109d854d7a8cbaba86a8e0823", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "5939781e73182199cd09e6bf6baf91a74bf2111e67a2379c073afdcb53ba75ff", "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", - "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "c79f3d29ae3c94cd9e1478cf16514e5223689d47fd6252c6e8aafdee8cc57075", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "e7446b75088edc5a49877ec5b4927c0d7bf65e523a1cc76a8c626a547bc7917b", "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", @@ -85,7 +85,7 @@ "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", - "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "af03c905cf03a946129d821708c500812e630358f09ba7ba59cec72b658b3ff8", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "482a8749cf737188e1106d77c1d657ed5c1c9626317be6e337c59e2b3e46793b", "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", @@ -103,7 +103,7 @@ ], "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site; the deploy job ships the same sealed bytes under the unchanged main-push guards. No hosted browser, device, macOS or live-service check is added.", "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", - "previous": "eng/policy/dependency-reviews/web-40-admission-r11.json", + "previous": "eng/policy/dependency-reviews/web-40-admission-r12.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -811,7 +811,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r12.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r13.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", @@ -877,7 +877,7 @@ "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." }, "deque.axecore.commons/4.13.0": { - "contentHash": "s+pmv/W8wJ9EXIJTbqqxFDjzsH78JC545k3J0n8Bdb7/dG8naE1Hi5MZWCU/p/0yHaDJWP6FFFVk2I6/frttVg==", + "contentHash": "roq2z6OmKY4KVXGj1lSigUbjE7DnIcYLqPOVcnWrusS6CI7oB6/He8Oo3ZO/TCOlnaTS1hkJOuKPwL+PyhyC6w==", "licence": "MPL-2.0", "testOnly": true, "nuspecSha256": "99c3d40cc4f4e0bf3cbcc7c272810116663adc8e0da00c6d3db038f8c09c8ce3", @@ -885,7 +885,7 @@ "classification": "Test-only axe-core script carrier (MPL-2.0 is recorded by the WEB.40 2026-10-09 adjudication). File-level weak copyleft, accepted only for a row marked testOnly: no product project restores it, and it is never shipped or compiled into a product output. Referenced only through Deque.AxeCore.Playwright by tests/browser." }, "deque.axecore.playwright/4.13.0": { - "contentHash": "Ltn/cHlelruijCvoKxYMKYpd58uZFeMsYNEdvTEWt+nEkMfbfNboPVhOPj0MZ0XoLpzLOhUutHIM/oPjRKJnFw==", + "contentHash": "tqzqVEsQfVg4fmx9/AkbgYjVMYHHOGI7WkTznKuP8ayi0uaBcd+2skOfxnVdUBlEhpHdu9stzxOZ6uPVhIS2JA==", "licence": "MIT", "nuspecSha256": "a57f77f0d8101e573f4dc7194387e9e8356a05445e403ac5558bd556332e2c99", "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.playwright/4.13.0/deque.axecore.playwright.nuspec", @@ -1228,7 +1228,7 @@ "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." }, "newtonsoft.json/13.0.1": { - "contentHash": "g3MbZi6vBTeaI/hEbvR7vBETSd1DWLe9i1E4P+nPY34v5i94zqUqDXvdWC3G+7tYN9SnsdU9zzegrnRz4h7nsQ==", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", "licence": "MIT", "nuspecSha256": "86ae2dca2b50badbad00c2f115cc268c8c4a3f42d0018d0576a0b4c20be6eb11", "source": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", @@ -1242,7 +1242,7 @@ "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." }, "system.io.abstractions/17.0.24": { - "contentHash": "O0ASsrpvPWW3bBT3v7HbauwiD/0KliKnXkt+T5pWR7/vZad2+EOWX+klsbJubvCgmpuBkVZgr5ayI0FRkgPPJw==", + "contentHash": "hA7bacntMiZv1Yf9xtjwl/GP3GT1mG84QxhAk7ijAUD0pJhJaVVwXScE13vMpXnNtlaRDW6SeyZdWg2j2qrh4w==", "licence": "MIT", "nuspecSha256": "5de77d739386b194c179ed5c4fc8e39e1362552a560f34ac6fb9daa4bafadf22", "source": "https://api.nuget.org/v3-flatcontainer/system.io.abstractions/17.0.24/system.io.abstractions.nuspec", diff --git a/eng/policy/dependency-reviews/web-40-admission-r13.json b/eng/policy/dependency-reviews/web-40-admission-r13.json new file mode 100644 index 0000000..62fb4f7 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r13.json @@ -0,0 +1,797 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-09", + "rationale": "WEB.40 successor of web-40-admission-r12, repairing the U6 admission after the coordinator's validation at f6c2610. (1) Four r12 rows recorded the SHA-512 of the .nupkg file (the .nupkg.sha512 value) as contentHash: Deque.AxeCore.Commons 4.13.0, Deque.AxeCore.Playwright 4.13.0, Newtonsoft.Json 13.0.1 and System.IO.Abstractions 17.0.24. The packages.lock.json of tests/browser/ArcForges.Web.Browser.Tests records a different value for each, every restored package's .nupkg.metadata repeats it, and every row admitted before r12 uses that lock and metadata value; NuGetClosureAdmissionTests compares the admitted record with the lock. Each of the four is now the restored lock value, read by script and cross-checked against .nupkg.metadata. No package, version, source, licence, nuspecSha256 or test-only class changed. Newtonsoft.Json 13.0.1 was restored from the machine-wide SDK package folder (C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages), not from api.nuget.org, so the hosted locked restore against nuget.org is the authority that confirms its value. (2) The packages.lock.json files of tests/ArcForges.Web.App.Tests, tests/ArcForges.Web.Operations.Tests and tests/ArcForges.Web.Ui.Tests were refreshed by 01246e8 (the centrally pinned AngleSharp 1.7.0, no new package) after r12 was committed, so their recorded digests were stale; each is re-bound to its current digest. Every other bound input digest was recomputed and is current. No npm coordinate changed.", + "inputs": { + ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "95e73db26cdb9ba15ccd3c752d804be4ee083f85fdf589acf837121a2c3975b7", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "eng/provenance/records/browser-resources-r12.json": "6b470604b64d692d0eaa215be133506af065856dde0fde9ee8e702bba883bb88", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "392b8874131b713ac07ab595ff541122fa3c2317f9ba81b34ffe6115f4363825", + "package.json": "60b8b8e853ae2ac0dfec58831d61c717b3e4fb26ed3848745b36e9a2ec370ea5", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "6ae92b481b6a892a9b4952f7e4b8da56b7caac8f6a6206151b017108e2c99229", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "75bd9b2ebbe6ed307c134473be0319fd536a7733ce00fe1237148299e5f59239", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "61d7150f179a77464386591f7a8c38cdbadb95b9c29fff18fe2ef912bef91931", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "5939781e73182199cd09e6bf6baf91a74bf2111e67a2379c073afdcb53ba75ff", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "e7446b75088edc5a49877ec5b4927c0d7bf65e523a1cc76a8c626a547bc7917b", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "15eb52424732fb8bf0a7479acf0c323b9a36c7e1503c3369bf92f402319b55e0", + "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", + "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "482a8749cf737188e1106d77c1d657ed5c1c9626317be6e337c59e2b3e46793b", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", + "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site; the deploy job ships the same sealed bytes under the unchanged main-push guards. No hosted browser, device, macOS or live-service check is added.", + "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r12.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r12.json", + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0ef2077c80cb51d09e4d5f5f48199ded191ee51353ac9821bd965d411be536a8" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f8c8f57805316d94f4590b7668827055a208ff38d69104721cba792bab40173a" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "fc6785f57d753800750bc9f6207cdd762099662d0a057693443a8918817cab2a" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "ef2704544d2bb1e42794150f2f88eb142618b5f5482517627d003b5d2c2cb952" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Test-only HTML5 parser: a direct reference of tests/ArcForges.Web.Site.Tests (the WEB.40 normalised-DOM parity gate, 2026-10-09 adjudication) and a transitive dependency of the bUnit closure. Reached only from tests/**; never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "7cb911a9fc3a5b32b4ddd44a333673408a742d0333f1714cdd3a61d7f67c3f2b", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "da69e059a761e3502214fe0b170800a6964186ff522d6cb244f1a713d0c46cd4", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "deque.axecore.commons/4.13.0": { + "contentHash": "roq2z6OmKY4KVXGj1lSigUbjE7DnIcYLqPOVcnWrusS6CI7oB6/He8Oo3ZO/TCOlnaTS1hkJOuKPwL+PyhyC6w==", + "licence": "MPL-2.0", + "testOnly": true, + "nuspecSha256": "99c3d40cc4f4e0bf3cbcc7c272810116663adc8e0da00c6d3db038f8c09c8ce3", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.commons/4.13.0/deque.axecore.commons.nuspec", + "classification": "Test-only axe-core script carrier (MPL-2.0 is recorded by the WEB.40 2026-10-09 adjudication). File-level weak copyleft, accepted only for a row marked testOnly: no product project restores it, and it is never shipped or compiled into a product output. Referenced only through Deque.AxeCore.Playwright by tests/browser." + }, + "deque.axecore.playwright/4.13.0": { + "contentHash": "tqzqVEsQfVg4fmx9/AkbgYjVMYHHOGI7WkTznKuP8ayi0uaBcd+2skOfxnVdUBlEhpHdu9stzxOZ6uPVhIS2JA==", + "licence": "MIT", + "nuspecSha256": "a57f77f0d8101e573f4dc7194387e9e8356a05445e403ac5558bd556332e2c99", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.playwright/4.13.0/deque.axecore.playwright.nuspec", + "classification": "Test-only accessibility package (WEB.40 P2-021 item 8): axe-core run through Microsoft.Playwright for .NET. Referenced only by tests/browser (local opt-in, never built in CI). axe-core is injected only into the page under test. Its Microsoft.Playwright 1.20.2 dependency resolves to the centrally pinned 1.62.0." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "9c4fe2bee0274bf65414c76277d456990c1047c4195a45bac29ba1cad35e2ed6", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "875f251262c6f7a61b4c9ded5ce938c716762a96cd11ae2193cb070b485a3358", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "62c5b29511b8f69f5de0b390e8ffbf1aeab20f3747269e08a674fdf5876a4d74", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "ba560eff82ade11b27b2f68c174eb23463ef4a059e6421919b1db39d40a1d570", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "9e75b5ea81a62fbc0158e8f0c6ccca578da92a1cb4dd0315471584d74aaa39e6", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "b5ceac406258d39d82df832a251aa1c81ad98d224e5f69850ff58ba4ac86ae4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "ad3d9f56c09fde38542a194d8f3b40f118b58669f3bf801b2e53c16fad818d8e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "newtonsoft.json/13.0.1": { + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", + "licence": "MIT", + "nuspecSha256": "86ae2dca2b50badbad00c2f115cc268c8c4a3f42d0018d0576a0b4c20be6eb11", + "source": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", + "classification": "Transitive MIT JSON library of Deque.AxeCore.Commons, test-only: reached only from tests/browser (local opt-in). No product project references it; the architecture policy refuses its use in product C# source." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "c2bddc6ea14dfd6d55e3c291330bce618d281d80b5eef5492fc7e531bf0d0f59", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.io.abstractions/17.0.24": { + "contentHash": "hA7bacntMiZv1Yf9xtjwl/GP3GT1mG84QxhAk7ijAUD0pJhJaVVwXScE13vMpXnNtlaRDW6SeyZdWg2j2qrh4w==", + "licence": "MIT", + "nuspecSha256": "5de77d739386b194c179ed5c4fc8e39e1362552a560f34ac6fb9daa4bafadf22", + "source": "https://api.nuget.org/v3-flatcontainer/system.io.abstractions/17.0.24/system.io.abstractions.nuspec", + "classification": "Transitive MIT file-system abstraction of Deque.AxeCore.Playwright, test-only: reached only from tests/browser (local opt-in). Its net6.0 dependency group is empty, so no further package is restored for net10.0." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "7f67c90b0f601e40b7235d0e44638f9648406b7c098f7d35065c589fa2bef5e2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "0735e7f174fa8156accf653e8e2dd4fd1c9dc5f9184dd5ef8f322e80c020f7ae", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/eng/provenance/files.json b/eng/provenance/files.json index 9affae6..91669fa 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -23,8 +23,8 @@ "Directory.Build.props", "Directory.Build.targets", "Directory.Packages.props", - "docs/web-40-accessibility.md", "eng/policy/dependency-reviews/web-40-admission-r12.json", + "eng/policy/dependency-reviews/web-40-admission-r13.json", "NuGet.config", "README.md", "SECURITY.md", diff --git a/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs b/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs index d4dcca7..2f464be 100644 --- a/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs +++ b/tests/ArcForges.Web.Site.Tests/SiteParityTests.cs @@ -128,6 +128,61 @@ public void TheOnlyContentSecurityPolicyDifferenceIsTheReactScriptHashes() Assert.NotEmpty(HeaderDifferences([reference], [" Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'"])); } + [Fact] + public void AMatchedStylesheetPairIsExcusedOnItsOwnSidesAndNothingElseIs() + { + var reference = new[] { "404.css", "__spa-fallback.html", "assets/entry.client-C6nGypqo.js", "assets/root-BPtceTrQ.css", "index.html" }; + var candidate = new[] { "404.css", "assets/site.d4912dbd6dc5e22b.css", "index.html" }; + + var comparison = CompareFileSets(reference, candidate, "assets/root-BPtceTrQ.css", "assets/site.d4912dbd6dc5e22b.css"); + + Assert.Empty(comparison.Failures); + Assert.Equal(new[] { "__spa-fallback.html", "assets/entry.client-C6nGypqo.js" }, comparison.ReferenceOnly); + Assert.Empty(comparison.CandidateOnly); + } + + [Fact] + public void AnUnmatchedStylesheetStillFailsOnEitherSide() + { + var reference = new[] { "assets/root-BPtceTrQ.css", "index.html" }; + var candidate = new[] { "assets/site.d4912dbd6dc5e22b.css", "index.html" }; + + // The React page names a stylesheet that the reference does not hold, so no pair is matched. + var unnamed = CompareFileSets(reference, candidate, "assets/missing.css", "assets/site.d4912dbd6dc5e22b.css"); + Assert.Contains(unnamed.Failures, failure => failure.Contains("is not a reference file", StringComparison.Ordinal)); + + // The candidate names a stylesheet that it does not hold. + var absent = CompareFileSets(reference, candidate, "assets/root-BPtceTrQ.css", "assets/site.nothere.css"); + Assert.Contains(absent.Failures, failure => failure.Contains("has no stylesheet file", StringComparison.Ordinal)); + + // The candidate has a second stylesheet that no reference stylesheet pairs with. + var extra = new[] { "assets/extra.css", "assets/site.d4912dbd6dc5e22b.css", "index.html" }; + var second = CompareFileSets(reference, extra, "assets/root-BPtceTrQ.css", "assets/site.d4912dbd6dc5e22b.css"); + Assert.Equal(new[] { "assets/extra.css" }, second.CandidateOnly); + Assert.Contains("The candidate has files the reference does not.", second.Failures); + } + + [Fact] + public void AReferenceStylesheetThatIsNotTheMatchedPairStillFailsTheRuntimeRule() + { + var reference = new[] { "__spa-fallback.html", "assets/entry.client-C6nGypqo.js", "assets/root-BPtceTrQ.css", "assets/unpaired.css", "index.html" }; + var candidate = new[] { "assets/site.d4912dbd6dc5e22b.css", "index.html" }; + + var comparison = CompareFileSets(reference, candidate, "assets/root-BPtceTrQ.css", "assets/site.d4912dbd6dc5e22b.css"); + + Assert.Equal(new[] { "__spa-fallback.html", "assets/entry.client-C6nGypqo.js", "assets/unpaired.css" }, comparison.ReferenceOnly); + Assert.Contains("The reference-only files are not exactly the React runtime files.", comparison.Failures); + } + + [Fact] + public void ANonIdenticalStylesheetPairIsNotTheSameBytes() + { + // The file-set check excuses the pair's names only; the bytes of the pair are compared by SameBytes. + Assert.True(SameBytes(Encoding.UTF8.GetBytes("a{color:red}"), Encoding.UTF8.GetBytes("a{color:red}"))); + Assert.False(SameBytes(Encoding.UTF8.GetBytes("a{color:red}"), Encoding.UTF8.GetBytes("a{color:blue}"))); + Assert.False(SameBytes(Encoding.UTF8.GetBytes("a{color:red}"), null)); + } + [Fact] public async Task TheCSharpSiteAgreesWithTheRecordedReactPrerenderWhenItIsNamed() { @@ -142,7 +197,8 @@ public async Task TheCSharpSiteAgreesWithTheRecordedReactPrerenderWhenItIsNamed( var failures = new List(); // File inventory. The reference-only files are the React runtime (hydration bundles and the SPA fallback). The candidate - // has no file the reference lacks; its content-hashed stylesheet is paired with the reference stylesheet below. + // has no file the reference lacks, except its content-hashed stylesheet, which is one matched pair with the reference + // stylesheet named by the React page. The pair is excused on its own side only; its bytes are compared below. var referenceFiles = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories) .Select(path => Path.GetRelativePath(root, path).Replace('\\', '/')) .OrderBy(path => path, StringComparer.Ordinal) @@ -150,19 +206,12 @@ public async Task TheCSharpSiteAgreesWithTheRecordedReactPrerenderWhenItIsNamed( var candidateFiles = site.Files.Select(file => file.Path).OrderBy(path => path, StringComparer.Ordinal).ToList(); var stylesheet = ReferenceStylesheet.Match(File.ReadAllText(Path.Combine(root, "index.html"), Encoding.UTF8)).Groups[1].Value; var candidateStylesheet = Assert.Single(candidateFiles, path => path.StartsWith("assets/site.", StringComparison.Ordinal)); - var referenceOnly = referenceFiles.Except(candidateFiles, StringComparer.Ordinal).ToList(); - var candidateOnly = candidateFiles.Except(referenceFiles, StringComparer.Ordinal).ToList(); - var expectedReferenceOnly = referenceFiles - .Where(path => path == "__spa-fallback.html" || (path.StartsWith("assets/", StringComparison.Ordinal) && path.EndsWith(".js", StringComparison.Ordinal))) - .ToList(); + var inventory = CompareFileSets(referenceFiles, candidateFiles, stylesheet, candidateStylesheet); report.AppendLine($"Reference files: {referenceFiles.Count}. Candidate files: {candidateFiles.Count}."); - report.AppendLine($"Reference-only (React runtime, intended): {string.Join(", ", referenceOnly)}"); - report.AppendLine($"Candidate-only: {(candidateOnly.Count == 0 ? "none" : string.Join(", ", candidateOnly))}"); + report.AppendLine($"Reference-only (React runtime, intended): {string.Join(", ", inventory.ReferenceOnly)}"); + report.AppendLine($"Candidate-only: {(inventory.CandidateOnly.Count == 0 ? "none" : string.Join(", ", inventory.CandidateOnly))}"); report.AppendLine($"Stylesheet pair: {stylesheet} and {candidateStylesheet}."); - if (!referenceOnly.SequenceEqual(expectedReferenceOnly, StringComparer.Ordinal)) - failures.Add("The reference-only files are not exactly the React runtime files."); - if (candidateOnly.Count > 0) - failures.Add("The candidate has files the reference does not."); + failures.AddRange(inventory.Failures); // HTML pages: each page is a document, and its normalised structure must be equal. foreach (var path in HtmlPaths) @@ -212,7 +261,45 @@ public async Task TheCSharpSiteAgreesWithTheRecordedReactPrerenderWhenItIsNamed( Assert.True(failures.Count == 0, text + string.Join(Environment.NewLine, failures)); } - private static bool SameBytes(byte[] reference, byte[]? candidate) => + /// The file-set verdict of the parity gate: what remains on each side once the matched stylesheet pair is excused. + internal sealed record FileSetComparison(IReadOnlyList ReferenceOnly, IReadOnlyList CandidateOnly, IReadOnlyList Failures); + + /// + /// Compares two sorted file inventories. The stylesheet the React page names and the candidate's content-hashed stylesheet + /// are one matched pair, excused on their own side only, and only when both files exist. Everything else stays strict: the + /// reference-only files must be exactly the React runtime files, and the candidate may have no other file the reference lacks. + /// + internal static FileSetComparison CompareFileSets( + IReadOnlyList referenceFiles, + IReadOnlyList candidateFiles, + string referenceStylesheet, + string candidateStylesheet) + { + var failures = new List(); + if (!referenceFiles.Contains(referenceStylesheet, StringComparer.Ordinal)) + failures.Add($"The React page names a stylesheet that is not a reference file: '{referenceStylesheet}'."); + if (!candidateFiles.Contains(candidateStylesheet, StringComparer.Ordinal)) + failures.Add($"The candidate has no stylesheet file '{candidateStylesheet}'."); + + var referenceOnly = referenceFiles + .Except(candidateFiles, StringComparer.Ordinal) + .Where(path => !string.Equals(path, referenceStylesheet, StringComparison.Ordinal)) + .ToList(); + var candidateOnly = candidateFiles + .Except(referenceFiles, StringComparer.Ordinal) + .Where(path => !string.Equals(path, candidateStylesheet, StringComparison.Ordinal)) + .ToList(); + var expectedReferenceOnly = referenceFiles + .Where(path => path == "__spa-fallback.html" || (path.StartsWith("assets/", StringComparison.Ordinal) && path.EndsWith(".js", StringComparison.Ordinal))) + .ToList(); + if (!referenceOnly.SequenceEqual(expectedReferenceOnly, StringComparer.Ordinal)) + failures.Add("The reference-only files are not exactly the React runtime files."); + if (candidateOnly.Count > 0) + failures.Add("The candidate has files the reference does not."); + return new FileSetComparison(referenceOnly, candidateOnly, failures); + } + + internal static bool SameBytes(byte[] reference, byte[]? candidate) => candidate is not null && SHA256.HashData(reference).AsSpan().SequenceEqual(SHA256.HashData(candidate)); /// The header lines that differ, after the React script hashes are removed from its policy. From 49f72bfca56ba7ebfe11601d7fd576cf52d2b6fd Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 07:03:40 +0100 Subject: [PATCH 29/38] [WEB.40] Make the error banner Dismiss a named keyboard button, raise App input boundaries to 3:1, and name the axe rules per screen (U-fix) - app.css: .field input and .input-row input borders move from #b9c1b4 (1.85:1 on white, 1.66:1 on the page) and #a3b1a4 (2.20:1 on the field fill) to #6b7a72 (4.51:1 on white, 4.44:1 on #fffdf7, 4.06:1 on the page). The disabled button fill #6b7a72 (4.06:1 on the page) and the focus outline #235844 (7.41:1 on the page, 7.43:1 on the error banner) already met 3:1 and are unchanged. - index.html: the unhandled-error Dismiss is diff --git a/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs b/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs new file mode 100644 index 0000000..4054a66 --- /dev/null +++ b/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// WCAG 2.2 AA checks of the App host page and stylesheet (WEB.40 accessibility re-proof, 2026-10-09): the unhandled-error +// banner's Dismiss is a named keyboard button (2.1.1, 4.1.2), no anchor without an href acts as a control, and the +// user-interface boundaries, the disabled-state fill and the focus indicators reach 3:1 against their adjacent colours +// (1.4.11). The host page and stylesheet are read from the source tree. +using System.Globalization; +using System.Text.RegularExpressions; +using AngleSharp.Html.Parser; +using Xunit; + +namespace ArcForges.Web.App.Tests; + +public sealed class HostPageAccessibilityTests +{ + private const string Paper = "#f4f3ed"; + private const string White = "#ffffff"; + private const string FieldFill = "#fffdf7"; + private const string BannerFill = "#fff3cd"; + private static readonly string Root = RepositoryRoot.Find(); + + private static string AppSource(string relative) => File.ReadAllText(Path.Combine(Root, "src", "ArcForges.Web.App", relative)); + + [Fact] + public void TheErrorBannerDismissIsANamedKeyboardButton() + { + var document = new HtmlParser().ParseDocument(AppSource("wwwroot/index.html")); + var dismiss = Assert.Single(document.QuerySelectorAll("#blazor-error-ui .dismiss")); + Assert.Equal("button", dismiss.LocalName); + Assert.Equal("button", dismiss.GetAttribute("type")); + Assert.Equal("Dismiss", dismiss.GetAttribute("aria-label")); + Assert.False(dismiss.HasAttribute("tabindex")); + } + + [Fact] + public void NoAnchorWithoutAnHrefIsLeftInTheHostPage() + { + var document = new HtmlParser().ParseDocument(AppSource("wwwroot/index.html")); + Assert.Empty(document.QuerySelectorAll("a:not([href])")); + } + + [Theory] + [InlineData(".field input", "border", White, "text input boundary on the white field")] + [InlineData(".field input", "border", Paper, "text input boundary on the page")] + [InlineData(".input-row input", "border", FieldFill, "text input boundary on the field fill")] + [InlineData(".input-row input", "border", Paper, "text input boundary on the page")] + [InlineData(".button[aria-disabled=\"true\"]", "background", Paper, "disabled button fill on the page")] + [InlineData(".button", "background", Paper, "button fill on the page")] + public void ComponentBoundariesAndStateFillsReachThreeToOne(string selector, string property, string adjacent, string what) + { + var colour = DeclaredColour(AppSource("wwwroot/app.css"), selector, property); + Assert.True( + Contrast.Ratio(colour, adjacent) >= 3.0, + $"{what}: {selector} {property} {colour} against {adjacent} is {Contrast.Ratio(colour, adjacent):0.00}:1"); + } + + [Theory] + [InlineData(Paper, "focus indicator on the page")] + [InlineData(BannerFill, "focus indicator on the error banner")] + public void TheFocusIndicatorReachesThreeToOne(string adjacent, string what) + { + var css = AppSource("wwwroot/app.css"); + var match = Regex.Match(css, @":focus-visible\s*\{[^}]*outline:\s*\d+px solid (#[0-9a-fA-F]{6})", RegexOptions.Singleline); + Assert.True(match.Success, "The :focus-visible outline is not declared."); + var colour = match.Groups[1].Value; + Assert.True(Contrast.Ratio(colour, adjacent) >= 3.0, $"{what}: {colour} against {adjacent} is {Contrast.Ratio(colour, adjacent):0.00}:1"); + } + + /// The first declaration of a property in the block of an exact selector, as a six-digit hex colour. + private static string DeclaredColour(string css, string selector, string property) + { + // The selector may head a selector list ("a, a:hover { ... }"); the first block whose list starts with it is the one read. + var block = Regex.Match(css, @"(?[^}]*)\}"); + Assert.True(block.Success, $"The selector {selector} is not declared."); + var declaration = Regex.Match(block.Groups["body"].Value, @"(?The WCAG 2.x contrast ratio of two six-digit hex colours.
+ private static class Contrast + { + internal static double Ratio(string first, string second) + { + var a = Luminance(first); + var b = Luminance(second); + return (Math.Max(a, b) + 0.05) / (Math.Min(a, b) + 0.05); + } + + private static double Luminance(string hex) + { + double Channel(int offset) + { + var value = int.Parse(hex.AsSpan(offset, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture) / 255.0; + return value <= 0.03928 ? value / 12.92 : Math.Pow((value + 0.055) / 1.055, 2.4); + } + + return 0.2126 * Channel(1) + 0.7152 * Channel(3) + 0.0722 * Channel(5); + } + } +} diff --git a/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs b/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs index 4f24a90..6c5b59a 100644 --- a/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs +++ b/tests/browser/ArcForges.Web.Browser.Tests/LocalAccessibilityBrowserTests.cs @@ -139,6 +139,48 @@ public async Task ChatAfterReplyPassesAxe() await session.AssertAxeAsync("Chat", "after reply"); } + [Fact] + public async Task ChatFailedSendPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + await session.Page.RouteAsync("**/api/arcforges.hello.v1.HelloService/SayHello", route => route.FulfillAsync(new RouteFulfillOptions { Status = 503, ContentType = "text/plain", Body = "unavailable" })); + await session.GotoAsync("/chat"); + await session.Page.WaitForSelectorAsync("form button[aria-disabled=false]"); + await session.Page.Locator("form button[type=submit]").ClickAsync(); + await session.Page.WaitForSelectorAsync("ol.transcript li:nth-child(2)"); + await session.AssertAxeAsync("Chat", "failed send (notice shown)"); + } + + /// + /// The unhandled-error banner (index.html). The banner is shown through Blazor's own critical-error entry point, which + /// shows #blazor-error-ui and binds the Dismiss handler. Dismiss must be a focusable button named Dismiss, and Enter on it + /// must hide the banner. + /// + [Fact] + public async Task ErrorBannerDismissIsKeyboardOperableAndPassesAxe() + { + var harness = Harness.FromEnvironment(LocalOptIn.Current(), profile: true); + if (harness is null) + Assert.Skip(SkipMessage); + await using var session = await harness.OpenAsync(); + await session.GotoAsync("/chat"); + await session.Page.WaitForSelectorAsync("form button[aria-disabled=false]"); + await session.Page.EvaluateAsync("window.Blazor._internal.dotNetCriticalError(new Error('WEB.40 accessibility banner state'))"); + var banner = session.Page.Locator("#blazor-error-ui"); + await banner.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible }); + await session.AssertAxeAsync("App error banner", "unhandled error shown"); + + var dismiss = session.Page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "Dismiss", Exact = true }); + Assert.Equal(1, await dismiss.CountAsync()); + await dismiss.FocusAsync(); + Assert.True(await dismiss.EvaluateAsync("el => el === document.activeElement && el.tagName === 'BUTTON'"), "Dismiss does not take keyboard focus."); + await session.Page.Keyboard.PressAsync("Enter"); + await banner.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Hidden }); + } + [Fact] public async Task SitePagesPassAxe() { @@ -283,19 +325,37 @@ internal async Task AssertAxeAsync(string screen, string state) // Deque.AxeCore.Commons.AxeResult exposes Violations, Passes, Incomplete, Inapplicable and AxeResultNode.Nodes as arrays. var result = await Page.RunAxe(); var violations = result.Violations ?? []; + var passes = result.Passes ?? []; + var incomplete = result.Incomplete ?? []; + var inapplicable = result.Inapplicable ?? []; var line = new StringBuilder() .Append(screen).Append(" | ").Append(state) .Append(" | url ").Append(new Uri(result.Url).AbsolutePath) .Append(" | violations ").Append(violations.Length) - .Append(" | passes ").Append(result.Passes?.Length ?? 0) - .Append(" | incomplete ").Append(result.Incomplete?.Length ?? 0) - .Append(" | inapplicable ").Append(result.Inapplicable?.Length ?? 0); + .Append(" | passes ").Append(passes.Length) + .Append(" | incomplete ").Append(incomplete.Length) + .Append(" | inapplicable ").Append(inapplicable.Length); foreach (var violation in violations) line.Append(" | ").Append(violation.Id).Append(" (").Append(violation.Impact).Append(", ").Append(violation.Nodes.Length).Append(" nodes)"); Record(line.ToString()); + // The rule-level line: the rule IDs of each result list, sorted and comma-separated ("(none)" when empty). + var rules = new StringBuilder() + .Append(screen).Append(" | ").Append(state) + .Append(" | rules passed ").Append(RuleIds(passes.Select(item => item.Id))) + .Append(" | rules incomplete ").Append(RuleIds(incomplete.Select(item => item.Id))) + .Append(" | rules inapplicable ").Append(RuleIds(inapplicable.Select(item => item.Id))) + .Append(" | rules violated ").Append(RuleIds(violations.Select(item => item.Id))); + Record(rules.ToString()); Assert.True(violations.Length == 0, $"axe found violations on {screen} ({state}): {line}"); } + /// The distinct rule IDs in ordinal order, comma-separated; "(none)" for an empty list. + private static string RuleIds(IEnumerable ids) + { + var sorted = ids.Distinct(StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + return sorted.Length == 0 ? "(none)" : string.Join(",", sorted); + } + private static void Record(string line) { var report = Environment.GetEnvironmentVariable(ReportVariable); From 73173c2cbb7b279e6583c7496f0501090ecbd601 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 07:21:07 +0100 Subject: [PATCH 30/38] [WEB.40] Put the error banner message and Reload link in a role=alert region so axe's region rule passes - index.html: the unhandled-error text sat directly in #blazor-error-ui, outside every landmark, region, live region or alert, and the Reload anchor (href="") was flagged by the axe 4.13.0 region rule once the banner text moved. The message and the Reload link are now inside , an inline element, so the banner's markup, its #blazor-error-ui selector, the .dismiss button, its fixed position and its appearance are unchanged. - HostPageAccessibilityTests: TheErrorBannerMessageIsAnAlertAndNoTextSitsDirectlyInTheBanner asserts the alert holds the message and the Reload link, that the banner has no text node of its own, and that every link in the banner sits inside the alert. Co-Authored-By: Claude Haiku 5.5 --- src/ArcForges.Web.App/wwwroot/index.html | 3 +-- .../HostPageAccessibilityTests.cs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/ArcForges.Web.App/wwwroot/index.html b/src/ArcForges.Web.App/wwwroot/index.html index 2d420e0..676cd4e 100644 --- a/src/ArcForges.Web.App/wwwroot/index.html +++ b/src/ArcForges.Web.App/wwwroot/index.html @@ -13,8 +13,7 @@
Loading…
- An unhandled error has occurred. - Reload + An unhandled error has occurred. Reload
diff --git a/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs b/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs index 4054a66..4e0ff96 100644 --- a/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs +++ b/tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs @@ -5,6 +5,7 @@ // (1.4.11). The host page and stylesheet are read from the source tree. using System.Globalization; using System.Text.RegularExpressions; +using AngleSharp.Dom; using AngleSharp.Html.Parser; using Xunit; @@ -31,6 +32,29 @@ public void TheErrorBannerDismissIsANamedKeyboardButton() Assert.False(dismiss.HasAttribute("tabindex")); } + [Fact] + public void TheErrorBannerMessageIsAnAlertAndNoTextSitsDirectlyInTheBanner() + { + // axe-core 4.13.0's region rule (WCAG 1.3.1 best practice) flags text and links outside every landmark, region, live region or + // alert (an anchor is exempt only for a same-page fragment target). role="alert" makes its content a region, so the message + // and the Reload link live inside that alert and the banner itself carries no text or link of its own. + var document = new HtmlParser().ParseDocument(AppSource("wwwroot/index.html")); + var banner = Assert.Single(document.QuerySelectorAll("#blazor-error-ui")); + var alert = Assert.Single(banner.QuerySelectorAll("[role=\"alert\"]")); + Assert.Equal("An unhandled error has occurred. Reload", Regex.Replace(alert.TextContent, @"\s+", " ").Trim()); + Assert.Single(alert.QuerySelectorAll("a.reload")); + foreach (var text in banner.ChildNodes.OfType()) + { + Assert.True( + string.IsNullOrWhiteSpace(text.Data), + $"Text \"{text.Data.Trim()}\" sits directly in #blazor-error-ui, outside the alert region."); + } + foreach (var link in banner.QuerySelectorAll("a")) + { + Assert.NotNull(link.Closest("[role=\"alert\"]")); + } + } + [Fact] public void NoAnchorWithoutAnHrefIsLeftInTheHostPage() { From 58f44c475bed3b887cbca89f40914d2a7ae2b0bd Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 07:37:50 +0100 Subject: [PATCH 31/38] [WEB.40] Record the WEB.40 accessibility re-proof at 73173c2 with per-state axe rule names, and list the record in provenance - docs/web-40-accessibility.md: 14 screen states (Account, Chat including the failed send, the App error banner, Site), each with the axe counts and the rule names that passed, were inapplicable, were incomplete and were violated (zero violations). pass (axe) cells are derived from the state's passed list; target-size and audio-caption were not evaluated by the run and are not claimed. - Adds pass (browser test) for the banner Dismiss keyboard assertion, the one check that is neither axe nor inspection. - Records the tool versions, the local opt-in status (P2-017), and that the Chrome version of the run is not recorded. - Open judgement items and the human AT session stay as not verified. Adds an Operations follow-up: its banner Dismiss (index.html:18) has the pre-fix markup and is outside WEB.40 scope. - eng/provenance/files.json: lists docs/web-40-accessibility.md after Directory.Packages.props (as 33c57df did) and tests/ArcForges.Web.App.Tests/HostPageAccessibilityTests.cs, which 49f72bf added without an inventory entry, so the provenance audit's inventory matches git. Co-Authored-By: Claude Haiku 5.5 --- docs/web-40-accessibility.md | 301 +++++++++++++++++++++++++++++++++++ eng/provenance/files.json | 2 + 2 files changed, 303 insertions(+) create mode 100644 docs/web-40-accessibility.md diff --git a/docs/web-40-accessibility.md b/docs/web-40-accessibility.md new file mode 100644 index 0000000..effdbca --- /dev/null +++ b/docs/web-40-accessibility.md @@ -0,0 +1,301 @@ +# WEB.40 accessibility re-proof: axe-core and WCAG 2.2 AA + +Status: recorded at run head 73173c2 (2026-10-09). This is local opt-in evidence (P2-017); no browser runs in CI. The coordinator re-runs the browser suites at the review head. + +Requirement: P2-021 item 8 (brief section 10, adjudication of 2026-10-09). Before the React apps retire, every replacement screen and state needs axe-core and a recorded WCAG 2.2 AA checklist. This record covers Account (five states), Chat (four states), the App error banner (one state) and the four Site pages: 14 screen states in all. Focus placement (AX-02) is recorded separately in [web-40-ax-02-focus.md](web-40-ax-02-focus.md), which links here for the axe and checklist evidence. The retirement table is [web-40-react-retirement.md](web-40-react-retirement.md), and the Site parity record is [web-40-site-parity.md](web-40-site-parity.md). + +## Method + +**Run.** `LocalAccessibilityBrowserTests` in `tests/browser/ArcForges.Web.Browser.Tests` declares 10 tests, and they write the 14 rows below. The run at head 73173c2 recorded 10 passed, 0 failed and 0 skipped in the local run log, which is not committed. The suite runs only when `ARCFORGES_LOCAL_BROWSER=1`, the build directories are named and no CI marker is set (`LocalOptIn.cs:32-39`). It serves `artifacts/publish/app/wwwroot` (the Account and Chat profiles, published from `src/ArcForges.Web.App`) and `artifacts/site` (the C# static Site) through Playwright routes. The Account and Chat service answers are fixed by the test, so no server or live service is used. + +**Tool versions.** + +| Component | Version | Where it is recorded | +|---|---|---| +| axe-core | 4.13.0 | Bundled in `Deque.AxeCore.Commons` 4.13.0; its embedded script reads `axe.version="4.13.0"` | +| Deque.AxeCore.Playwright | 4.13.0 | `Directory.Packages.props:11` | +| Microsoft.Playwright (.NET) | 1.62.0 | `Directory.Packages.props:15`; test project only | +| Chrome | Not recorded for the run. The run log names neither the browser nor its version, and it does not show `ARCFORGES_CHROMIUM_PATH`. The Chrome installed on this machine reports 156.0.8078.12 when read on 2026-10-09, but the record does not show the run used that executable. | `LocalOptIn.cs:19` names the variable; the focus record ([web-40-ax-02-focus.md](web-40-ax-02-focus.md), "Record of the local browser run") says its run on the same machine used the installed Chromium named by that variable | + +Playwright's own Chromium was not used by the focus run, because the Playwright build expects Chromium 1234 and that build is not installed on this machine (web-40-ax-02-focus.md, "Record of the local browser run"). + +**How axe ran.** `AssertAxeAsync` calls `Page.RunAxe()` with no options (`LocalAccessibilityBrowserTests.cs:326`), so the default rule selection applies and the axe-core script is the one bundled in the Deque package. The suite does not read `ARCFORGES_AXE_CORE_PATH`. Each screen asserts zero violations. + +**Rules evaluated.** The bundled axe-core 4.13.0 rule set has 105 rules. The run evaluated 89 of them on every state. The 16 it did not evaluate are seven experimental rules (css-orientation-lock, focus-order-semantics, hidden-content, label-content-name-mismatch, p-as-heading, table-fake-caption, td-has-header) and nine others (aria-roledescription, audio-caption, color-contrast-enhanced, duplicate-id, duplicate-id-active, identical-links-same-purpose, landmark-complementary-is-top-level, meta-refresh-no-exceptions, target-size). Two of the nine map to criteria in scope: target-size (2.5.8, AA) and audio-caption (1.2.1, tagged deprecated). The other seven are deprecated, obsolete or AAA. The report does not record why these rules were not evaluated, so the rows for 2.5.8 do not use axe. + +**Recording.** Each state appends two lines to the file named by `ARCFORGES_A11Y_REPORT`. The first holds the screen, the state, the URL and the counts of violations, passes, incomplete and inapplicable rules. The second names the sorted rule IDs that passed, were incomplete, were inapplicable and were violated. The rule lines let each pass (axe) cell be checked against the rules that actually passed on that state. + +**Reproduce.** Set `ARCFORGES_LOCAL_BROWSER=1`, `ARCFORGES_PROFILE_DIR` (the published Account and Chat `wwwroot`), `ARCFORGES_SITE_DIR` (the Site output), `ARCFORGES_CHROMIUM_PATH` (an installed Chrome or Edge) and `ARCFORGES_A11Y_REPORT`, with CI markers unset. Then run `dotnet test tests/browser/ArcForges.Web.Browser.Tests --no-build -c Release --filter FullyQualifiedName~LocalAccessibilityBrowserTests`. + +**Status values.** Each checklist cell holds exactly one of five values. + +- `pass (axe)`: a rule mapped to the criterion (its WCAG tag in the axe 4.13.0 rule metadata) is in that state's passed list. A rule that was inapplicable on a state cannot support `pass (axe)` there. +- `pass (inspection)`: checked by reading the Razor sources, CSS and Site HTML. The file and line are cited. Nothing was rendered. +- `pass (browser test)`: asserted by a test in `LocalAccessibilityBrowserTests` that drives the page and checks the criterion itself, and that the run recorded as passed. This value is added for this record, because one assertion (keyboard operation of the banner Dismiss) is neither axe nor inspection. It is used only for 2.1.1 on E1. +- `not applicable`: the reason is given in the Basis column. +- `not verified: needs a human assistive-technology session`: used where the criterion needs a screen reader, a judgement or a manual rendered check (reflow, target spacing, a focus overlap). It is also used where inspection found a likely failure, which is named under Open issues. There is no separate fail value, so this is the only value that stands in for a failure. + +**Derivation.** Where a criterion has mapped rules, a cell is `pass (axe)` when a mapped rule passed on that state, and otherwise it keeps the inspection status. Cells that stay `not verified` where a rule does apply are judgements the rule cannot settle: 2.4.4 on S1 to S3 (link-name passes there, but the purpose of Source in context is a judgement). The 1.4.1 cells on C1, C3 and C4 have no passing rule, because the grey Cancel state is a colour change that axe does not test. The checklist was generated from the rule lines, the axe tags and the criterion data, and every `pass (axe)` cell was checked against the passed list of its state. + +**Limits.** axe covers only part of WCAG 2.2. No rule in the run is tagged with 1.3.2, 1.3.3, 1.4.5, 1.4.10, 1.4.11, 1.4.13, 2.1.2, 2.4.5, 2.4.6, 2.4.7, 2.4.11, 2.5.7, 3.2.6, 3.3.3, 3.3.4, 3.3.7, 3.3.8 or 4.1.3, so their rows rest on inspection, judgement, a browser test or not applicable. The three rules tagged 2.1.1 (frame-focusable-content, scrollable-region-focusable and server-side-image-map) are inapplicable on every state, so 2.1.1 rests on inspection and the banner browser test. The experimental rules that map to 1.3.4 (css-orientation-lock), 2.4.3 (focus-order-semantics) and 2.5.3 (label-content-name-mismatch) are not run. The 2.2 criteria 2.4.11, 2.5.7, 3.2.6, 3.3.7 and 3.3.8 have no axe rule. + +**Not done in this record.** No browser was started, no screenshot was taken and nothing was rendered for this record. The results are those of the recorded run at 73173c2. The bUnit suites, `HostPageAccessibilityTests` and `LocalFocusBrowserTests` were not re-run here. The contrast ratios were recomputed from the hex values in `app.css` and the Site stylesheet. + +## Axe results + +Each row is one screen state. Passes, inapplicable, incomplete and violations sum to 89 on every row. The counts are from the local run at head 73173c2, and the coordinator re-confirms them at the review head. + +| Screen | State | URL | Violations | Incomplete | Passes | Inapplicable | Violated rules | +|---|---|---|---|---|---|---|---| +| Account | anonymous first read | /account | 0 | 0 | 33 | 56 | none | +| Account | signed in | /account | 0 | 0 | 38 | 51 | none | +| Account | sign-out in flight | /account | 0 | 0 | 38 | 51 | none | +| Account | session ended by sign-out (after the sign-out) | /account | 0 | 0 | 36 | 53 | none | +| Account | failed read | /account | 0 | 0 | 36 | 53 | none | +| Chat | idle | /chat | 0 | 0 | 37 | 52 | none | +| Chat | pending | /chat | 0 | 0 | 38 | 51 | none | +| Chat | after reply | /chat | 0 | 0 | 38 | 51 | none | +| Chat | failed send (notice shown) | /chat | 0 | 0 | 38 | 51 | none | +| App error banner | unhandled error shown | /chat | 0 | 0 | 41 | 48 | none | +| Site | page / | / | 0 | 0 | 30 | 59 | none | +| Site | page /hello/ | /hello/ | 0 | 0 | 39 | 50 | none | +| Site | page /cloud-hello/ | /cloud-hello/ | 0 | 0 | 35 | 54 | none | +| Site | page /404.html | /404.html | 0 | 0 | 17 | 72 | none | + +Result: 14 of 14 rows report zero violations and zero incomplete items. The 10 tests write the 14 rows: the Account states, the Chat states including the failed send, the App error banner shown through Blazor's critical-error entry point, and the four Site pages. + +### Rule names per state + +The rule lines of the run, one entry per state. Passed and inapplicable rules are listed exactly as the report names them. + +**A1 Account, anonymous first read** (/account): 33 passed, 0 incomplete, 56 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-hidden-focus`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `button-name`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `nested-interactive`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**A2 Account, signed in** (/account): 38 passed, 0 incomplete, 51 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `definition-list`, `dlitem`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**A3 Account, sign-out in flight** (/account): 38 passed, 0 incomplete, 51 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `definition-list`, `dlitem`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**A4 Account, session ended by sign-out (after the sign-out)** (/account): 36 passed, 0 incomplete, 53 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**A5 Account, failed read** (/account): 36 passed, 0 incomplete, 53 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**C1 Chat, idle** (/chat): 37 passed, 0 incomplete, 52 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `form-field-multiple-labels`, `heading-order`, `html-has-lang`, `html-lang-valid`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `list`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-deprecated-role`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-attr`, `aria-required-children`, `aria-required-parent`, `aria-roles`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `link-in-text-block`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**C2 Chat, pending** (/chat): 38 passed, 0 incomplete, 51 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `form-field-multiple-labels`, `heading-order`, `html-has-lang`, `html-lang-valid`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `list`, `listitem`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-deprecated-role`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-attr`, `aria-required-children`, `aria-required-parent`, `aria-roles`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `link-in-text-block`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**C3 Chat, after reply** (/chat): 38 passed, 0 incomplete, 51 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `form-field-multiple-labels`, `heading-order`, `html-has-lang`, `html-lang-valid`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `list`, `listitem`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-deprecated-role`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-attr`, `aria-required-children`, `aria-required-parent`, `aria-roles`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `link-in-text-block`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**C4 Chat, failed send (notice shown)** (/chat): 38 passed, 0 incomplete, 51 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `form-field-multiple-labels`, `heading-order`, `html-has-lang`, `html-lang-valid`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `list`, `listitem`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-deprecated-role`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-attr`, `aria-required-children`, `aria-required-parent`, `aria-roles`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `link-in-text-block`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**E1 App error banner, unhandled error shown** (/chat): 41 passed, 0 incomplete, 48 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `avoid-inline-spacing`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `form-field-multiple-labels`, `heading-order`, `html-has-lang`, `html-lang-valid`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-in-text-block`, `link-name`, `list`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**S1 Site, page /** (/): 30 passed, 0 incomplete, 59 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-conditional-attr`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-valid-attr`, `aria-valid-attr-value`, `bypass`, `color-contrast`, `document-title`, `duplicate-id-aria`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-allowed-role`, `aria-braille-equivalent`, `aria-command-name`, `aria-deprecated-role`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-attr`, `aria-required-children`, `aria-required-parent`, `aria-roles`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `avoid-inline-spacing`, `blink`, `button-name`, `definition-list`, `dlitem`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `nested-interactive`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**S2 Site, page /hello/** (/hello/): 39 passed, 0 incomplete, 50 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `button-name`, `bypass`, `color-contrast`, `document-title`, `duplicate-id-aria`, `empty-heading`, `form-field-multiple-labels`, `heading-order`, `html-has-lang`, `html-lang-valid`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `avoid-inline-spacing`, `blink`, `definition-list`, `dlitem`, `empty-table-header`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**S3 Site, page /cloud-hello/** (/cloud-hello/): 35 passed, 0 incomplete, 54 inapplicable, 0 violated. + +- Passed: `aria-allowed-attr`, `aria-allowed-role`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-hidden-body`, `aria-hidden-focus`, `aria-prohibited-attr`, `aria-required-attr`, `aria-roles`, `aria-valid-attr`, `aria-valid-attr-value`, `button-name`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-main-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `nested-interactive`, `page-has-heading-one`, `region`, `skip-link`, `tabindex`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-braille-equivalent`, `aria-command-name`, `aria-dialog-name`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-required-children`, `aria-required-parent`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `autocomplete-valid`, `avoid-inline-spacing`, `blink`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `summary-name`, `svg-img-alt`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + +**S4 Site, page /404.html** (/404.html): 17 passed, 0 incomplete, 72 inapplicable, 0 violated. + +- Passed: `aria-hidden-body`, `bypass`, `color-contrast`, `document-title`, `empty-heading`, `heading-order`, `html-has-lang`, `html-lang-valid`, `landmark-main-is-top-level`, `landmark-no-duplicate-main`, `landmark-one-main`, `landmark-unique`, `link-name`, `meta-viewport`, `meta-viewport-large`, `page-has-heading-one`, `region`. +- Incomplete: none. +- Inapplicable: `accesskeys`, `area-alt`, `aria-allowed-attr`, `aria-allowed-role`, `aria-braille-equivalent`, `aria-command-name`, `aria-conditional-attr`, `aria-deprecated-role`, `aria-dialog-name`, `aria-hidden-focus`, `aria-input-field-name`, `aria-meter-name`, `aria-progressbar-name`, `aria-prohibited-attr`, `aria-required-attr`, `aria-required-children`, `aria-required-parent`, `aria-roles`, `aria-tab-name`, `aria-text`, `aria-toggle-field-name`, `aria-tooltip-name`, `aria-treeitem-name`, `aria-valid-attr`, `aria-valid-attr-value`, `autocomplete-valid`, `avoid-inline-spacing`, `blink`, `button-name`, `definition-list`, `dlitem`, `duplicate-id-aria`, `empty-table-header`, `form-field-multiple-labels`, `frame-focusable-content`, `frame-tested`, `frame-title`, `frame-title-unique`, `html-xml-lang-mismatch`, `image-alt`, `image-redundant-alt`, `input-button-name`, `input-image-alt`, `label`, `label-title-only`, `landmark-banner-is-top-level`, `landmark-contentinfo-is-top-level`, `landmark-no-duplicate-banner`, `landmark-no-duplicate-contentinfo`, `link-in-text-block`, `list`, `listitem`, `marquee`, `meta-refresh`, `nested-interactive`, `no-autoplay-audio`, `object-alt`, `presentation-role-conflict`, `role-img-alt`, `scope-attr-valid`, `scrollable-region-focusable`, `select-name`, `server-side-image-map`, `skip-link`, `summary-name`, `svg-img-alt`, `tabindex`, `table-duplicate-name`, `td-headers-attr`, `th-has-data-cells`, `valid-lang`, `video-caption`. +- Violated: none. + + +## WCAG 2.2 checklist + +Screen state keys: A1 Account, anonymous first read (`/account`); A2 Account, signed in; A3 Account, sign-out in flight; A4 Account, session ended by sign-out; A5 Account, failed read. C1 Chat, idle (`/chat`); C2 Chat, pending; C3 Chat, after reply; C4 Chat, failed send (notice shown). E1 App error banner, unhandled error shown (on `/chat`). S1 `/`; S2 `/hello/`; S3 `/cloud-hello/`; S4 `/404.html`. + +WCAG 2.2 has 31 Level A and 24 Level AA success criteria. 4.1.1 Parsing was removed in 2.2 and is not listed. The six 2.2 additions in scope are 2.4.11, 2.5.7 and 2.5.8 (AA), 3.2.6 and 3.3.7 (A), and 3.3.8 (AA). Every criterion has one row and every state has one status per row: 55 criteria, 14 states, 770 cells. + +### Level A + +| SC | Criterion | A1 Account, anonymous | A2 Account, signed in | A3 Account, sign-out in flight | A4 Account, session ended | A5 Account, failed read | C1 Chat, idle | C2 Chat, pending | C3 Chat, after reply | C4 Chat, failed send | E1 App error banner | S1 Site / | S2 Site /hello/ | S3 Site /cloud-hello/ | S4 Site /404.html | Basis | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1.1.1 | Non-text Content | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | Inspection: no img, role=img, area or object on any screen. The icons are decorative and aria-hidden (Arrow.razor:1 through Button.razor:1; Shell.razor:1; HomeContent.razor:1, hello-art). The banner's dismiss control is a text glyph. The five mapped axe rules are inapplicable on every state. 404.html has no non-text content (Assets/404.html:12-17). | +| 1.2.1 | Audio-only and Video-only (Prerecorded) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No audio or video on any screen. The audio-caption rule carries the 1.2.1 tag, is deprecated, and is not evaluated by the run (Method). | +| 1.2.2 | Captions (Prerecorded) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No prerecorded video on any screen. The video-caption rule is inapplicable on every state. | +| 1.2.3 | Audio Description or Media Alternative (Prerecorded) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No prerecorded media on any screen. | +| 1.3.1 | Info and Relationships | pass (inspection) | pass (axe) | pass (axe) | pass (inspection) | pass (inspection) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | axe definition-list and dlitem pass on the signed-in Account states where the dl is shown (A2 and A3; Account.razor:32-47). axe list passes on C1 to C4 and E1, and listitem passes on C2 to C4, where the transcript list is shown (Chat.razor:21-25). Where no mapped rule passes, inspection: headings (Account.razor:9, Chat.razor:9, HomeContent.razor:1, HelloExample.razor:1, CloudHelloExample.razor:2, Assets/404.html:14); label association (Chat.razor:15-16, HelloExample.razor:1); section labelling (HomeContent.razor:1). | +| 1.3.2 | Meaningful Sequence | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | Inspection: DOM order equals visual order; app.css and site.css have no order, row-reverse or column-reverse rule. The sequence a screen reader announces is not checked here, and no axe rule covers it. | +| 1.3.3 | Sensory Characteristics | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: the instructions are text (Chat.razor:10; Account.razor:20; HelloExample.razor:1). The arrow glyphs are aria-hidden decoration (Arrow.razor:1; Shell.razor:1), and no text depends on them. No axe rule. | +| 1.4.1 | Use of Color | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not verified: needs a human assistive-technology session | pass (inspection) | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | pass (axe) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | axe link-in-text-block passes on E1, where the Reload link sits in running text; it is inapplicable on the other states. Chat idle (C1), after a reply (C3) and after a failed send (C4) show Cancel as grey with aria-disabled, and the grey is the only visual cue for that state (app.css:161-166; Chat.razor:19), so those cells are not verified (issue 8). On /hello/ and /cloud-hello/ the disabled controls have no visual state at all (site.css has no disabled rule), so no information is carried by colour there. That missing state is issue 2, not a 1.4.1 failure. | +| 1.4.2 | Audio Control | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No audio plays on any screen. The no-autoplay-audio rule is inapplicable on every state. | +| 2.1.1 | Keyboard | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (browser test) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: every control is a native button, input, form or link (Button.razor:1; Account.razor:53-56; Chat.razor:14-19; Shell.razor:1; HelloExample.razor:1). The sources have no key handler or pointer-only control (grep). Error banner (E1): Dismiss is a named native button (index.html:17). The browser test ErrorBannerDismissIsKeyboardOperableAndPassesAxe asserts that Dismiss takes keyboard focus and that Enter hides the banner; the run recorded it as passed at 73173c2, so E1 is pass (browser test). | +| 2.1.2 | No Keyboard Trap | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: no modal, focus trap or focus call. The tabindex attributes are ProfileLayout.razor:9 (main), Chat.razor:19 (Cancel while idle) and Shell.razor:1 (main). Tab can leave every control. | +| 2.1.4 | Character Key Shortcuts | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No accesskey and no single-key handler in the sources (grep). The accesskeys rule is best-practice and inapplicable on every state. | +| 2.2.1 | Timing Adjustable | pass (inspection) | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: no timer or time limit in the Razor sources. Account.razor:41-44 shows Session ends and Idle expiry on the signed-in states (A2, A3) with no warning and no way to extend (issue 4). Whether this server session expiry is a time limit under 2.2.1 is a judgement. The meta-refresh rule is inapplicable on every state. | +| 2.2.2 | Pause, Stop, Hide | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: nothing moves, blinks or updates by itself. The only transitions are hover colour changes (app.css:151, 153-156), and prefers-reduced-motion is honoured (app.css:373-380; site.css). The blink and marquee rules are inapplicable on every state. | +| 2.3.1 | Three Flashes or Below Threshold | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: no flashing content. The only animation is the 140 ms hover background transition (app.css:151). | +| 2.4.1 | Bypass Blocks | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | axe bypass (wcag241) passes on every state. The App has a skip link (ProfileLayout.razor:4) to main (ProfileLayout.razor:9), and the Site pages have one (Shell.razor:1). 404.html has no skip link and one main landmark (404.html:12). | +| 2.4.2 | Page Titled | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | axe document-title (wcag242) passes on every state. Titles: Account.razor:5 (Account session); Chat.razor:5 (Chat probe); SitePages.cs:15-17 (Hello, world; Your hello; Server connection); Assets/404.html:7 (Page not found). | +| 2.4.3 | Focus Order | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: focus follows DOM order, which is the visual order, and no positive tabindex is used. Account: skip link then Sign out (Account.razor:53-56). Chat: Name, Send, and Cancel only while pending (Chat.razor:14-19). FocusOrder.cs (tests/ArcForges.Web.App.Tests, line 35) asserts the same in bUnit; it was not re-run for this record. The axe focus-order-semantics rule is experimental and not run. | +| 2.4.4 | Link Purpose (In Context) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | pass (axe) | axe link-name (wcag244) passes on every state that has a link: the skip link on the App states, the Reload link on E1, and the Site links. On the Site, Source (Shell.razor:1; the arrow is aria-hidden) names its target only through its nav context, which is a judgement (issue 9), so S1 to S3 are not verified. The 404 link Back home (404.html:16) is clear. | +| 2.5.1 | Pointer Gestures | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: every action is one tap or click (Account.razor:55; Chat.razor:19; links). The sources contain no path-based or multipoint gesture. | +| 2.5.2 | Pointer Cancellation | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: actions run on click (@onclick at Account.razor:55 and Chat.razor:19), which fires on release. The sources have no mousedown, pointerdown or touchstart handler (grep). | +| 2.5.3 | Label in Name | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not verified: needs a human assistive-technology session | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: each accessible name contains its visible text. Sign out, Signing out..., Try again (Account.razor:88-90; the arrow is aria-hidden in Button.razor:1); Send, Sending..., Cancel (Chat.razor:18-19); Name (Chat.razor:15-16); ArcForges home contains ArcForges (Shell.razor:1); Source (Shell.razor:1). E1: the visible text of Dismiss is the glyph x and its accessible name is Dismiss, so whether the glyph is a label under 2.5.3 is a judgement (issue 10). The axe label-content-name-mismatch rule is experimental and not run. | +| 2.5.4 | Motion Actuation | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No function is operated by device motion or orientation (grep of the sources for devicemotion, deviceorientation and shake). | +| 3.1.1 | Language of Page | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | axe html-has-lang and html-lang-valid (wcag311) pass on every state. lang=en at index.html:2, SiteDocument.razor:1 and Assets/404.html:2. | +| 3.2.1 | On Focus | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: no focus handler and no code runs on focus. Chat.razor:16 binds on input, not on focus. | +| 3.2.2 | On Input | not applicable | not applicable | not applicable | not applicable | not applicable | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | not applicable | pass (inspection) | not applicable | not applicable | Chat: a change in Name only updates the value (Chat.razor:16, 36), and a submit happens on Send or Enter (Chat.razor:14). /hello/: the static input is disabled and runs no change handler (HelloExample.razor:1). The Account page and the error banner have no input that changes context. | +| 3.2.6 | Consistent Help | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No help mechanism on any screen: no help link or contact control in ProfileLayout.razor, Shell.razor, Account.razor, Chat.razor, the error banner or 404.html. | +| 3.3.1 | Error Identification | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not verified: needs a human assistive-technology session | not applicable | not applicable | not applicable | not applicable | not applicable | No input error is detected on these screens. The Chat failed-send notice (C4) reports a server rejection, not an input error; whether it must identify a field is a judgement (issue 6). The Account failure text (Account.razor:24) is not an input error, because Account has no input. The static Site cannot show an error: HelloExample.razor:1 sets _error only on submit. | +| 3.3.2 | Labels or Instructions | not applicable | not applicable | not applicable | not applicable | not applicable | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | not applicable | pass (inspection) | not applicable | not applicable | Chat: the visible label Name wraps the field (Chat.razor:15-16), and instructions are text (Chat.razor:10). /hello/: label for=hello-name reads Your name, with hint #name-hint (HelloExample.razor:1). The axe label rule applies to these fields but is tagged 4.1.2, not 3.3.2. | +| 3.3.7 | Redundant Entry | not applicable | not applicable | not applicable | not applicable | not applicable | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | not applicable | not applicable | not applicable | not applicable | Chat keeps the Name value across sends (Chat.razor:36, 54-63), so no entry is asked for again. The Account and Site pages have no process that asks for the same entry twice. | +| 4.1.2 | Name, Role, Value | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | axe link-name passes on every state. button-name passes on 11 states, label on 6, and aria-allowed-attr and aria-valid-attr-value on 13 each (Rule names per state). The banner's Dismiss has aria-label (index.html:17), and the browser test asserts its role and name. | + +### Level AA + +| SC | Criterion | A1 Account, anonymous | A2 Account, signed in | A3 Account, sign-out in flight | A4 Account, session ended | A5 Account, failed read | C1 Chat, idle | C2 Chat, pending | C3 Chat, after reply | C4 Chat, failed send | E1 App error banner | S1 Site / | S2 Site /hello/ | S3 Site /cloud-hello/ | S4 Site /404.html | Basis | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 2.4.5 | Multiple Ways | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not applicable | Judgement. The App profile has two pages (/account and /chat) with no link between them (ProfileLayout.razor:3-14), so each is reached only by its path (issue 5). The Site pages link to one another (Shell.razor:1 nav; HelloExample.razor:1 link to /cloud-hello/), and the Site output has no sitemap. 404 is not part of the page set. | +| 2.4.6 | Headings and Labels | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | Judgement on whether headings and labels describe their purpose. Headings: Account.razor:9; Chat.razor:9; HomeContent.razor:1; HelloExample.razor:1; CloudHelloExample.razor:2; Assets/404.html:14. Labels: Name (Chat.razor:15-16); Your name (HelloExample.razor:1). Adequacy needs a human reading. | +| 1.2.4 | Captions (Live) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No live media on any screen. | +| 1.2.5 | Audio Description (Prerecorded) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No prerecorded video on any screen. | +| 1.3.4 | Orientation | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: no orientation media query or orientation lock in app.css, site.css or the Razor sources. The axe css-orientation-lock rule is experimental and not run. | +| 1.3.5 | Identify Input Purpose | not applicable | not applicable | not applicable | not applicable | not applicable | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not applicable | not applicable | not verified: needs a human assistive-technology session | not applicable | not applicable | axe autocomplete-valid (wcag135) is inapplicable on every state, including /hello/, where the disabled input carries autocomplete="off" (HelloExample.razor:1). The Chat Name field has no autocomplete token (Chat.razor:16). Whether that field collects the user's own name, which 1.3.5 covers, is a judgement (issue 7). The /hello/ purpose is not identified either. | +| 1.4.3 | Contrast (Minimum) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | axe color-contrast (wcag143) passes on every state, with 0 violations and 0 incomplete. Inspected pairs, recomputed for this record: body ink #1e2824 on #f4f3ed 13.65:1; hero copy #5a675e on #f4f3ed 5.34:1; eyebrow #496052 on #f4f3ed 6.14:1; button #ffffff on #235844 8.23:1 (app.css:137-147); disabled button #ffffff on #6b7a72 4.51:1; banner ink #1e2824 on #fff3cd 13.69:1. | +| 1.4.4 | Resize Text | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | axe meta-viewport (wcag144) passes on every state. The viewport meta does not disable zoom (index.html:5; SiteDocument.razor:1; Assets/404.html:5). The 200 percent text check is part of 1.4.10 and is not run here. | +| 1.4.5 | Images of Text | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: all text is HTML and CSS. The large hello. on the home page is a styled span in an aria-hidden block (HomeContent.razor:1), not an image. No axe rule. | +| 1.4.10 | Reflow | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | A manual check at 320 CSS px and 400 percent zoom was not run. Inspection: body min-width 320 px (app.css:22-25); flex-wrap on .field (app.css:408-411); input min-width 240 px (app.css:415-416); fixed header heights (app.css:52-53; site.css .site-header). The error banner is fixed to the bottom edge (app.css:442-451). No axe rule covers reflow. | +| 1.4.11 | Non-text Contrast | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection. The Chat Name field border is #6b7a72, 4.51:1 on white (app.css, .field input, around line 415). The App input-row border is #6b7a72 (app.css, .input-row input), 4.44:1 on #fffdf7. Both replaced #b9c1b4 (1.85:1) and #a3b1a4 (2.20:1) in commit 49f72bf. The Send fill #235844 is 8.23:1 on white. The Cancel fill #6b7a72 is 4.06:1 on the page. The focus ring #235844 is 7.41:1 on the page. HostPageAccessibilityTests asserts 3:1 on these boundaries. The Site /hello/ input border #a3b1a4 (site.css, .input-row input) is 2.20:1 on #fffdf7, but that input is disabled in the static output, which is inactive and exempt (issue 2). No axe rule covers 1.4.11. | +| 1.4.12 | Text Spacing | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (axe) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | axe avoid-inline-spacing (wcag1412) passes on the states where it is listed as passed (Rule names per state); elsewhere inspection: no style attribute in any Razor source, so no inline style blocks text spacing. Fixed heights apply only to the header (app.css:52-53; site.css .site-header), which holds one line of text. Not rendered. | +| 1.4.13 | Content on Hover or Focus | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | pass (inspection) | The only content shown on focus is the skip link (app.css:81-91; site.css). Focused, it sits at top 12 px and left 24 px (app.css:90). By geometry it overlaps the top of the brand (header 112 px, app.css:53; brand from 64 px left, app.css:60-70; site-shell padding 64 px). The overlap is not rendered here and dismissal without moving focus is not provided, so a human check is needed (issue 1). The 404 page has no skip link and shows no content on focus, so it is pass (inspection). No axe rule. | +| 2.4.7 | Focus Visible | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: global :focus-visible outline, 3 px #235844, offset 5 px (app.css:43-46); button offset (app.css:157-159); site.css focus-visible rule; 404.css:26-29. The ring is 7.41:1 against the paper background and 7.43:1 on the banner background (commit 49f72bf). Checked in CSS, not in a rendered focus state. | +| 2.4.11 | Focus Not Obscured (Minimum) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not verified: needs a human assistive-technology session | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | Inspection: no sticky or fixed UI in the recorded states except #blazor-error-ui (app.css:442-451), which is hidden unless an unhandled error occurs. On E1 the banner is shown fixed to the bottom edge. The focused element in that state is the banner's own Dismiss, which is visible, but the banner can cover other focus targets on the page, which needs a human check (issue 11). No axe rule covers 2.4.11. | +| 2.5.7 | Dragging Movements | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No drag operation on any screen. The inputs are text fields and buttons only. No axe rule covers 2.5.7. | +| 2.5.8 | Target Size (Minimum) | not applicable | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | not verified: needs a human assistive-technology session | The axe target-size rule (wcag22aa) is not evaluated by the run: it is absent from both rule lists on every state (Method). Inspection: the Account buttons and the Chat Send and Cancel buttons are 52 px tall (app.css:137-142, .button min-height 52px), and so are the Site buttons (site.css, .button min-height 52px), and the Chat Name field is taller than 24 px (app.css:415-416). The anonymous Account state has no visible target: the skip link is off-screen until focused (app.css:81-86). The Site header links and back links are about 21 px tall (site.css, 14 px text at a 1.5 line height), below 24 px, so they need the spacing exception, which is a manual measurement (issue 12). The banner's Dismiss is a glyph with no padding, which is a manual measurement on E1. | +| 3.1.2 | Language of Parts | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | axe valid-lang (wcag312) is inapplicable on every state, because only the html element carries a lang attribute. Inspection: no passage in another language. | +| 3.2.3 | Consistent Navigation | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | The same header and footer wrap both App pages (ProfileLayout.razor:3-14) and every Site page (Shell.razor:1). The banner is not navigation. 404.html has no repeated navigation beyond its home link (404.html:16). | +| 3.2.4 | Consistent Identification | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | Each function keeps one label: the brand link reads ArcForges home on every Site page (Shell.razor:1). The App uses one Button component (Button.razor:1) for Sign out, Try again and Send, and the same element persists across states. The banner's Dismiss has one name (aria-label, index.html:17). | +| 3.3.3 | Error Suggestion | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not verified: needs a human assistive-technology session | not applicable | not applicable | not applicable | not applicable | not applicable | No input error is detected on these screens. The failed-send notice (C4) gives no correction, and whether it must is a judgement (issue 6). The failure text for a rejected request is in ProbeFailure.cs:40. | +| 3.3.4 | Error Prevention (Legal, Financial, Data) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No legal, financial or stored-data commitment on these screens. Chat sends an anonymous greeting and keeps the transcript in memory (Chat.razor:21-25). Account ends the session (Account.razor:140-153). Server-side handling is not inspected. | +| 3.3.8 | Accessible Authentication (Minimum) | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | not applicable | No sign-in step, password, code or object test on these screens. The anonymous Account state says You are not signed in. (Account.razor:28) and has no form. | +| 4.1.3 | Status Messages | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | pass (inspection) | not applicable | pass (inspection) | pass (inspection) | not applicable | Inspection of the programmatic roles: Account panel role=status (Account.razor:13); failure role=alert (Account.razor:24); Chat transcript aria-live=polite (Chat.razor:21); the failed-send notice is a transcript entry in that region (C4); the banner message is role=alert (E1; index.html:16); greeting role=status (HelloExample.razor:1); connection region role changes (CloudHelloExample.razor:2). Whether each announcement is spoken is for the AT session (issue 3). No axe rule (4.1.3 has no axe tag). | + +### Status counts + +Counts across all 55 criteria and 14 states (770 cells). + +| Status | Cells | +|---|---| +| pass (axe) | 113 | +| pass (inspection) | 328 | +| pass (browser test) | 1 | +| not applicable | 238 | +| not verified: needs a human assistive-technology session | 90 | +| total | 770 | + +## Fixed since the earlier draft + +These were resolved before this record, so they are not open issues. Each is backed by a commit and a test. + +- **Chat Name field boundary (1.4.11).** `.field input` moved from `#b9c1b4` (1.85:1 on white) to `#6b7a72` (4.51:1 on white), and the App input-row border moved from `#a3b1a4` to `#6b7a72` (4.44:1 on `#fffdf7`). Commit 49f72bf; the 3:1 checks are in `HostPageAccessibilityTests`. +- **Error banner Dismiss (2.1.1 and 4.1.2).** The unhandled-error Dismiss was an anchor with no `href`, role or tabindex. It is now `
private static readonly PacksFolderRow[] PacksFolderRows = [ @@ -200,10 +192,18 @@ public void TestOnlyPackagesAreRestoredByNoProductProject() Platform: "Windows", PacksFolder: "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", Version: "10.0.12", - Reason: "The hosted windows-2025-vs2026 image (runner image 20260925.250.1, CI job 113727560820) preinstalls the wasm.tools workload into C:\\Program Files\\dotnet, which holds this pack at the bundled version 10.0.12 (the same run's build lists the 10.0.12 Emscripten workload packs in that folder), so the SDK adds no NuGet download. Ubuntu, and a local machine whose packs folder lacks 10.0.12, restore the NuGet copy and it is verified by digest there."), + Marker: Path.Combine("runtimes", "browser-wasm", "native", "dotnet.native.wasm"), + Reason: "The hosted windows-2025-vs2026 image (runner image 20260925.250.1, CI job 113727560820) preinstalls the wasm.tools workload into C:\\Program Files\\dotnet, which holds this pack at the bundled version 10.0.12 (the same run's build lists the 10.0.12 Emscripten workload packs in that folder), so the SDK adds no NuGet download. A local machine whose packs folder lacks 10.0.12 restores the NuGet copy and it is verified by digest there."), + new( + Key: "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12", + Platform: "Linux", + PacksFolder: "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", + Version: "10.0.12", + Marker: Path.Combine("runtimes", "browser-wasm", "native", "dotnet.native.wasm"), + Reason: "The Ubuntu hosted image carries no WebAssembly workload, so this row is checked there and not satisfied: the SDK adds the implicit pack as a NuGet download, restores it and the test verifies its digest. The NuGet copy is skipped only when a 10.0.12 packs copy with the marker exists locally, and in that case the SDK does not download the pack either."), ]; - private sealed record PacksFolderRow(string Key, string Platform, string PacksFolder, string Version, string Reason); + private sealed record PacksFolderRow(string Key, string Platform, string PacksFolder, string Version, string Marker, string Reason); private static bool SatisfiedByPacksFolder(string packsRoot, string key, out string reason) { @@ -212,7 +212,8 @@ private static bool SatisfiedByPacksFolder(string packsRoot, string key, out str { if (row.Key != key || row.Platform != CurrentPlatform()) continue; - if (!Directory.Exists(Path.Combine(packsRoot, row.PacksFolder, row.Version))) + // The folder alone is not evidence of an installed pack: the content marker must exist at the admitted version. + if (!File.Exists(Path.Combine(packsRoot, row.PacksFolder, row.Version, row.Marker))) return false; reason = row.Reason; return true; From 4cca32e4157c97953fd33cafc8022bb4a9b07a62 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 13:58:38 +0100 Subject: [PATCH 37/38] [WEB.40] Seal a deterministic Site archive and keep the shell encodings off the profile root S20(a) of the migration brief (CLOUD.85 decisions D2 option A and D3): - Add web-site-.tar, a deterministic ustar archive of the C# static Site output written by the same TarArchive writer as web-profiles. Entries are the Site files in ordinal path order with fixed metadata. The candidate build seals it as a root member beside the assets directory, so the seal records its digest. The verifier regenerates the Site and refuses a second archive, a name that is not the digest, or any bytes other than the regenerated archive. The Cloudflare deploy still serves only the assets directory, so nothing served changes. - Publish web-site-.tar in the same main-push release as web-profiles, and only there (ci.yml). - Exclude the root index.html.br and index.html.gz from the web-profiles bundle. The shell is not served at the root, and its encodings are not either. Every other publish entry is kept. Verify refuses an archive that serves either encoding at the root. - Tests: Site archive determinism, name equal to digest, exact read-back; seal and verify coverage (changed, resealed, renamed, duplicated archive refused); bundle exclusion and its refusal. - Successor receipt web-40-admission-r16 (chained from r15, r15 unchanged) for the changed ci.yml input, reviewer w-deku-20261008-rev-web-40, and the provenance inventory update. Docs updated. Co-Authored-By: Claude Haiku 5.5 --- .github/workflows/ci.yml | 6 +- docs/deploying.md | 4 +- docs/profile-bundle.md | 2 +- eng/policy/dependency-policy.json | 12 +- .../web-40-admission-r16.json | 851 ++++++++++++++++++ eng/provenance/files.json | 3 + .../ProfileBundlePublishTests.cs | 77 ++ .../SiteArchiveTests.cs | 71 ++ tests/provenance/csharp-candidate.test.ts | 105 ++- .../Candidate/CandidateBuilder.cs | 3 + .../Candidate/CandidateVerifier.cs | 8 + .../Candidate/SiteArchive.cs | 37 + .../Profiles/ProfileBundle.cs | 13 +- 13 files changed, 1180 insertions(+), 12 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r16.json create mode 100644 tests/ArcForges.Web.Tooling.Tests/SiteArchiveTests.cs create mode 100644 tools/ArcForges.Web.Tooling/Candidate/SiteArchive.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cb1a9e..bed6e66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -406,9 +406,13 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + set -euo pipefail VERSION=$(node --input-type=module -e 'import fs from "node:fs"; console.log(JSON.parse(fs.readFileSync("artifacts/candidate/manifest.json")).version)') + # The Site archive (web-site-.tar) is the candidate's one sealed root member, verified by npm run deploy above. + # It is published here beside web-profiles, and only by this main-push release. It is not a deployed asset. + test "$(ls artifacts/candidate/web-site-*.tar | wc -l)" = 1 tar -czf "artifacts/web-${VERSION}.tar.gz" -C artifacts/candidate . - gh release create "web-${VERSION}" "artifacts/web-${VERSION}.tar.gz" artifacts/deployment.json artifacts/profiles/web-profiles-*.tar --target "$GITHUB_SHA" --title "Web ${VERSION}" --prerelease --generate-notes + gh release create "web-${VERSION}" "artifacts/web-${VERSION}.tar.gz" artifacts/deployment.json artifacts/profiles/web-profiles-*.tar artifacts/candidate/web-site-*.tar --target "$GITHUB_SHA" --title "Web ${VERSION}" --prerelease --generate-notes - name: Preserve deployment evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/docs/deploying.md b/docs/deploying.md index 59a2c6f..b35fb5f 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -26,7 +26,7 @@ The token stays in CI. The browser never receives Cloudflare management credenti 2. The candidate job publishes the Blazor WebAssembly application, emits the Worker and the build identity from the reviewed sources (`tooling/candidate.ts`), then builds the sealed candidate with the C# tool (`candidate build`) twice, compares the bytes, and verifies it (`candidate verify`). The Account and Chat profile bundle (`profiles bundle`) is built and verified beside it. 3. `Verify` requires applicable checks. Main deployment consumes the workflow candidate by artifact ID. Before any upload, its entry point verifies the seal again (`candidate verify`: the exact member set, the wrangler configuration, the pinned Worker, the regenerated Site compared byte for byte, the static page graph, and the licence, notice, SBOM and policy receipt against the repository), then re-derives the build identity. It does not rebuild. 4. Deployment confirms the current main commit and intended domain mapping, then runs Wrangler. No DNS mapping is recreated. -5. Successful provider completion records `status: deployed` in `artifacts/deployment.json` and creates a prerelease with the original candidate archive and deployment record. It does not claim live HTTP or browser acceptance. +5. Successful provider completion records `status: deployed` in `artifacts/deployment.json` and creates a prerelease with the original candidate archive, the Site archive, the profile bundle and the deployment record. It does not claim live HTTP or browser acceptance. There are no browser installations, local/live E2E, public-file hash downloads, readiness polling or real Cloud calls in CI. The optional local diagnostic commands reject CI execution. See [validation policy](validation-policy.md). @@ -36,6 +36,8 @@ There are no browser installations, local/live E2E, public-file hash downloads, The Blazor profile bundle (`web-profiles-.tar`) keeps its name pattern, its manifest-first layout, its root `_headers` and its `account/` and `chat/` pages. Its files come from the publish of ArcForges.Web.App; its browser-graph check is the publish's static web assets manifest (every file's integrity). The archive is consumed by the Cloud proof deployment and is not deployed by this Web workflow. +The sealed candidate also holds one deterministic Site archive at its root, `web-site-.tar` (`candidate build`). It is written by the same ustar writer as the profile bundle, holds the C# Site files in ordinal path order with fixed metadata, and is named by the digest of its own bytes. The seal records that digest. `candidate verify` regenerates the Site and refuses any other bytes, a second archive, or a name that is not the digest. Only the main-push prerelease publishes it, beside `web-profiles-.tar`. Wrangler deploys `artifacts/candidate/assets` only, so the Site archive is not a deployed asset and changes nothing that Cloudflare serves. + PR, schedule and manually dispatched workflows validate but do not deploy. Main pushes deploy automatically once the credential is present. No second Cloudflare Git integration is required; enabling one would create an independent deployment path that bypasses this candidate process. ## Serving and security diff --git a/docs/profile-bundle.md b/docs/profile-bundle.md index b99d185..282e50a 100644 --- a/docs/profile-bundle.md +++ b/docs/profile-bundle.md @@ -8,7 +8,7 @@ The Account and Chat profiles are one published application (`src/ArcForges.Web. - First entry: `manifest.json` (schema 1). It lists each profile (`page`, `buildDigest`, `csp`) and every other entry with its SHA-256 and byte count. - Root `_headers`: security headers for `/*`; one block per profile path (`/account/*`, `/chat/*`) with its Content-Security-Policy, `X-Frame-Options: DENY` and `no-cache`; `/assets/*` immutable; `/_framework/*` immutable; `/_framework/blazor.webassembly.js` and `/_framework/dotnet.js` revalidated (`no-cache`). - Profile pages: `account/index.html` and `chat/index.html`, each the application shell, with the profile's own policy. -- Served tree: the publish `wwwroot` at its own root paths. The shell `index.html` is not served at the root. Blazor's base href is `/`, so `_framework/` files are root-relative. The tree includes the precompressed `.br` and `.gz` siblings the SDK writes. +- Served tree: the publish `wwwroot` at its own root paths. The shell `index.html` is not served at the root, and neither are its precompressed siblings `index.html.br` and `index.html.gz`, because they only encode the shell. Blazor's base href is `/`, so `_framework/` files are root-relative. Every other file of the publish is kept, including the `.br` and `.gz` siblings of the `_framework/` files that the SDK writes. ## Rules the tool enforces diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 8283a1d..427c212 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -6,7 +6,7 @@ "package.json" ], "inputs": { - ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", + ".github/workflows/ci.yml": "aa1d456b4a196bed61b3cca281545a1c95395f9361ad15eec5862611b4e0c66a", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", @@ -50,9 +50,9 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-09", - "rationale": "WEB.40 successor of web-40-admission-r14, recording the two changes of commit 389b9fd that the independent review of 6ff8e1e and 389b9fd required a receipt for. Reviewer w-deku-20261008-rev-web-40; reviewed base 389b9fd746eefb80496263ae83072adb04f1a6a2. No package, version, source, licence, contentHash, nuspecSha256, closure coordinate or bound input changed: packages, inputs, con07Identity, browserWasmPacks, nugetClosure and nugetPackDownloads are copied from r14 unchanged. (1) Scanner exception. .gitleaks.toml gains one [[allowlists]] block (scannerExceptions) with rule generic-api-key, AND over the exact path eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1/source.json and one exact anchored line regex. It matches the key public/http/v1/native-auth.schema.json whose value e7807ae0... is the SHA-256 of the packaged schemas/public/http/v1/native-auth.schema.json inside arcforges.contracts.publicapi 1.0.0-ci.287.1, a verified false positive. The block adds one regex, scoped to that one path; it does not widen the r1 to r11 browser-resources exception. (2) Packs folder. NuGetClosureAdmissionTests.SatisfiedByPacksFolder skips the NuGet copy of a pack-download row only through an explicit PacksFolderRows entry for the current platform, and only when the packs folder holds the admitted version and its content marker (runtimes/browser-wasm/native/dotnet.native.wasm), not merely the folder. The Windows row covers the hosted windows-latest image, whose preinstalled wasm.tools workload supplies the 10.0.12 browser-wasm pack, so the SDK downloads no NuGet copy there. The Linux row records that the Ubuntu image carries no workload: the row is not satisfied there, so the NuGet copy is required and its digest verified. Every other row needs the NuGet copy and its digest on every platform. The redundant final Assert.All of the test is removed. (3) Correction of r14. The r14 rationale sentence that reads the convention as the restore 'whose .nupkg SHA-512 equals the row contentHash' is wrong. Each row's contentHash equals the contentHash in the package's .nupkg.metadata and in packages.lock.json. It is not the SHA-512 of the .nupkg file, which the .nupkg.sha512 sidecar records separately; this was confirmed for arcforges.contracts.publicapi 1.0.0-ci.287.1, newtonsoft.json 13.0.1 and microsoft.netcore.app.runtime.mono.browser-wasm 10.0.12. nuspecSha256 is the SHA-256 of the restored .nuspec entry bytes, UTF-8 BOM included, read from the NuGet packages folder. r14 is not edited. Verification is local only: gitleaks and Docker are not installed, so the history scan is an emulation. The hosted quality and csharp jobs at 389b9fd must be green, and the independent exact-head review of the scanner exception by w-deku-20261008-rev-web-40 remains a merge condition.", + "rationale": "WEB.40 successor of web-40-admission-r15 for the S20(a) Site release asset (CLOUD.85 decisions D2 option A and D3). The one bound input that changed is .github/workflows/ci.yml: the main-push release now publishes web-site-.tar beside web-profiles, guarded so that exactly one Site archive is in the sealed candidate. The Site archive is a deterministic root member of the candidate, sealed by its digest and verified byte for byte against the regenerated Site; it is not part of the Cloudflare assets directory. The profile bundle drops the shell's precompressed siblings (index.html.br, index.html.gz) from the root. No other bound input changed: the csproj files, packages.lock.json files, props, global.json, package.json, package-lock.json, wrangler.json, .node-version, .npmrc, NuGet.config and the browser-resources profile and record r11 and r12 keep their digests. Reviewer w-deku-20261008-rev-web-40; reviewed base efe787254ae64cc8027a6b801b3cbd9080481321. No package, version, source, licence, contentHash, nuspecSha256, closure coordinate or scanner exception changed: packages, scannerExceptions, packsFolderRows, con07Identity, browserWasmPacks, nugetClosure and nugetPackDownloads are copied from r15 unchanged. r15 is not edited. Verification is local only: the local Node is 24.20 against the 24.21 pin, and actionlint, gitleaks and Docker are not installed; the hosted quality and csharp jobs at the exact head are the authority, and the exact-head review by w-deku-20261008-rev-web-40 remains a merge condition.", "inputs": { - ".github/workflows/ci.yml": "936d1bc03be14b592eec22fc6ef8f1b9086dc6e4a1f5e2a012a848a38e932695", + ".github/workflows/ci.yml": "aa1d456b4a196bed61b3cca281545a1c95395f9361ad15eec5862611b4e0c66a", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", @@ -101,9 +101,9 @@ "sbom", "framework-posture" ], - "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site; the deploy job ships the same sealed bytes under the unchanged main-push guards. No hosted browser, device, macOS or live-service check is added.", + "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site and its Site archive; the deploy job ships the same sealed bytes under the unchanged main-push guards, and the Site archive is a release asset only, never a deployed asset. No hosted browser, device, macOS or live-service check is added.", "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", - "previous": "eng/policy/dependency-reviews/web-40-admission-r14.json", + "previous": "eng/policy/dependency-reviews/web-40-admission-r15.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -811,7 +811,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r15.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r16.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", diff --git a/eng/policy/dependency-reviews/web-40-admission-r16.json b/eng/policy/dependency-reviews/web-40-admission-r16.json new file mode 100644 index 0000000..0b550bc --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r16.json @@ -0,0 +1,851 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-09", + "rationale": "WEB.40 successor of web-40-admission-r15 for the S20(a) Site release asset (CLOUD.85 decisions D2 option A and D3). The one bound input that changed is .github/workflows/ci.yml: the main-push release now publishes web-site-.tar beside web-profiles, guarded so that exactly one Site archive is in the sealed candidate. The Site archive is a deterministic root member of the candidate, sealed by its digest and verified byte for byte against the regenerated Site; it is not part of the Cloudflare assets directory. The profile bundle drops the shell's precompressed siblings (index.html.br, index.html.gz) from the root. No other bound input changed: the csproj files, packages.lock.json files, props, global.json, package.json, package-lock.json, wrangler.json, .node-version, .npmrc, NuGet.config and the browser-resources profile and record r11 and r12 keep their digests. Reviewer w-deku-20261008-rev-web-40; reviewed base efe787254ae64cc8027a6b801b3cbd9080481321. No package, version, source, licence, contentHash, nuspecSha256, closure coordinate or scanner exception changed: packages, scannerExceptions, packsFolderRows, con07Identity, browserWasmPacks, nugetClosure and nugetPackDownloads are copied from r15 unchanged. r15 is not edited. Verification is local only: the local Node is 24.20 against the 24.21 pin, and actionlint, gitleaks and Docker are not installed; the hosted quality and csharp jobs at the exact head are the authority, and the exact-head review by w-deku-20261008-rev-web-40 remains a merge condition.", + "inputs": { + ".github/workflows/ci.yml": "aa1d456b4a196bed61b3cca281545a1c95395f9361ad15eec5862611b4e0c66a", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "95e73db26cdb9ba15ccd3c752d804be4ee083f85fdf589acf837121a2c3975b7", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "eng/provenance/records/browser-resources-r12.json": "6b470604b64d692d0eaa215be133506af065856dde0fde9ee8e702bba883bb88", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "392b8874131b713ac07ab595ff541122fa3c2317f9ba81b34ffe6115f4363825", + "package.json": "60b8b8e853ae2ac0dfec58831d61c717b3e4fb26ed3848745b36e9a2ec370ea5", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "6ae92b481b6a892a9b4952f7e4b8da56b7caac8f6a6206151b017108e2c99229", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "75bd9b2ebbe6ed307c134473be0319fd536a7733ce00fe1237148299e5f59239", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "61d7150f179a77464386591f7a8c38cdbadb95b9c29fff18fe2ef912bef91931", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "5939781e73182199cd09e6bf6baf91a74bf2111e67a2379c073afdcb53ba75ff", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "e7446b75088edc5a49877ec5b4927c0d7bf65e523a1cc76a8c626a547bc7917b", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "15eb52424732fb8bf0a7479acf0c323b9a36c7e1503c3369bf92f402319b55e0", + "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", + "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "482a8749cf737188e1106d77c1d657ed5c1c9626317be6e337c59e2b3e46793b", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", + "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site and its Site archive; the deploy job ships the same sealed bytes under the unchanged main-push guards, and the Site archive is a release asset only, never a deployed asset. No hosted browser, device, macOS or live-service check is added.", + "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r15.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r15.json", + "reviewedBase": "efe787254ae64cc8027a6b801b3cbd9080481321", + "scannerExceptions": [ + { + "file": ".gitleaks.toml", + "introducedBy": "389b9fd746eefb80496263ae83072adb04f1a6a2", + "findingCommit": "2a5e17f", + "findingPath": "eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1/source.json", + "findingLine": 9, + "findingKey": "public/http/v1/native-auth.schema.json", + "findingValuePrefix": "e7807ae0", + "rule": "generic-api-key", + "digest": "e7807ae0ab6b4530d817941a922d7df90e4feb525c68d81a245a4c089ad3d296", + "digestDefinition": "SHA-256 of schemas/public/http/v1/native-auth.schema.json in arcforges.contracts.publicapi 1.0.0-ci.287.1", + "status": "verified false positive", + "allowlist": { + "description": "Reviewed public schema digest in the CON.07 Contracts publication record: the key name contains auth, and the value is the SHA-256 of the packaged public schema (verified against the NuGet package bytes)", + "condition": "AND", + "targetRules": [ + "generic-api-key" + ], + "regexTarget": "line", + "paths": [ + "^eng/contracts/ArcForges\\.Contracts\\.PublicApi/1\\.0\\.0-ci\\.287\\.1/source\\.json$" + ], + "regexes": [ + "^\\s*\"public/http/v1/native-auth\\.schema\\.json\": \"e7807ae0ab6b4530d817941a922d7df90e4feb525c68d81a245a4c089ad3d296\",?$" + ] + } + } + ], + "packsFolderRows": [ + { + "key": "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12", + "platform": "Windows", + "packsFolder": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", + "version": "10.0.12", + "marker": "runtimes/browser-wasm/native/dotnet.native.wasm", + "reason": "The hosted windows-2025-vs2026 image (runner image 20260925.250.1, CI job 113727560820) preinstalls the wasm.tools workload into C:\\Program Files\\dotnet, which holds this pack at the bundled version 10.0.12 (the same run's build lists the 10.0.12 Emscripten workload packs in that folder), so the SDK adds no NuGet download. A local machine whose packs folder lacks 10.0.12 restores the NuGet copy and it is verified by digest there." + }, + { + "key": "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12", + "platform": "Linux", + "packsFolder": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", + "version": "10.0.12", + "marker": "runtimes/browser-wasm/native/dotnet.native.wasm", + "reason": "The Ubuntu hosted image carries no WebAssembly workload, so this row is checked there and not satisfied: the SDK adds the implicit pack as a NuGet download, restores it and the test verifies its digest. The NuGet copy is skipped only when a 10.0.12 packs copy with the marker exists locally, and in that case the SDK does not download the pack either." + } + ], + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f18300e6b837a6e8ccafb56d77de97fe74ed6437f6a2952ba82e9d3eff6e121f" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "72f9ffbff6488f970b48d7b35458c0e0eb4223865caea9adc16fc7105a393ad8" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "affe7a268a8acae0e9a3ea168b615f215f921b3ac481a73bfe3c0886c85fb647" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "a732f6d69c500d569ab44966de8182d95495fad9d4df874a12838df85841b65c" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "6fc4636e918c1510556afede34b23f73e8132613832e6e2f98058c4282c24d08" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "bc55aa1371a4ce334c64b54150011444fe5880d4cd264e73fc1ff1136889c9ff" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Test-only HTML5 parser: a direct reference of tests/ArcForges.Web.Site.Tests (the WEB.40 normalised-DOM parity gate, 2026-10-09 adjudication) and a transitive dependency of the bUnit closure. Reached only from tests/**; never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "72f9ffbff6488f970b48d7b35458c0e0eb4223865caea9adc16fc7105a393ad8", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "f18300e6b837a6e8ccafb56d77de97fe74ed6437f6a2952ba82e9d3eff6e121f", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "deque.axecore.commons/4.13.0": { + "contentHash": "roq2z6OmKY4KVXGj1lSigUbjE7DnIcYLqPOVcnWrusS6CI7oB6/He8Oo3ZO/TCOlnaTS1hkJOuKPwL+PyhyC6w==", + "licence": "MPL-2.0", + "testOnly": true, + "nuspecSha256": "6d1ad9cf06acab6eb22fe449e1aead325803b14ec00efd57224cdc1b376883c1", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.commons/4.13.0/deque.axecore.commons.nuspec", + "classification": "Test-only axe-core script carrier (MPL-2.0 is recorded by the WEB.40 2026-10-09 adjudication). File-level weak copyleft, accepted only for a row marked testOnly: no product project restores it, and it is never shipped or compiled into a product output. Referenced only through Deque.AxeCore.Playwright by tests/browser." + }, + "deque.axecore.playwright/4.13.0": { + "contentHash": "tqzqVEsQfVg4fmx9/AkbgYjVMYHHOGI7WkTznKuP8ayi0uaBcd+2skOfxnVdUBlEhpHdu9stzxOZ6uPVhIS2JA==", + "licence": "MIT", + "nuspecSha256": "6a77936de180b932c71a289bac7d638be4485e6b8a83a463429a72f7b8b44a8a", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.playwright/4.13.0/deque.axecore.playwright.nuspec", + "classification": "Test-only accessibility package (WEB.40 P2-021 item 8): axe-core run through Microsoft.Playwright for .NET. Referenced only by tests/browser (local opt-in, never built in CI). axe-core is injected only into the page under test. Its Microsoft.Playwright 1.20.2 dependency resolves to the centrally pinned 1.62.0." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "d6a4a567ce4889e25d098fd39d13a65ce05c847e8574818edc0d1eee6277eb40", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "bba00f5d5be8423dd351662e313648b0abd13100ef67d55f6fc29de44c544a96", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "da47ce027ddbbbf7c0b1f242b1efe64a5aeaa2959da54716a100fcc6e4520efb", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "32a43f3448ab90bb71c946533db4d5135cef4728a3ee29bf88d9a0cbf1e67413", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "f621aa566e63340b572a4f0bd2b996b934407c77b8cdf121f38578e93ee796cb", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "54e463a46bb9c12338e654dcd6e2cf1fa956a29e2b826e84d07b86348581e6d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "4467841e7a008d302cc7f6e06f601ff8d3a6afc4b74ae292e2937596d4883ed5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "newtonsoft.json/13.0.1": { + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", + "licence": "MIT", + "nuspecSha256": "4ffea71f07d70dff4c57529871f8fc377bf85e62e93b24f1ae5409ff73570c5e", + "source": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", + "classification": "Transitive MIT JSON library of Deque.AxeCore.Commons, test-only: reached only from tests/browser (local opt-in). No product project references it; the architecture policy refuses its use in product C# source." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "81b52aa34460e41f850b163546fa6e72e007499e2b8ed7396c217944b2ec778f", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.io.abstractions/17.0.24": { + "contentHash": "hA7bacntMiZv1Yf9xtjwl/GP3GT1mG84QxhAk7ijAUD0pJhJaVVwXScE13vMpXnNtlaRDW6SeyZdWg2j2qrh4w==", + "licence": "MIT", + "nuspecSha256": "c9d330f98c4f078404906c2705a4f47b142a0aac500022e1813939cbb276e251", + "source": "https://api.nuget.org/v3-flatcontainer/system.io.abstractions/17.0.24/system.io.abstractions.nuspec", + "classification": "Transitive MIT file-system abstraction of Deque.AxeCore.Playwright, test-only: reached only from tests/browser (local opt-in). Its net6.0 dependency group is empty, so no further package is restored for net10.0." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "6eb69422b052d9619c40ba521b5e1f0c52ebcbc3394c4a76ac232494ac924439", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "954d8efea4619301fcb2321397f6ca63b7fa44ceecf1509cd9130dc1677013ee", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/eng/provenance/files.json b/eng/provenance/files.json index ae75a94..2b20fed 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -28,6 +28,7 @@ "eng/policy/dependency-reviews/web-40-admission-r13.json", "eng/policy/dependency-reviews/web-40-admission-r14.json", "eng/policy/dependency-reviews/web-40-admission-r15.json", + "eng/policy/dependency-reviews/web-40-admission-r16.json", "NuGet.config", "README.md", "SECURITY.md", @@ -238,6 +239,7 @@ "tests/ArcForges.Web.Tooling.Tests/ProfileBundleArchiveTests.cs", "tests/ArcForges.Web.Tooling.Tests/ProfileBundlePublishTests.cs", "tests/ArcForges.Web.Tooling.Tests/ProfileBundleRulesTests.cs", + "tests/ArcForges.Web.Tooling.Tests/SiteArchiveTests.cs", "tests/ArcForges.Web.Tooling.Tests/RepositoryRoot.cs", "tests/ArcForges.Web.Tooling.Tests/packages.lock.json", "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj", @@ -268,6 +270,7 @@ "tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs", "tools/ArcForges.Web.Tooling/Candidate/CandidateCore.cs", "tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs", + "tools/ArcForges.Web.Tooling/Candidate/SiteArchive.cs", "tools/ArcForges.Web.Tooling/Profiles/ProfileBudget.cs", "tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs", "tools/ArcForges.Web.Tooling/Profiles/TarArchive.cs", diff --git a/tests/ArcForges.Web.Tooling.Tests/ProfileBundlePublishTests.cs b/tests/ArcForges.Web.Tooling.Tests/ProfileBundlePublishTests.cs index 15f7677..62ca74a 100644 --- a/tests/ArcForges.Web.Tooling.Tests/ProfileBundlePublishTests.cs +++ b/tests/ArcForges.Web.Tooling.Tests/ProfileBundlePublishTests.cs @@ -294,4 +294,81 @@ public void AnArchiveWhoseDigestDoesNotMatchIsRefused() Assert.Contains("Bundle digest mismatch.", error.Message, StringComparison.Ordinal); } + + private static byte[] Gzip(byte[] bytes) + { + using var output = new MemoryStream(); + using (var gzip = new System.IO.Compression.GZipStream(output, System.IO.Compression.CompressionLevel.Optimal)) + gzip.Write(bytes); + return output.ToArray(); + } + + private static byte[] Brotli(byte[] bytes) + { + using var output = new MemoryStream(); + using (var brotli = new System.IO.Compression.BrotliStream(output, System.IO.Compression.CompressionLevel.Optimal)) + brotli.Write(bytes); + return output.ToArray(); + } + + [Fact] + public void TheShellEncodingsAreNotServedAtTheRootAndEveryOtherFileIsKept() + { + // The encodings are real Brotli and gzip streams of the shell, because the static graph check decodes them. + var withEncodings = StandardFiles(); + withEncodings["index.html.br"] = Brotli(Utf8(Shell)); + withEncodings["index.html.gz"] = Gzip(Utf8(Shell)); + using var plain = new Publish(StandardFiles()); + using var encoded = new Publish(withEncodings); + + var bundle = ProfileBundle.Build(encoded.Location); + var served = TarArchive.Read(bundle.Archive); + var reference = TarArchive.Read(ProfileBundle.Build(plain.Location).Archive); + + Assert.DoesNotContain("index.html.br", served.Select(entry => entry.Path)); + Assert.DoesNotContain("index.html.gz", served.Select(entry => entry.Path)); + Assert.DoesNotContain(ProfileBundle.ShellPath, served.Select(entry => entry.Path)); + // Without the encodings, every served entry after the manifest is the same path and the same bytes. + Assert.Equal(reference.Skip(1).Select(entry => entry.Path), served.Skip(1).Select(entry => entry.Path)); + Assert.Equal(reference.Skip(1).Select(entry => entry.Bytes), served.Skip(1).Select(entry => entry.Bytes)); + ProfileBundle.Verify(bundle.Archive, bundle.Digest); + } + + [Fact] + public void AnArchiveThatServesAShellEncodingAtTheRootIsRefused() + { + var bundle = BuildStandard(); + var entries = TarArchive.Read(bundle.Archive).ToList(); + var encoding = Gzip(Utf8(Shell)); + var manifest = System.Text.Json.Nodes.JsonNode.Parse(entries[0].Bytes)!.AsObject(); + // The forged manifest lists the encoding with its real digest, in the ordinal order the verifier requires. + var rows = new SortedDictionary(StringComparer.Ordinal); + foreach (var pair in manifest["files"]!.AsObject()) + rows[pair.Key] = pair.Value!.DeepClone(); + rows["index.html.gz"] = new System.Text.Json.Nodes.JsonObject + { + ["sha256"] = Convert.ToHexString(SHA256.HashData(encoding)).ToLowerInvariant(), + ["bytes"] = encoding.Length, + }; + var files = new System.Text.Json.Nodes.JsonObject(); + foreach (var (path, row) in rows) + files[path] = row; + var forgedManifest = new System.Text.Json.Nodes.JsonObject + { + ["schema"] = ProfileBundle.Schema, + ["profiles"] = manifest["profiles"]!.DeepClone(), + ["files"] = files, + }; + var forgedEntries = new List + { + new(ProfileBundle.ManifestPath, Encoding.UTF8.GetBytes(forgedManifest.ToJsonString())), + }; + forgedEntries.AddRange(entries.Skip(1).Append(new TarArchive.Entry("index.html.gz", encoding)) + .OrderBy(entry => entry.Path, StringComparer.Ordinal)); + var forged = TarArchive.Write(forgedEntries); + + var error = Assert.Throws(() => ProfileBundle.Verify(forged)); + + Assert.Contains("encodings must not be served at the root", error.Message, StringComparison.Ordinal); + } } diff --git a/tests/ArcForges.Web.Tooling.Tests/SiteArchiveTests.cs b/tests/ArcForges.Web.Tooling.Tests/SiteArchiveTests.cs new file mode 100644 index 0000000..18b684a --- /dev/null +++ b/tests/ArcForges.Web.Tooling.Tests/SiteArchiveTests.cs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Security.Cryptography; +using ArcForges.Web.Site; +using ArcForges.Web.Tooling.Candidate; +using ArcForges.Web.Tooling.Profiles; +using Xunit; + +namespace ArcForges.Web.Tooling.Tests; + +/// +/// The Site release asset (web-site-<sha256>.tar): two builds of the same Site are the same bytes, the name is the +/// digest of those bytes, and the archive holds exactly the Site's own files in ordinal path order. The candidate seal and +/// the refusal of a tampered archive are covered by tests/provenance/csharp-candidate.test.ts on a sealed candidate. +/// +public sealed class SiteArchiveTests +{ + private const string SourceRef = "0123456789abcdef0123456789abcdef01234567"; + + private static async Task BuildSiteAsync() => + await SiteBuilder.BuildAsync(new SiteOptions { SourceRef = SourceRef }); + + private static string Digest(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); + + [Fact] + public async Task TwoBuildsOfTheSiteGiveTheSameArchiveBytes() + { + var first = SiteArchive.Build((await BuildSiteAsync()).Files); + var second = SiteArchive.Build((await BuildSiteAsync()).Files); + + Assert.Equal(first, second); + } + + [Fact] + public async Task TheArchiveIsNamedByTheDigestOfItsOwnBytes() + { + var archive = SiteArchive.Build((await BuildSiteAsync()).Files); + var name = SiteArchive.Name(Digest(archive)); + + Assert.Matches("^web-site-[a-f0-9]{64}\\.tar$", name); + Assert.True(SiteArchive.IsName(name)); + Assert.False(SiteArchive.IsName("web-site-" + new string('a', 64) + ".tar.gz")); + Assert.False(SiteArchive.IsName("web-profiles-" + Digest(archive) + ".tar")); + } + + [Fact] + public async Task TheArchiveHoldsEverySiteFileInOrderAndNothingElse() + { + var site = await BuildSiteAsync(); + var expected = site.Files.OrderBy(file => file.Path, StringComparer.Ordinal).ToArray(); + + var entries = TarArchive.Read(SiteArchive.Build(site.Files)); + + Assert.Equal(expected.Select(file => file.Path), entries.Select(entry => entry.Path)); + Assert.Equal(expected.Select(file => file.Content), entries.Select(entry => entry.Bytes)); + } + + [Fact] + public async Task AChangedSiteFileChangesTheArchiveAndItsName() + { + var site = await BuildSiteAsync(); + var original = SiteArchive.Build(site.Files); + var changed = site.Files + .Select(file => file.Path == "robots.txt" ? file with { Content = [.. file.Content, (byte)'#'] } : file) + .ToArray(); + + var other = SiteArchive.Build(changed); + + Assert.NotEqual(original, other); + Assert.NotEqual(SiteArchive.Name(Digest(original)), SiteArchive.Name(Digest(other))); + } +} diff --git a/tests/provenance/csharp-candidate.test.ts b/tests/provenance/csharp-candidate.test.ts index e00e66f..1ad64ad 100644 --- a/tests/provenance/csharp-candidate.test.ts +++ b/tests/provenance/csharp-candidate.test.ts @@ -5,7 +5,17 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { access, cp, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import { + access, + cp, + mkdtemp, + readdir, + readFile, + rename, + rm, + stat, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { test } from "node:test"; @@ -367,3 +377,96 @@ test("the seal lists every member and no member is a link or a source map", asyn "A source map is published", ); }); + +// The Site release asset. The candidate seals one deterministic archive of the Site beside the deployed assets; the seal +// records its digest, its name is that digest, and the verifier refuses any other bytes. +const siteArchiveName = async (directory: string) => + (await listFiles(directory)).find((file) => /^web-site-[0-9a-f]{64}\.tar$/u.test(file)) ?? ""; + +/** Flips the low bit of the first content byte: the first entry's content starts after its 512-byte header. */ +function flipContent(bytes: Buffer): Buffer { + const flipped = Buffer.from(bytes); + flipped[512] = (flipped[512] ?? 0) ^ 0x01; + return flipped; +} + +/** The entry names of a plain ustar archive, read from the 512-byte headers (no content parsing beyond the size field). */ +function tarNames(archive: Buffer): string[] { + const names: string[] = []; + for (let offset = 0; offset + 512 <= archive.length; ) { + const head = archive.subarray(offset, offset + 512); + if (head.every((byte) => byte === 0)) break; + names.push(head.subarray(0, 100).toString("latin1").replace(/\0[\s\S]*$/u, "")); + const size = Number.parseInt( + head.subarray(124, 135).toString("latin1").replace(/\0[\s\S]*$/u, "").trim(), + 8, + ); + offset += 512 + Math.ceil(size / 512) * 512; + } + return names; +} + +test("the seal records one Site archive named by its digest, outside the deployed assets", async () => { + const name = await siteArchiveName(baseline); + assert.match(name, /^web-site-[0-9a-f]{64}\.tar$/u); + const bytes = await readFile(path.join(baseline, name)); + assert.equal(name, `web-site-${sha(bytes)}.tar`, "The Site archive name is not its digest"); + const manifest = JSON.parse(await readFile(path.join(baseline, "manifest.json"), "utf8")); + assert.equal(manifest.files[name], sha(bytes), "The seal does not record the Site archive digest"); + assert(!name.startsWith("assets/"), "The Site archive must stay outside the deployed assets"); + assert.deepEqual( + await readFile(path.join(repeat, name)), + bytes, + "Two candidate builds disagree on the Site archive", + ); + assert.equal(tarNames(bytes)[0], "404.css", "The Site archive holds the Site files in ordinal order"); +}); + +test("a Site archive changed after sealing is refused by the seal", async () => { + const dir = await copyCandidate(baseline, "site-archive-changed"); + const name = await siteArchiveName(dir); + await writeFile(path.join(dir, name), flipContent(await readFile(path.join(dir, name)))); + expectRefusal(verify(dir, source), `Candidate changed: ${name}`); +}); + +test("a resealed Site archive with other bytes is refused by the regenerated Site", async () => { + const dir = await copyCandidate(baseline, "site-archive-resealed"); + const name = await siteArchiveName(dir); + const tampered = flipContent(await readFile(path.join(dir, name))); + await rm(path.join(dir, name)); + await writeFile(path.join(dir, `web-site-${sha(tampered)}.tar`), tampered); + await reseal(dir); + expectRefusal(verify(dir, source), "The Site archive differs from the regenerated Site."); +}); + +test("a resealed Site archive under a name that is not its digest is refused", async () => { + const dir = await copyCandidate(baseline, "site-archive-renamed"); + const name = await siteArchiveName(dir); + await rename(path.join(dir, name), path.join(dir, `web-site-${"0".repeat(64)}.tar`)); + await reseal(dir); + expectRefusal(verify(dir, source), "The Site archive name does not match its digest."); +}); + +test("a second Site archive beside the sealed one is refused", async () => { + const dir = await copyCandidate(baseline, "site-archive-second"); + const name = await siteArchiveName(dir); + await writeFile(path.join(dir, `web-site-${"f".repeat(64)}.tar`), await readFile(path.join(dir, name))); + await reseal(dir); + expectRefusal(verify(dir, source), "The candidate must hold exactly one Site archive."); +}); + +test( + "the profile bundle does not serve the application shell's encodings at the root", + { skip: !havePublish }, + async () => { + const out = candidateDirectory("bundle-root-names"); + const built = await buildBundle(out); + assert.equal(built.status, 0, built.output); + const names = tarNames(await readFile(path.join(out, built.name))); + assert.equal(names[0], "manifest.json"); + for (const shell of ["index.html", "index.html.br", "index.html.gz"]) + assert(!names.includes(shell), `The bundle serves ${shell} at the root`); + for (const served of ["account/index.html", "chat/index.html", "_headers"]) + assert(names.includes(served), `The bundle does not hold ${served}`); + }, +); diff --git a/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs b/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs index 24590b7..e8e09d3 100644 --- a/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs +++ b/tools/ArcForges.Web.Tooling/Candidate/CandidateBuilder.cs @@ -54,6 +54,9 @@ void Add(string path, byte[] bytes) foreach (var file in site.Files) Add(CandidateCore.AssetsPrefix + file.Path, file.Content); + // The Site archive is a root member, outside the deployed assets directory: the seal records its digest. + var siteArchive = SiteArchive.Build(site.Files); + Add(SiteArchive.Name(CandidateCore.Sha256(siteArchive)), siteArchive); Add(CandidateCore.AssetsPrefix + "license.txt", CandidateCore.Licence(repository)); Add(CandidateCore.AssetsPrefix + "third-party-notices.txt", CandidateCore.Notices(repository)); Add(CandidateCore.AssetsPrefix + "source-provenance.txt", CandidateCore.SourceNotice(repository)); diff --git a/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs b/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs index 7180f89..3622c2d 100644 --- a/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs +++ b/tools/ArcForges.Web.Tooling/Candidate/CandidateVerifier.cs @@ -127,6 +127,14 @@ private static async Task VerifySiteAsync(SortedDictionary tree, foreach (var file in site.Files) Require(tree[CandidateCore.AssetsPrefix + file.Path].SequenceEqual(file.Content), "Public Site bytes differ from the regenerated Site: " + file.Path); + + // The Site archive is sealed beside the assets: exactly one, named by the digest of its own bytes, and equal to the + // archive of the regenerated Site. Any other bytes, name or count is refused. + var archives = tree.Keys.Where(SiteArchive.IsName).ToArray(); + Require(archives.Length == 1, "The candidate must hold exactly one Site archive."); + var archive = archives[0]; + Require(archive == SiteArchive.Name(CandidateCore.Sha256(tree[archive])), "The Site archive name does not match its digest."); + Require(tree[archive].SequenceEqual(SiteArchive.Build(site.Files)), "The Site archive differs from the regenerated Site."); } private static void VerifyStaticGraph(SortedDictionary tree) diff --git a/tools/ArcForges.Web.Tooling/Candidate/SiteArchive.cs b/tools/ArcForges.Web.Tooling/Candidate/SiteArchive.cs new file mode 100644 index 0000000..d496f26 --- /dev/null +++ b/tools/ArcForges.Web.Tooling/Candidate/SiteArchive.cs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0-only +using System.Text.RegularExpressions; +using ArcForges.Web.Site; +using ArcForges.Web.Tooling.Profiles; + +namespace ArcForges.Web.Tooling.Candidate; + +/// +/// The Site release asset: the deterministic ustar archive of the C# static Site output, named by the digest of its own +/// bytes (web-site-<sha256>.tar). It is written by the same ustar writer as the profile bundle, so its entries are +/// plain regular files in ordinal path order with fixed metadata, and equal Site output is the same bytes. +/// The archive sits at the root of the sealed candidate, beside the assets directory that Cloudflare deploys, so the seal +/// records its digest and the verifier refuses any other bytes. It is published only by the main-push release, beside +/// web-profiles. It is not part of the deployed assets. +/// +internal static partial class SiteArchive +{ + /// The archive file name for a digest. + public static string Name(string digest) => "web-site-" + digest + ".tar"; + + /// Writes the Site files, in ordinal path order, as one archive. + public static byte[] Build(IReadOnlyList files) + { + ArgumentNullException.ThrowIfNull(files); + var entries = files + .OrderBy(file => file.Path, StringComparer.Ordinal) + .Select(file => new TarArchive.Entry(file.Path, file.Content)) + .ToArray(); + return TarArchive.Write(entries); + } + + /// The root file name of a Site archive: its name pattern, with the digest of its own bytes. + public static bool IsName(string path) => NamePattern().IsMatch(path); + + [GeneratedRegex(@"^web-site-[0-9a-f]{64}\.tar$", RegexOptions.CultureInvariant)] + private static partial Regex NamePattern(); +} diff --git a/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs b/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs index 923b40d..06214ab 100644 --- a/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs +++ b/tools/ArcForges.Web.Tooling/Profiles/ProfileBundle.cs @@ -21,7 +21,8 @@ public sealed record ProfileBundleSummary(string Name, string Digest, int Member /// The archive keeps the React bundle's name pattern (web-profiles-<sha256>.tar), its first entry /// (manifest.json), its root _headers with one Content-Security-Policy per profile path, and its /// <profile>/index.html pages. Blazor's framework files are root-relative under the page's base href="/", -/// so the published tree is served at its own root paths and assets/ is empty in this bundle. +/// so the published tree is served at its own root paths and assets/ is empty in this bundle. The shell and its +/// precompressed siblings (index.html.br, index.html.gz) are not served at the root; every other file is. /// public static partial class ProfileBundle { @@ -37,6 +38,12 @@ public static partial class ProfileBundle /// The application shell. It is the source of every profile page and is not served at the root. public const string ShellPath = "index.html"; + /// + /// The shell's precompressed siblings that the SDK writes. They are encodings of the shell, so they are not served at the + /// root either. Every other file of the publish is kept. + /// + public static readonly string[] ShellEncodingPaths = ["index.html.br", "index.html.gz"]; + /// The profile paths. The one published application serves both routes. public static readonly string[] Profiles = ["account", "chat"]; @@ -77,7 +84,7 @@ public static ProfileBundleResult Build(string publishDirectory) var served = new SortedDictionary(StringComparer.Ordinal); foreach (var (path, bytes) in tree) { - if (path == ShellPath) + if (path == ShellPath || ShellEncodingPaths.Contains(path, StringComparer.Ordinal)) continue; if (path == HeadersPath || path == ManifestPath || Profiles.Any(profile => path.StartsWith(profile + "/", StringComparison.Ordinal))) throw new InvalidOperationException("The publish output uses a reserved bundle path: " + path); @@ -184,6 +191,8 @@ public static ProfileBundleSummary Verify(byte[] archive, string? expectedDigest Require(row["sha256"]?.GetValue() == CandidateCore.Sha256(entry.Bytes), "Content of " + entry.Path); } Require(rest.All(entry => entry.Path != ShellPath), "The application shell must not be served at the root."); + Require(rest.All(entry => !ShellEncodingPaths.Contains(entry.Path, StringComparer.Ordinal)), + "The application shell's encodings must not be served at the root."); var members = rest.ToDictionary(entry => entry.Path, entry => entry.Bytes, StringComparer.Ordinal); var policies = new SortedDictionary(StringComparer.Ordinal); From c58e5280f0d54c2c754c1806e41de23360526e45 Mon Sep 17 00:00:00 2001 From: sammiller Date: Fri, 9 Oct 2026 14:40:14 +0100 Subject: [PATCH 38/38] [WEB.40] Read the Site archive without ls in the main-push release and admit web-40-admission-r17 The release step located web-site-.tar with ls, which actionlint flags as SC2012 in the hosted quality job. It now collects the archive with a nullglob bash array, requires exactly one archive, and requires the archive name to equal web-site-.tar, all fail-closed. Only that script changes in ci.yml. The successor receipt web-40-admission-r17 chains from r16 and changes only the ci.yml input hash, the review metadata that names its predecessor, and the dependency-policy and provenance inventory entries that point at it. r16 is not edited. Co-Authored-By: Claude Haiku 5.5 --- .github/workflows/ci.yml | 10 +- eng/policy/dependency-policy.json | 10 +- .../web-40-admission-r17.json | 851 ++++++++++++++++++ eng/provenance/files.json | 1 + 4 files changed, 865 insertions(+), 7 deletions(-) create mode 100644 eng/policy/dependency-reviews/web-40-admission-r17.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bed6e66..8061f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -410,9 +410,15 @@ jobs: VERSION=$(node --input-type=module -e 'import fs from "node:fs"; console.log(JSON.parse(fs.readFileSync("artifacts/candidate/manifest.json")).version)') # The Site archive (web-site-.tar) is the candidate's one sealed root member, verified by npm run deploy above. # It is published here beside web-profiles, and only by this main-push release. It is not a deployed asset. - test "$(ls artifacts/candidate/web-site-*.tar | wc -l)" = 1 + shopt -s nullglob + site_archives=(artifacts/candidate/web-site-*.tar) + shopt -u nullglob + test "${#site_archives[@]}" = 1 + site_archive="${site_archives[0]}" + site_digest=$(sha256sum "$site_archive" | cut -d ' ' -f 1) + test "$(basename "$site_archive")" = "web-site-${site_digest}.tar" tar -czf "artifacts/web-${VERSION}.tar.gz" -C artifacts/candidate . - gh release create "web-${VERSION}" "artifacts/web-${VERSION}.tar.gz" artifacts/deployment.json artifacts/profiles/web-profiles-*.tar artifacts/candidate/web-site-*.tar --target "$GITHUB_SHA" --title "Web ${VERSION}" --prerelease --generate-notes + gh release create "web-${VERSION}" "artifacts/web-${VERSION}.tar.gz" artifacts/deployment.json artifacts/profiles/web-profiles-*.tar "$site_archive" --target "$GITHUB_SHA" --title "Web ${VERSION}" --prerelease --generate-notes - name: Preserve deployment evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/eng/policy/dependency-policy.json b/eng/policy/dependency-policy.json index 427c212..0591c62 100644 --- a/eng/policy/dependency-policy.json +++ b/eng/policy/dependency-policy.json @@ -6,7 +6,7 @@ "package.json" ], "inputs": { - ".github/workflows/ci.yml": "aa1d456b4a196bed61b3cca281545a1c95395f9361ad15eec5862611b4e0c66a", + ".github/workflows/ci.yml": "45457067f67bf6c94f3c237a4217004f2cf5b78970fa47096d73ab9501c2034a", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", @@ -50,9 +50,9 @@ "owner": "Web dependency maintainer", "reviewer": "w-deku-20261008-rev-web-40", "date": "2026-10-09", - "rationale": "WEB.40 successor of web-40-admission-r15 for the S20(a) Site release asset (CLOUD.85 decisions D2 option A and D3). The one bound input that changed is .github/workflows/ci.yml: the main-push release now publishes web-site-.tar beside web-profiles, guarded so that exactly one Site archive is in the sealed candidate. The Site archive is a deterministic root member of the candidate, sealed by its digest and verified byte for byte against the regenerated Site; it is not part of the Cloudflare assets directory. The profile bundle drops the shell's precompressed siblings (index.html.br, index.html.gz) from the root. No other bound input changed: the csproj files, packages.lock.json files, props, global.json, package.json, package-lock.json, wrangler.json, .node-version, .npmrc, NuGet.config and the browser-resources profile and record r11 and r12 keep their digests. Reviewer w-deku-20261008-rev-web-40; reviewed base efe787254ae64cc8027a6b801b3cbd9080481321. No package, version, source, licence, contentHash, nuspecSha256, closure coordinate or scanner exception changed: packages, scannerExceptions, packsFolderRows, con07Identity, browserWasmPacks, nugetClosure and nugetPackDownloads are copied from r15 unchanged. r15 is not edited. Verification is local only: the local Node is 24.20 against the 24.21 pin, and actionlint, gitleaks and Docker are not installed; the hosted quality and csharp jobs at the exact head are the authority, and the exact-head review by w-deku-20261008-rev-web-40 remains a merge condition.", + "rationale": "WEB.40 successor of web-40-admission-r16 for the S20(a) Site release asset (CLOUD.85 decisions D2 option A and D3). The one bound input that changed since r16 is .github/workflows/ci.yml: the main-push release step that locates the Site archive no longer parses ls output (actionlint SC2012 in the hosted quality job at 4cca32e). It now collects web-site-*.tar with a nullglob bash array, requires exactly one archive, and requires the archive name to equal web-site-.tar, all fail-closed; the release then attaches that single archive. The Site archive is a deterministic root member of the candidate, sealed by its digest and verified byte for byte against the regenerated Site; it is not part of the Cloudflare assets directory. The profile bundle drops the shell's precompressed siblings (index.html.br, index.html.gz) from the root, as in r16. No other bound input changed: the csproj files, packages.lock.json files, props, global.json, package.json, package-lock.json, wrangler.json, .node-version, .npmrc, NuGet.config and the browser-resources profile and record r11 and r12 keep their digests. Reviewer w-deku-20261008-rev-web-40; reviewed base efe787254ae64cc8027a6b801b3cbd9080481321. No package, version, source, licence, contentHash, nuspecSha256, closure coordinate or scanner exception changed: packages, scannerExceptions, packsFolderRows, con07Identity, browserWasmPacks, nugetClosure and nugetPackDownloads are copied from r16 unchanged. r16 is not edited. Verification is local only: actionlint reports no findings for .github/workflows/*.yml, the new release snippet was exercised in bash with zero, one, two and name-mismatched archives, and shellcheck, gitleaks and Docker are not installed locally, so the shell check is the hosted actionlint run at the exact head; the hosted quality and csharp jobs are the authority, and the exact-head review by w-deku-20261008-rev-web-40 remains a merge condition.", "inputs": { - ".github/workflows/ci.yml": "aa1d456b4a196bed61b3cca281545a1c95395f9361ad15eec5862611b4e0c66a", + ".github/workflows/ci.yml": "45457067f67bf6c94f3c237a4217004f2cf5b78970fa47096d73ab9501c2034a", ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", @@ -103,7 +103,7 @@ ], "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site and its Site archive; the deploy job ships the same sealed bytes under the unchanged main-push guards, and the Site archive is a release asset only, never a deployed asset. No hosted browser, device, macOS or live-service check is added.", "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", - "previous": "eng/policy/dependency-reviews/web-40-admission-r15.json", + "previous": "eng/policy/dependency-reviews/web-40-admission-r16.json", "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." }, "closure": { @@ -811,7 +811,7 @@ "unenv": "2.0.0-rc.24", "youch": "4.1.0-beta.10" }, - "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r16.json", + "reviewRecord": "eng/policy/dependency-reviews/web-40-admission-r17.json", "dependencyClasses": { "build-inputs": "Complete exact npm lock inventory; licence metadata is preserved, not inferred as a redistribution grant. Existing transitive development prereleases are named build-only migration inputs and cannot enter runtime/stable closures.", "runtime": "Only the independently enforced existing shipped artifact records admit runtime inputs and legal scopes.", diff --git a/eng/policy/dependency-reviews/web-40-admission-r17.json b/eng/policy/dependency-reviews/web-40-admission-r17.json new file mode 100644 index 0000000..47f9328 --- /dev/null +++ b/eng/policy/dependency-reviews/web-40-admission-r17.json @@ -0,0 +1,851 @@ +{ + "review": { + "owner": "Web dependency maintainer", + "reviewer": "w-deku-20261008-rev-web-40", + "date": "2026-10-09", + "rationale": "WEB.40 successor of web-40-admission-r16 for the S20(a) Site release asset (CLOUD.85 decisions D2 option A and D3). The one bound input that changed since r16 is .github/workflows/ci.yml: the main-push release step that locates the Site archive no longer parses ls output (actionlint SC2012 in the hosted quality job at 4cca32e). It now collects web-site-*.tar with a nullglob bash array, requires exactly one archive, and requires the archive name to equal web-site-.tar, all fail-closed; the release then attaches that single archive. The Site archive is a deterministic root member of the candidate, sealed by its digest and verified byte for byte against the regenerated Site; it is not part of the Cloudflare assets directory. The profile bundle drops the shell's precompressed siblings (index.html.br, index.html.gz) from the root, as in r16. No other bound input changed: the csproj files, packages.lock.json files, props, global.json, package.json, package-lock.json, wrangler.json, .node-version, .npmrc, NuGet.config and the browser-resources profile and record r11 and r12 keep their digests. Reviewer w-deku-20261008-rev-web-40; reviewed base efe787254ae64cc8027a6b801b3cbd9080481321. No package, version, source, licence, contentHash, nuspecSha256, closure coordinate or scanner exception changed: packages, scannerExceptions, packsFolderRows, con07Identity, browserWasmPacks, nugetClosure and nugetPackDownloads are copied from r16 unchanged. r16 is not edited. Verification is local only: actionlint reports no findings for .github/workflows/*.yml, the new release snippet was exercised in bash with zero, one, two and name-mismatched archives, and shellcheck, gitleaks and Docker are not installed locally, so the shell check is the hosted actionlint run at the exact head; the hosted quality and csharp jobs are the authority, and the exact-head review by w-deku-20261008-rev-web-40 remains a merge condition.", + "inputs": { + ".github/workflows/ci.yml": "45457067f67bf6c94f3c237a4217004f2cf5b78970fa47096d73ab9501c2034a", + ".node-version": "73fb1b615e2043a933be1c0895cde4358036acc28d785692509b822aa53c761f", + ".npmrc": "73814024102037fcc5a961119bcbf39f81e0a73af504735653d3a6086651cef1", + "Directory.Build.props": "2c1597403bd2a3eb04480ef96ba7accd57c0d2a9f7176ca1073a2d7a6fdcf3a0", + "Directory.Build.targets": "66716168ad9b701833e0d750f27acf4962fc23bdac4c8f121ea57635d5116c35", + "Directory.Packages.props": "95e73db26cdb9ba15ccd3c752d804be4ee083f85fdf589acf837121a2c3975b7", + "NuGet.config": "cb93c65e28718aa9075fce221e9f9cea9e72d3fe1b30462383448a591f5b3aa1", + "eng/provenance/profiles/browser-resources-r11.json": "e9c496ff9b97757b4845b08abe07eec456bcc103b8454fd2447b7dbacbd53177", + "eng/provenance/profiles/browser-resources-r12.json": "5dd1d44a13562b616f156648909d7f4d4d2e67d40bc802c70df392da46e0d6ee", + "eng/provenance/records/browser-resources-r11.json": "15f6bb8a2157107e197b85ae799c53fc8b934a0ac463d141e0fc7bb5922f3bf8", + "eng/provenance/records/browser-resources-r12.json": "6b470604b64d692d0eaa215be133506af065856dde0fde9ee8e702bba883bb88", + "global.json": "67381be18aa807c04218165844967cf58235875477f30495ba98c3648248a9d4", + "package-lock.json": "392b8874131b713ac07ab595ff541122fa3c2317f9ba81b34ffe6115f4363825", + "package.json": "60b8b8e853ae2ac0dfec58831d61c717b3e4fb26ed3848745b36e9a2ec370ea5", + "src/ArcForges.Web.App/ArcForges.Web.App.csproj": "3b7e2424cf6f9ce1d448cfe462ba825c7a4a486b77189c2f63ef665c52f9919b", + "src/ArcForges.Web.App/packages.lock.json": "6ae92b481b6a892a9b4952f7e4b8da56b7caac8f6a6206151b017108e2c99229", + "src/ArcForges.Web.Operations/ArcForges.Web.Operations.csproj": "ad7f8197fada085fb3ba6bee69f1d45716e3b79725ed7d388f0a9080ae290077", + "src/ArcForges.Web.Operations/packages.lock.json": "75bd9b2ebbe6ed307c134473be0319fd536a7733ce00fe1237148299e5f59239", + "src/ArcForges.Web.Site/ArcForges.Web.Site.csproj": "3262ccbf711251e32859c732422b472975f3e4269b31814f9df298f1be698344", + "src/ArcForges.Web.Site/packages.lock.json": "61d7150f179a77464386591f7a8c38cdbadb95b9c29fff18fe2ef912bef91931", + "src/ArcForges.Web.Ui/ArcForges.Web.Ui.csproj": "2c922134e6471da2db5194204b52b7722a341a54d6eee4db17b5c6459c631fa6", + "src/ArcForges.Web.Ui/packages.lock.json": "dfa414cbeec347e07835b0ff8f19f8b020f967f57d0267edb6eb206e4c2b65f8", + "tests/ArcForges.Web.App.Tests/ArcForges.Web.App.Tests.csproj": "d9044cf90b86c7a380fc1a5a24a1539ad94507b307dc2ba045e4c32d722f98c6", + "tests/ArcForges.Web.App.Tests/packages.lock.json": "5939781e73182199cd09e6bf6baf91a74bf2111e67a2379c073afdcb53ba75ff", + "tests/ArcForges.Web.Operations.Tests/ArcForges.Web.Operations.Tests.csproj": "85d50eb3e78de52b2f2fc38cbeed1b5ec8d0875344bbe5dc3a279697a2c25f6c", + "tests/ArcForges.Web.Operations.Tests/packages.lock.json": "e7446b75088edc5a49877ec5b4927c0d7bf65e523a1cc76a8c626a547bc7917b", + "tests/ArcForges.Web.Policy.Tests/ArcForges.Web.Policy.Tests.csproj": "bce3090ee3c4fc71a4d90802f75e15ebefdf2027f4dcc5b86e9cb485fbe24742", + "tests/ArcForges.Web.Policy.Tests/packages.lock.json": "d7f4dfec2064957427d7daabf5a99a95fead21d916f1b958976fd81c22d6dc3f", + "tests/ArcForges.Web.Site.Tests/ArcForges.Web.Site.Tests.csproj": "46e0e121cd17ffbe4e16bf3c83795c4129ad990c020c038ae9192184c4ee4be0", + "tests/ArcForges.Web.Site.Tests/packages.lock.json": "15eb52424732fb8bf0a7479acf0c323b9a36c7e1503c3369bf92f402319b55e0", + "tests/ArcForges.Web.Tooling.Tests/ArcForges.Web.Tooling.Tests.csproj": "5e9db68756a4b34f65529305d47dcdf620ba1e3afb518c12e46457f091f08d37", + "tests/ArcForges.Web.Tooling.Tests/packages.lock.json": "90f22f0780280e371cc5f892a0e41d1a68b9886b3543ed3293ed276d5eebe1a7", + "tests/ArcForges.Web.Ui.Tests/ArcForges.Web.Ui.Tests.csproj": "ebc006964e176e7c71b7a36b3e94a030fa6a2d807450ab6ef8e829c7ebe2177e", + "tests/ArcForges.Web.Ui.Tests/packages.lock.json": "482a8749cf737188e1106d77c1d657ed5c1c9626317be6e337c59e2b3e46793b", + "tests/browser/ArcForges.Web.Browser.Tests/ArcForges.Web.Browser.Tests.csproj": "6d113146859ece321fad7c614ae1680a5219ce8ab22af01c7d1a60291d2bd2a7", + "tests/browser/ArcForges.Web.Browser.Tests/packages.lock.json": "931e944d1c7c4862d63a42da064af96b46e45e3f87203a45844fef12fc1708cd", + "tools/ArcForges.Web.Tooling/ArcForges.Web.Tooling.csproj": "91bf31b448f0d9b06fe73d4daebb5971e09d727fc1a0541db5047734df3f9fe4", + "tools/ArcForges.Web.Tooling/packages.lock.json": "045a81bc88f39433f4e088019d028ef57c8ad60dd9bc496e50ff1d504b09c3e6", + "wrangler.json": "b597ad91281c4d77c504dfeebace4aa7b8ce2f9bc724dbe40978643b23171c07" + }, + "checks": [ + "locked-restore", + "licence-provenance", + "security", + "class-compilation", + "compatibility", + "sbom", + "framework-posture" + ], + "runtimeAssessment": "The deployed public candidate is the C# static Site with no runtime JavaScript; the Worker stays the thin TypeScript adapter (worker/index.ts), emitted by Node type stripping. Node remains build and deploy tooling for wrangler only. The candidate job enforces the Blazor profile budgets and seals the Site and its Site archive; the deploy job ships the same sealed bytes under the unchanged main-push guards, and the Site archive is a release asset only, never a deployed asset. No hosted browser, device, macOS or live-service check is added.", + "maintenance": "The npm closure is pinned exactly and admitted by this receipt; any change needs a successor receipt that names this file. The Contracts identity is the NuGet publication record eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1, bound by eng/version-sources.json and Directory.Packages.props; any change needs a successor record. Published Contracts npm versions stay immutable on the registry and Web no longer consumes them. Historical receipts stay immutable.", + "previous": "eng/policy/dependency-reviews/web-40-admission-r16.json", + "immutableHistory": "Admitted review records are retained in Git history. A dependency change requires a new reviewed source commit and updated input-bound rationale; never amend an already accepted provenance record." + }, + "packages": { + "@cloudflare/kv-asset-handler@0.5.0": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "@cloudflare/unenv-preset@2.16.2": "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ==", + "@cloudflare/workerd-darwin-64@1.20260926.1": "sha512-VCIpwgJu5Dm1o+VHLclNOKIdpttEqss7oDHS5DYxgVoonM9Dxxxr1xCQvJbh6IXFuQ1DDrbm5krjte5NsTgX3Q==", + "@cloudflare/workerd-darwin-arm64@1.20260926.1": "sha512-oVuLXfCnr1Xu9sBMNYrEPcFZQNMcviNcacz9l+oqQiLHc5UrVp++lpKbsuWstCfh+/c39Pp37TVeaNBLA8+ReA==", + "@cloudflare/workerd-linux-64@1.20260926.1": "sha512-BjmiDvYBVBG5248gCHSZS3yONbcm3/3qKTu9RaQA8o7jQHJfXx9JnWbhkld2KsbXnRGEGHFwpLLidIpIGtQYUA==", + "@cloudflare/workerd-linux-arm64@1.20260926.1": "sha512-28bdfQDIFcpaKXMPisADy8BchpIsX+tEAhD+bhfEBXukFGFr8F3ouyiG3HzN3CqAh/OYvifJizzFZtptUPCy7w==", + "@cloudflare/workerd-windows-64@1.20260926.1": "sha512-ehdL3ataKdEmlnW9oom6OZUR8n3XsTL/zl8+KGREpLi5sMSW0mek0PrF4wK9SX51f3Alnlu8tF8eKBg+mc3xFw==", + "@cspotcode/source-map-support@0.8.1": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "@jridgewell/trace-mapping@0.3.9": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "@emnapi/runtime@1.11.3": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "@esbuild/aix-ppc64@0.28.1": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "@esbuild/android-arm@0.28.1": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "@esbuild/android-arm64@0.28.1": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "@esbuild/android-x64@0.28.1": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "@esbuild/darwin-arm64@0.28.1": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "@esbuild/darwin-x64@0.28.1": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "@esbuild/freebsd-arm64@0.28.1": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "@esbuild/freebsd-x64@0.28.1": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "@esbuild/linux-arm@0.28.1": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "@esbuild/linux-arm64@0.28.1": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "@esbuild/linux-ia32@0.28.1": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "@esbuild/linux-loong64@0.28.1": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "@esbuild/linux-mips64el@0.28.1": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "@esbuild/linux-ppc64@0.28.1": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "@esbuild/linux-riscv64@0.28.1": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "@esbuild/linux-s390x@0.28.1": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "@esbuild/linux-x64@0.28.1": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "@esbuild/netbsd-arm64@0.28.1": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "@esbuild/netbsd-x64@0.28.1": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "@esbuild/openbsd-arm64@0.28.1": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "@esbuild/openbsd-x64@0.28.1": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "@esbuild/openharmony-arm64@0.28.1": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "@esbuild/sunos-x64@0.28.1": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "@esbuild/win32-arm64@0.28.1": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "@esbuild/win32-ia32@0.28.1": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "@esbuild/win32-x64@0.28.1": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "@img/colour@1.1.0": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "@img/sharp-darwin-arm64@0.35.5": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "@img/sharp-darwin-x64@0.35.5": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "@img/sharp-freebsd-wasm32@0.35.5": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "@img/sharp-libvips-darwin-arm64@1.3.4": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "@img/sharp-libvips-darwin-x64@1.3.4": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "@img/sharp-libvips-linux-arm@1.3.4": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "@img/sharp-libvips-linux-arm64@1.3.4": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "@img/sharp-libvips-linux-ppc64@1.3.4": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "@img/sharp-libvips-linux-riscv64@1.3.4": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "@img/sharp-libvips-linux-s390x@1.3.4": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "@img/sharp-libvips-linux-x64@1.3.4": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "@img/sharp-libvips-linuxmusl-arm64@1.3.4": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "@img/sharp-libvips-linuxmusl-x64@1.3.4": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "@img/sharp-linux-arm@0.35.5": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "@img/sharp-linux-arm64@0.35.5": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "@img/sharp-linux-ppc64@0.35.5": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "@img/sharp-linux-riscv64@0.35.5": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "@img/sharp-linux-s390x@0.35.5": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "@img/sharp-linux-x64@0.35.5": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "@img/sharp-linuxmusl-arm64@0.35.5": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "@img/sharp-linuxmusl-x64@0.35.5": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "@img/sharp-wasm32@0.35.5": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "@img/sharp-webcontainers-wasm32@0.35.5": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "@img/sharp-win32-arm64@0.35.5": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "@img/sharp-win32-ia32@0.35.5": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "@img/sharp-win32-x64@0.35.5": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "@jridgewell/resolve-uri@3.1.2": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "@jridgewell/sourcemap-codec@1.6.0": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "@poppinss/colors@4.1.6": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "@poppinss/dumper@0.6.5": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "@poppinss/exception@1.2.3": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "@sindresorhus/is@7.2.0": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "@speed-highlight/core@1.2.24": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "@types/node@24.13.5": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "@typescript/typescript-aix-ppc64@7.0.2": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "@typescript/typescript-darwin-arm64@7.0.2": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "@typescript/typescript-darwin-x64@7.0.2": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "@typescript/typescript-freebsd-arm64@7.0.2": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "@typescript/typescript-freebsd-x64@7.0.2": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "@typescript/typescript-linux-arm@7.0.2": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "@typescript/typescript-linux-arm64@7.0.2": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "@typescript/typescript-linux-loong64@7.0.2": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "@typescript/typescript-linux-mips64el@7.0.2": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "@typescript/typescript-linux-ppc64@7.0.2": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "@typescript/typescript-linux-riscv64@7.0.2": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "@typescript/typescript-linux-s390x@7.0.2": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "@typescript/typescript-linux-x64@7.0.2": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "@typescript/typescript-netbsd-arm64@7.0.2": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "@typescript/typescript-netbsd-x64@7.0.2": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "@typescript/typescript-openbsd-arm64@7.0.2": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "@typescript/typescript-openbsd-x64@7.0.2": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "@typescript/typescript-sunos-x64@7.0.2": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "@typescript/typescript-win32-arm64@7.0.2": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "@typescript/typescript-win32-x64@7.0.2": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "blake3-wasm@2.1.5": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "cookie@1.1.1": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "detect-libc@2.1.2": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "error-stack-parser-es@1.0.5": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "esbuild@0.28.1": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "fsevents@2.3.3": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "kleur@4.1.5": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "miniflare@5.20260926.1-alpha": "sha512-vc4lgl8YVxR2aundPnVPTIcL46UQKK+ebW7yinXNE1Y9joFS0JoakMazr7r3M8DXRvaemKS8EEmszkv/b05qUw==", + "undici@7.29.1": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "path-to-regexp@6.3.0": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "pathe@2.0.3": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "semver@7.8.5": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "sharp@0.35.5": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "supports-color@10.2.2": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "tslib@2.8.1": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "typescript@7.0.2": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "undici-types@7.18.2": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "unenv@2.0.0-rc.24": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "workerd@1.20260926.1": "sha512-YojhsWBuZwk3Jk9OC0QfTgVbLeQOD1tK1olC9ZXAUcmeip8z4NE/xtttpxCsuTvQBRqppvy7Tkac0kxPYMjIXA==", + "wrangler@4.143.1": "sha512-ilQJRMRcMgdk82XWIg7hlgeB1I2CoOWmSiJeoqZufoNCWUiJTN5DaYPYb6M/oIXBXk3+IQIxNDgdtPUuxcxdeg==", + "ws@8.21.0": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "youch@4.1.0-beta.10": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "youch-core@0.3.3": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==" + }, + "supersedes": "eng/policy/dependency-reviews/web-40-admission-r16.json", + "reviewedBase": "efe787254ae64cc8027a6b801b3cbd9080481321", + "scannerExceptions": [ + { + "file": ".gitleaks.toml", + "introducedBy": "389b9fd746eefb80496263ae83072adb04f1a6a2", + "findingCommit": "2a5e17f", + "findingPath": "eng/contracts/ArcForges.Contracts.PublicApi/1.0.0-ci.287.1/source.json", + "findingLine": 9, + "findingKey": "public/http/v1/native-auth.schema.json", + "findingValuePrefix": "e7807ae0", + "rule": "generic-api-key", + "digest": "e7807ae0ab6b4530d817941a922d7df90e4feb525c68d81a245a4c089ad3d296", + "digestDefinition": "SHA-256 of schemas/public/http/v1/native-auth.schema.json in arcforges.contracts.publicapi 1.0.0-ci.287.1", + "status": "verified false positive", + "allowlist": { + "description": "Reviewed public schema digest in the CON.07 Contracts publication record: the key name contains auth, and the value is the SHA-256 of the packaged public schema (verified against the NuGet package bytes)", + "condition": "AND", + "targetRules": [ + "generic-api-key" + ], + "regexTarget": "line", + "paths": [ + "^eng/contracts/ArcForges\\.Contracts\\.PublicApi/1\\.0\\.0-ci\\.287\\.1/source\\.json$" + ], + "regexes": [ + "^\\s*\"public/http/v1/native-auth\\.schema\\.json\": \"e7807ae0ab6b4530d817941a922d7df90e4feb525c68d81a245a4c089ad3d296\",?$" + ] + } + } + ], + "packsFolderRows": [ + { + "key": "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12", + "platform": "Windows", + "packsFolder": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", + "version": "10.0.12", + "marker": "runtimes/browser-wasm/native/dotnet.native.wasm", + "reason": "The hosted windows-2025-vs2026 image (runner image 20260925.250.1, CI job 113727560820) preinstalls the wasm.tools workload into C:\\Program Files\\dotnet, which holds this pack at the bundled version 10.0.12 (the same run's build lists the 10.0.12 Emscripten workload packs in that folder), so the SDK adds no NuGet download. A local machine whose packs folder lacks 10.0.12 restores the NuGet copy and it is verified by digest there." + }, + { + "key": "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12", + "platform": "Linux", + "packsFolder": "Microsoft.NETCore.App.Runtime.Mono.browser-wasm", + "version": "10.0.12", + "marker": "runtimes/browser-wasm/native/dotnet.native.wasm", + "reason": "The Ubuntu hosted image carries no WebAssembly workload, so this row is checked there and not satisfied: the SDK adds the implicit pack as a NuGet download, restores it and the test verifies its digest. The NuGet copy is skipped only when a 10.0.12 packs copy with the marker exists locally, and in that case the SDK does not download the pack either." + } + ], + "con07Identity": { + "ledger": "Plan ledger/tasks/con-07.md, status complete, recorded 2026-10-02", + "version": "1.0.0-ci.287.1", + "mergeCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "publicationRun": "https://github.com/ArcForges/Contracts/actions/runs/36966686225", + "verifiedAt": "2026-10-08", + "method": "nuget.org flat-container index (version listed) and nuspec (repository commit, licence, SHA-256) per package", + "packages": [ + { + "id": "ArcForges.Contracts.PublicApi", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "f18300e6b837a6e8ccafb56d77de97fe74ed6437f6a2952ba82e9d3eff6e121f" + }, + { + "id": "ArcForges.Contracts.Foundation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "72f9ffbff6488f970b48d7b35458c0e0eb4223865caea9adc16fc7105a393ad8" + }, + { + "id": "ArcForges.Contracts.CloudInternal", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "affe7a268a8acae0e9a3ea168b615f215f921b3ac481a73bfe3c0886c85fb647" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Chat", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "af38382283e1c7d1e425050a650aeb02c63bdff8543c3b68666161d80bf0c045", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Platform", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d9a60fe72441f5928d02aed3a92c3d9835f45af750f1511999d185e7b1a38e3b", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Sandbox", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "d0aa13ae853055a5a18efb918d9136cded089b73704c7ef82917ef636c0589da", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.LocalRpc.Scope", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "3533ec3b77ed0cedfaa8478ddad887038c3764309134fc871d80e2f2f184baa9", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Cli", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "cdae97a78340ce8276efe8d67373fbdaf2c1498203819c6045db9d2132abd490", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Contracts.Events", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "a732f6d69c500d569ab44966de8182d95495fad9d4df874a12838df85841b65c" + }, + { + "id": "ArcForges.Contracts.Validation", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "6fc4636e918c1510556afede34b23f73e8132613832e6e2f98058c4282c24d08" + }, + { + "id": "ArcForges.Sdk.Client", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "0d1d2efe6a3c6b7c27fbad668e30a096354636c07013369efc33f608c78488a7", + "nuspecSha256Status": "unverified: the 1.0.0-ci.287.1 nuspec is not in the local NuGet packages folder; the 2026-10-08 value is kept and is not re-derived under the raw-bytes convention" + }, + { + "id": "ArcForges.Sdk.Contracts", + "versionListed": true, + "repositoryCommit": "ca45f36cccbdd31380f76b8f6cdecc958fdcb430", + "licence": "Apache-2.0", + "nuspecSha256": "bc55aa1371a4ce334c64b54150011444fe5880d4cd264e73fc1ff1136889c9ff" + } + ], + "admittedClosureRows": [ + "arcforges.contracts.foundation/1.0.0-ci.287.1", + "arcforges.contracts.publicapi/1.0.0-ci.287.1", + "google.protobuf/3.36.1", + "grpc.core.api/2.84.0" + ] + }, + "browserWasmPacks": { + "sdk": "10.0.401 (BundledNETCoreAppPackageVersion 10.0.12)", + "admitted": [ + "microsoft.net.runtime.webassembly.sdk/10.0.12", + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12" + ], + "refused": [ + { + "id": "Microsoft.AspNetCore.App.Runtime.browser-wasm", + "reason": "no such package on nuget.org (flat-container BlobNotFound on 2026-10-08); not admitted" + } + ], + "localPacksFolder": "C:\\Program Files\\dotnet\\packs holds 10.0.11 browser-wasm packs from the local workload; the admitted 10.0.12 packs are the locked NuGet coordinates" + }, + "nugetClosure": { + "anglesharp.css/1.0.1": { + "contentHash": "6S13xNHH+SUGPZd6EO1MhkuDbpEnFroUM6A8DZpLJHQnLRTvtBJb3Ydu65s618E4gWF9FSWmM5jhKk4RIfwT2A==", + "licence": "MIT", + "nuspecSha256": "60ac56de204d91e27e538b57e00b56f7c31eac756fce5b9a9c2e8e6b9dec74e1", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.css/1.0.1/anglesharp.css.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp.diffing/1.1.1": { + "contentHash": "Uzz5895TpRyMA4t8GExmq4TxXqi3+bZTgb3kJgH+tdwrSuo9CO6Dlmv+Oqyhb2pNRfbnReLUzhlHAuT7c+3jSg==", + "licence": "MIT", + "nuspecSha256": "bb89bd205ef7a42d55c289a90251564734ea28d387c7d4e0ce43b5b989de15e4", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp.diffing/1.1.1/anglesharp.diffing.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "anglesharp/1.7.0": { + "contentHash": "v1R++46dblGRBo2/fKFUfgtZOaFnDGQhqBM0AarxgZSJuumj1e1vexBbjlTv2hx3Olr3qeoJa2yUoWyv5fuJ5A==", + "licence": "MIT", + "nuspecSha256": "ef30ecc9d4d7fe9bae40cc1c02fdc1d8653f578c7da486efda3e1b6e0b2476ef", + "source": "https://api.nuget.org/v3-flatcontainer/anglesharp/1.7.0/anglesharp.nuspec", + "classification": "Test-only HTML5 parser: a direct reference of tests/ArcForges.Web.Site.Tests (the WEB.40 normalised-DOM parity gate, 2026-10-09 adjudication) and a transitive dependency of the bUnit closure. Reached only from tests/**; never in a published output." + }, + "arcforges.build.policy/1.0.0-ci.94.1": { + "contentHash": "PCpeEXikk2m87zhMF6L8tbHe2QrI87mO4Nc5lJBenEDhOePwwV4TkArc6IaM1G11Pm8ojVjurLAIoPW22ozb/A==", + "licence": "AGPL-3.0-only", + "nuspecSha256": "bd85acc0b99828a7e9c52751a9d57059fbaa7130986871a562e37b7f6b971c7c", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.build.policy/1.0.0-ci.94.1/arcforges.build.policy.nuspec", + "classification": "Direct first-party AGPL build-policy package (GOV.03 inheritance) referenced with PrivateAssets all in every managed project; build-time analyzers and metadata only, never a published output." + }, + "arcforges.contracts.foundation/1.0.0-ci.287.1": { + "contentHash": "Jwe5Z6K57vDsKmcxQFbbwyQZQUQsOkzYH26ogQIRb/biRtaGxLCtOsNg4felCuNTJt9bwRmC6AGx3eJyCoCk/g==", + "licence": "Apache-2.0", + "nuspecSha256": "72f9ffbff6488f970b48d7b35458c0e0eb4223865caea9adc16fc7105a393ad8", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.foundation/1.0.0-ci.287.1/arcforges.contracts.foundation.nuspec", + "classification": "Transitive Apache-2.0 foundation package of the CON.07 Contracts candidate 1.0.0-ci.287.1 (merge ca45f36), required by ArcForges.Contracts.PublicApi; consumed as an exact published package." + }, + "arcforges.contracts.publicapi/1.0.0-ci.287.1": { + "contentHash": "/chAZbT/NTs4yyQBV5dgh37/dWBTKt3PHCikIidI9T90LheydsJoCWxKHGC6Tjzw0rqA62rYxnQfXGzKhTea5Q==", + "licence": "Apache-2.0", + "nuspecSha256": "f18300e6b837a6e8ccafb56d77de97fe74ed6437f6a2952ba82e9d3eff6e121f", + "source": "https://api.nuget.org/v3-flatcontainer/arcforges.contracts.publicapi/1.0.0-ci.287.1/arcforges.contracts.publicapi.nuspec", + "classification": "Direct Apache-2.0 Contracts publication of the CON.07 candidate 1.0.0-ci.287.1 (merge ca45f36), referenced by ArcForges.Web.Ui for the generated SayHello messages and the gRPC-Web service bindings. Consumed as an exact published package, never as source; identity verified in the web-40-admission-r2 receipt." + }, + "bunit/2.9.0": { + "contentHash": "Jz9ywwpjjYHppFDw4bIMkk5cNOy0vN74p3PzJFVIlCZicK8vpAovM5LsuNDLKE4bpwISNKrjwSrsw4SIVzT+0g==", + "licence": "MIT", + "nuspecSha256": "fb50d54a04677cab1b9386dcc4e4cff2fb3babfc9c2f0e5d0e6158f48ad64441", + "source": "https://api.nuget.org/v3-flatcontainer/bunit/2.9.0/bunit.nuspec", + "classification": "Direct bUnit component-test package for ArcForges.Web.Ui.Tests; test-only and never in a published output." + }, + "deque.axecore.commons/4.13.0": { + "contentHash": "roq2z6OmKY4KVXGj1lSigUbjE7DnIcYLqPOVcnWrusS6CI7oB6/He8Oo3ZO/TCOlnaTS1hkJOuKPwL+PyhyC6w==", + "licence": "MPL-2.0", + "testOnly": true, + "nuspecSha256": "6d1ad9cf06acab6eb22fe449e1aead325803b14ec00efd57224cdc1b376883c1", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.commons/4.13.0/deque.axecore.commons.nuspec", + "classification": "Test-only axe-core script carrier (MPL-2.0 is recorded by the WEB.40 2026-10-09 adjudication). File-level weak copyleft, accepted only for a row marked testOnly: no product project restores it, and it is never shipped or compiled into a product output. Referenced only through Deque.AxeCore.Playwright by tests/browser." + }, + "deque.axecore.playwright/4.13.0": { + "contentHash": "tqzqVEsQfVg4fmx9/AkbgYjVMYHHOGI7WkTznKuP8ayi0uaBcd+2skOfxnVdUBlEhpHdu9stzxOZ6uPVhIS2JA==", + "licence": "MIT", + "nuspecSha256": "6a77936de180b932c71a289bac7d638be4485e6b8a83a463429a72f7b8b44a8a", + "source": "https://api.nuget.org/v3-flatcontainer/deque.axecore.playwright/4.13.0/deque.axecore.playwright.nuspec", + "classification": "Test-only accessibility package (WEB.40 P2-021 item 8): axe-core run through Microsoft.Playwright for .NET. Referenced only by tests/browser (local opt-in, never built in CI). axe-core is injected only into the page under test. Its Microsoft.Playwright 1.20.2 dependency resolves to the centrally pinned 1.62.0." + }, + "google.protobuf/3.36.1": { + "contentHash": "77AqPEoaY1ODE+syYBHti0jXiwQq0J/fUr/fRyYhNlc9oKtH5dZZEr/OLKtdKNVG83PRnCYB2r8B80ZrObzOGQ==", + "licence": "BSD-3-Clause", + "nuspecSha256": "d6a4a567ce4889e25d098fd39d13a65ce05c847e8574818edc0d1eee6277eb40", + "source": "https://api.nuget.org/v3-flatcontainer/google.protobuf/3.36.1/google.protobuf.nuspec", + "classification": "Transitive BSD-3-Clause protobuf runtime required by the generated Contracts bindings (ArcForges.Contracts.PublicApi and Foundation)." + }, + "grpc.core.api/2.84.0": { + "contentHash": "p2SOMl6q/GZ4/5MLkgboC/z55g4zKUEruRn/g46QgppjQBVnsLZVBU/9VO7n60ll38G3Eo8Zu9b7x+W01GKFVg==", + "licence": "Apache-2.0", + "nuspecSha256": "bba00f5d5be8423dd351662e313648b0abd13100ef67d55f6fc29de44c544a96", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.core.api/2.84.0/grpc.core.api.nuspec", + "classification": "Transitive Apache-2.0 gRPC core API required by the generated Contracts service bindings (ArcForges.Contracts.PublicApi)." + }, + "grpc.net.client.web/2.84.0": { + "contentHash": "6KB6IFGx+KfGMaN06nATBVgvgsOEVf12/nJHvdAPsY2mULmWCSn4moFUT8UnCnQ19C3tpGbc2CnB1/e0hVUmaA==", + "licence": "Apache-2.0", + "nuspecSha256": "da47ce027ddbbbf7c0b1f242b1efe64a5aeaa2959da54716a100fcc6e4520efb", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client.web/2.84.0/grpc.net.client.web.nuspec", + "classification": "Apache-2.0 gRPC-Web handler of the Account and Chat profiles (binary application/grpc-web+proto unary; WEB.40 step 3)." + }, + "grpc.net.client/2.84.0": { + "contentHash": "My6Fms2YCyAbihTBZZ3kLS39iTY2pkMAzGDBTC6XhPOujg70eB07AEJ7tPiW3tZ70bhFzHcc3+d4j3DaIPF3aA==", + "licence": "Apache-2.0", + "nuspecSha256": "32a43f3448ab90bb71c946533db4d5135cef4728a3ee29bf88d9a0cbf1e67413", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.client/2.84.0/grpc.net.client.nuspec", + "classification": "Apache-2.0 gRPC client channel of the Account and Chat profiles; the generated Contracts hello client runs over it (WEB.40 step 3)." + }, + "grpc.net.common/2.84.0": { + "contentHash": "DPxm/H8G/lDRrz997wM0M2zN/PGqVeZKDakhBbtABEbv6WIW4HI9g3BvntBQT6ZUROuGacMzfyyF9e5kiTVFfQ==", + "licence": "Apache-2.0", + "nuspecSha256": "f621aa566e63340b572a4f0bd2b996b934407c77b8cdf121f38578e93ee796cb", + "source": "https://api.nuget.org/v3-flatcontainer/grpc.net.common/2.84.0/grpc.net.common.nuspec", + "classification": "Apache-2.0 shared gRPC .NET types required by Grpc.Net.Client (WEB.40 step 3)." + }, + "microsoft.applicationinsights/2.23.0": { + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==", + "licence": "MIT", + "nuspecSha256": "184c649613d487d2b11466882ad83e375bf94f6aa228afae78dd4005bb0bb2a0", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.applicationinsights/2.23.0/microsoft.applicationinsights.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.app.internal.assets/10.0.12": { + "contentHash": "+sIBo9AMBZH46qHWHQdfzcGMt+RuBaPYrUMOAM9VjA7MQ9QtIez3lq1C3E05+Yroa94KKt3DSWaxmCI4H28a9Q==", + "licence": "MIT", + "nuspecSha256": "54e463a46bb9c12338e654dcd6e2cf1fa956a29e2b826e84d07b86348581e6d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.app.internal.assets/10.0.12/microsoft.aspnetcore.app.internal.assets.nuspec", + "classification": "MIT ASP.NET Core internal assets package restored for the Blazor WebAssembly profile build; framework plumbing, never a Web source (WEB.40 step 3)." + }, + "microsoft.aspnetcore.authorization/10.0.10": { + "contentHash": "FxWC2bc788/51CiSiMkKW5gQyqrSSs1991BhphS6ZARMGii+3Qr355Q9OSB/nBpdHVjIopfegAhreZ6UPpO9WA==", + "licence": "MIT", + "nuspecSha256": "b3129f87c7f0054d30bd3ec2e3c7d4c5d8591dde38df9808f47a1fcdd52294fd", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.authorization/10.0.10/microsoft.aspnetcore.authorization.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.analyzers/10.0.10": { + "contentHash": "ZBJobkEDv6yWftw3ycZMkuV9YpvJOBmjWpg2UbQ0Ol4THObhKai0PNl/SGFBmpw03JKCPA63RgplQOA1LQbRDg==", + "licence": "MIT", + "nuspecSha256": "4513a4977113b2abc1f2b6679b03b79ac7ffa5504d386a2b2f0f39eab4016679", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.analyzers/10.0.10/microsoft.aspnetcore.components.analyzers.nuspec", + "classification": "Transitive analyzer package; build-time only, adds no runtime output." + }, + "microsoft.aspnetcore.components.authorization/10.0.10": { + "contentHash": "em2xVQkvtn3BHFT3J9pp4K3yDgPnp0S5a+gcGxb6xkjqy+81yDa8XEEaQnkruCVRusFMltORFFmnlrnHa/A90w==", + "licence": "MIT", + "nuspecSha256": "9fff5973fa96969c1cef96589379bb004f077dffd6732b673ba001d9a9165726", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.authorization/10.0.10/microsoft.aspnetcore.components.authorization.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.forms/10.0.10": { + "contentHash": "09cKk0/t83Y5DN5ne4J9Y+OiLJEQ+XBFGIoD0UXAnae8yQEkFa/OYbX0bBGkx0k5r5ndta/CMdlMk3lCG81R0g==", + "licence": "MIT", + "nuspecSha256": "14b8a3dc38324f0af1bd43917389a11f65134478181e7ae4add9f457f3666962", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.forms/10.0.10/microsoft.aspnetcore.components.forms.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.web/10.0.10": { + "contentHash": "8+cyStKhSy94Q2L/2zmgk0H1f+GKvBGEvTImT0qdsCqeNbT7ilGDKTIJ1PiugsPtSNLuUEABUqMpA3rE1yAKFg==", + "licence": "MIT", + "nuspecSha256": "f87599d2398ca96444534e5ac2a69d37212d057b2569912a20075d63e89f7c11", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.web/10.0.10/microsoft.aspnetcore.components.web.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.components.webassembly.authentication/10.0.10": { + "contentHash": "D2GIsqVJ2+tVDViJmbEF4tc+gxhdAl5xkBAMZHWCJV7Pt+oBkU2omBSRbgVTq3N2OgreVNSp7w+FrZNgu97d5A==", + "licence": "MIT", + "nuspecSha256": "7fb21a70c4bec90f286d35f4ee16addcb0867828c173cb9040a5b53a7204966e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly.authentication/10.0.10/microsoft.aspnetcore.components.webassembly.authentication.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.aspnetcore.components.webassembly/10.0.10": { + "contentHash": "0Zib7U6HmGw4Noj/+yJz1YuEJwo3XOV0iLezXDLFsrHTaC8L+KUOR/1+1tX7AAeipDG6jP3gPZOwpirk6G3LYQ==", + "licence": "MIT", + "nuspecSha256": "20a20d43da60671546db4fc23ac99f0dc23f8df470660c3b60d61a4338d891fa", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components.webassembly/10.0.10/microsoft.aspnetcore.components.webassembly.nuspec", + "classification": "Direct Blazor WebAssembly runtime package of the Account, Chat and Operations standalone profiles; its runtime class is present only in those browser profile outputs." + }, + "microsoft.aspnetcore.components/10.0.10": { + "contentHash": "wlCBFU9YxqhUc2kVbl/FCGNFSbRjS312WVk9YRVjs/OcZ+Yqa9QsD7z+UcBzNNAbcPpEM98f/PaaD6lHt5rmIg==", + "licence": "MIT", + "nuspecSha256": "d087b5911f3dff47e1e0ffdb73e86403d5881009cd0139f4d21206ed3f16fc9e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.components/10.0.10/microsoft.aspnetcore.components.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.aspnetcore.metadata/10.0.10": { + "contentHash": "nmZYV9OQ69r3EDOj0SW1JLgD4uFHfMNDKb8mk8Mf4y0Enw3AzkMD85zPuAk5FqmEz8PS0WagR/tJCwLg8xpI3Q==", + "licence": "MIT", + "nuspecSha256": "e50c6e8ebe919bf9285078b1c477149e62e2802e6ea093c0de3d0740d7d08d2a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.aspnetcore.metadata/10.0.10/microsoft.aspnetcore.metadata.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.bcl.asyncinterfaces/6.0.0": { + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==", + "licence": "MIT", + "nuspecSha256": "3a965c424d56f1cee169d88cc9c04f2c04f01bd65f20147b26ac093d761beec9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.bcl.asyncinterfaces/6.0.0/microsoft.bcl.asyncinterfaces.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.abstractions/10.0.10": { + "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "licence": "MIT", + "nuspecSha256": "f711589381828be613ead1bc3dd1f0f1fedb2dc06c7eb63893aa57be75f3822a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.abstractions/10.0.10/microsoft.extensions.caching.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.caching.memory/10.0.10": { + "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "licence": "MIT", + "nuspecSha256": "384c576df52ad0bf752492e7d9c795d08a297f064f1d6ea0c6cc5533bc94e2c5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.caching.memory/10.0.10/microsoft.extensions.caching.memory.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.configuration.abstractions/10.0.10": { + "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "licence": "MIT", + "nuspecSha256": "9782906731f192523e01dde11f749492c342738614c73f2fb794b13232eb5e8c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.abstractions/10.0.10/microsoft.extensions.configuration.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.binder/10.0.10": { + "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "licence": "MIT", + "nuspecSha256": "4c56d948cb232dafa93210efb3417fb8867eb1a2d4e7ae983ec1b8f931b2b2c6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.binder/10.0.10/microsoft.extensions.configuration.binder.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.fileextensions/10.0.10": { + "contentHash": "ZOhZYwvbXGTgGVRwswIirofEMVHuWdxjdh0JeUZXwaF9cgcjXdz/t0ELtgaevw7ezTyv47yPNCgGreWtLkn3IQ==", + "licence": "MIT", + "nuspecSha256": "5499a83d28a055d0faa12093446b4812ab53b56cbd992577e97488a9bbe8524a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.fileextensions/10.0.10/microsoft.extensions.configuration.fileextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration.json/10.0.10": { + "contentHash": "uvJ6sHwjgrkMEJOgiC76G0mcZGXerwyyWkwX34EOjCbxKG6TCtfAoqDKAMsCvEBf9HxjlGQEgqsSMOGCmGBf+A==", + "licence": "MIT", + "nuspecSha256": "325fdf5addefb211ec3de14dbb522e467d2041fd60979722cc8b151f0714a2c9", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/10.0.10/microsoft.extensions.configuration.json.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.configuration/10.0.10": { + "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "licence": "MIT", + "nuspecSha256": "f71c929fe65083b11410622a1f7f1777432dfe8a6b8cb043d9de08ec53493b3c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/10.0.10/microsoft.extensions.configuration.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection.abstractions/10.0.10": { + "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==", + "licence": "MIT", + "nuspecSha256": "0c95e70ef7abddd0120b259dd80f5677c22b4f638fb8c5aa50846c03c65a7569", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/10.0.10/microsoft.extensions.dependencyinjection.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.dependencyinjection/10.0.10": { + "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "licence": "MIT", + "nuspecSha256": "bddd1cd5168cdcc297ae2609203985005d8da2533f29930b75ea993b2d847e71", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/10.0.10/microsoft.extensions.dependencyinjection.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics.abstractions/10.0.10": { + "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "licence": "MIT", + "nuspecSha256": "343d515100c9dd789f91cb1dffcd966ef482b688cdc5711c2b4de20adfb82474", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics.abstractions/10.0.10/microsoft.extensions.diagnostics.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.diagnostics/10.0.10": { + "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "licence": "MIT", + "nuspecSha256": "de354b4b7c973cdcf45cb30def81fe1c89ce76ce67460b6010719cef38c870a7", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.diagnostics/10.0.10/microsoft.extensions.diagnostics.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.abstractions/10.0.10": { + "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "licence": "MIT", + "nuspecSha256": "7c4a745071335ffaf8c632cef40da244796bed7c529948e73343bc52f74cbbed", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.abstractions/10.0.10/microsoft.extensions.fileproviders.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.fileproviders.physical/10.0.10": { + "contentHash": "jhJAyo38kSrH3ARvWUk0h8itogVnQu2DCZuPo+s0Z+tXes0ugTxMPaHYzap85785eHQmPFqD9TYERqBbtGxn/w==", + "licence": "MIT", + "nuspecSha256": "b331afc04c5bbb04bcc28b5775edd252d35a90c920aab28c3d2dd67bacbf6e6a", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.fileproviders.physical/10.0.10/microsoft.extensions.fileproviders.physical.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.filesystemglobbing/10.0.10": { + "contentHash": "jSOCVxEwCd4Aq925kJVz1kSO1EpX2OHYKL04qVREXkDU7Ce3pVDdHPYm+fEy8y/th2kJf/DAstRHpJAqoNWP8w==", + "licence": "MIT", + "nuspecSha256": "c1d9e67b4fe7973d1f880b1cd6cc08252d6bd7452b0a129a8e255e8b63f4a4b2", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.filesystemglobbing/10.0.10/microsoft.extensions.filesystemglobbing.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.localization.abstractions/10.0.10": { + "contentHash": "j1DygDJR1wP3LG3d7a1rTFPUZcPfhMggcmDEGKhK2iLK0TuAIaW9bHiq1f9k650gdsXsANTm71hEg/k9msNnAQ==", + "licence": "MIT", + "nuspecSha256": "d3c0179cdf2b411b2fb0c3659b200e83c63696b3f32acf8fc85853f8dfef939c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.localization.abstractions/10.0.10/microsoft.extensions.localization.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.extensions.logging.abstractions/10.0.10": { + "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "licence": "MIT", + "nuspecSha256": "bedb09d9a04ea6d879e6566e886f200ae741a364d1d5b234e09376217a948e56", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging.abstractions/10.0.10/microsoft.extensions.logging.abstractions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.logging/10.0.10": { + "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "licence": "MIT", + "nuspecSha256": "48ddb1f3a4421752024d12b2fa1745b46a069acaecfc775677a9c91c8c5129bc", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.logging/10.0.10/microsoft.extensions.logging.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options.configurationextensions/10.0.10": { + "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "licence": "MIT", + "nuspecSha256": "5fdf2d674cbbb97544ffa98a32d8a39d0ce0c8dacc2a902618fc0f15b68029d6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options.configurationextensions/10.0.10/microsoft.extensions.options.configurationextensions.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.options/10.0.10": { + "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "licence": "MIT", + "nuspecSha256": "ee55c6f0fca958a27d90d3c9a9d974ff1b8dd7e625565bb8219d84c3bed4e894", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.options/10.0.10/microsoft.extensions.options.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.primitives/10.0.10": { + "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==", + "licence": "MIT", + "nuspecSha256": "7c5215df6da89c10f7a9813d8f5c8968a7ac6870afb71129fd045c37f5861bbe", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.primitives/10.0.10/microsoft.extensions.primitives.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.extensions.validation/10.0.10": { + "contentHash": "d77zBZk+Z4uo6hEuLvMY+yMjLkn2T/vX5c2sB0sdoSxQVZk7qvec6SuORp2/uZp7UchX4hNp+VJm4GsoSE0waA==", + "licence": "MIT", + "nuspecSha256": "7adbba31b1b3c50bb1822413d5a6e6cc23c0e4308a26bf123fd298bbc90c50de", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.validation/10.0.10/microsoft.extensions.validation.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop.webassembly/10.0.10": { + "contentHash": "FjPAGQWbCP85FWY2Wl9opHSsY+u9nZb8Ui6eTVHEMY/RJateOhW78jEtC4QOQ/6slkS1MKyIAF6ZHIrUR1fX/w==", + "licence": "MIT", + "nuspecSha256": "574f8356e9862d801b4340fca77f264263c9675a71bf5147265ec89ea65303a6", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop.webassembly/10.0.10/microsoft.jsinterop.webassembly.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.jsinterop/10.0.10": { + "contentHash": "VivWvz1iFR3Ntf/e5/xci6o/MF2tcHiprNkhRlL+gMkBoDjQJCioiO+Gwp/DP8scuzqbp3b9C3wh+zuy4pD4AA==", + "licence": "MIT", + "nuspecSha256": "7fe75cbe54982483c6821fca3761cfcb247afe165c8a55af297c9b6e079bb9ca", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.jsinterop/10.0.10/microsoft.jsinterop.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.illink.tasks/10.0.12": { + "contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ==", + "licence": "MIT", + "nuspecSha256": "bf5762457b552fed442157eb53c6c6693ded498ecfd5581a77cbdb3898bda628", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.illink.tasks/10.0.12/microsoft.net.illink.tasks.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.net.sdk.webassembly.pack/10.0.12": { + "contentHash": "MIeBVeWkbKdggl3kmmIUnx8ZNEgjD2m1gTN/0W+TDRn6zcL1WZdq5hXhGQ0wjuSlw/8tvwwCg79PemfkJ3+5JA==", + "licence": "MIT", + "nuspecSha256": "5bb5f25804b65c8047d50b124eb17c491a374bf3202f9f44c85940fae9c42562", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.sdk.webassembly.pack/10.0.12/microsoft.net.sdk.webassembly.pack.nuspec", + "classification": "Transitive dependency of the admitted WebAssembly or Ui closure; its runtime class ships only if the browser publish output of an admitted profile includes it." + }, + "microsoft.playwright/1.62.0": { + "contentHash": "cos6dmHPcMpS6bXw2WEgoXb8KXwMhs3c5L0P/cUf2rdUpjVA3g3u+KiSpVp2VCzsXymRpy2tFHqm+Wnz7s5VhQ==", + "licence": "MIT", + "nuspecSha256": "4467841e7a008d302cc7f6e06f601ff8d3a6afc4b74ae292e2937596d4883ed5", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.playwright/1.62.0/microsoft.playwright.nuspec", + "classification": "Apache-2.0 Microsoft.Playwright for .NET: test-only local opt-in browser tooling, never CI and never shipped (P2-021 item 8)." + }, + "microsoft.testing.extensions.telemetry/2.4.0": { + "contentHash": "JeP1RFqBa11fWmBk8xEfZcMKr4rxWSyI6OZ+659V069CaMkTEOQBW2UdSSeNz3absOsygcn7JJkzerC4LGnZ9w==", + "licence": "MIT", + "nuspecSha256": "5a13a0f3b3da2e1ea48c7faac4f0d3a8ca570a562ec4928e8e59d8f10efa9738", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.telemetry/2.4.0/microsoft.testing.extensions.telemetry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.extensions.trxreport.abstractions/2.4.0": { + "contentHash": "uRb+4qM42dFDg4kWJZ2kFEcwESNVCRZJjItp5vETorN3rSJGysP617LqYirOcrCYmxg+obPreHMM5JcsNDKtBw==", + "licence": "MIT", + "nuspecSha256": "f99165b201b607209e5f03db0b2be881d020d45da108028c011fa2b2f6b57b99", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.extensions.trxreport.abstractions/2.4.0/microsoft.testing.extensions.trxreport.abstractions.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform.msbuild/2.4.0": { + "contentHash": "qr5M6h16YHMJLFDcWELFVMMpGte2BUmveBZKT5YoBV+bmuJRPu9bv/Zqke4yQuOEKRNxoAETrG5jr+/6Rnr3Hg==", + "licence": "MIT", + "nuspecSha256": "1fe1b7f9981ae944b65a9cf851ca791bd5e0dd66dedd5d114f838aeecf5bea4c", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform.msbuild/2.4.0/microsoft.testing.platform.msbuild.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.testing.platform/2.4.0": { + "contentHash": "dp1N3P1ujb0ztwFgqz2o/ItEvq+pm19/AiA+Xq7Zpcy7oPMcxDBZLYGtf0LlU1MveEBJuKVjZhI+LnkfcH/0jQ==", + "licence": "MIT", + "nuspecSha256": "66933f17979c2b024e80d6c8b4144150ecc5773153ee6a485aa430645da96b5d", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.testing.platform/2.4.0/microsoft.testing.platform.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "microsoft.win32.registry/5.0.0": { + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==", + "licence": "MIT", + "nuspecSha256": "eb8c4b9d4ebe62d8528dec6ab2a752c2e3488f28f882a6c45a33dcc21eacc58e", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.win32.registry/5.0.0/microsoft.win32.registry.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "newtonsoft.json/13.0.1": { + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", + "licence": "MIT", + "nuspecSha256": "4ffea71f07d70dff4c57529871f8fc377bf85e62e93b24f1ae5409ff73570c5e", + "source": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.nuspec", + "classification": "Transitive MIT JSON library of Deque.AxeCore.Commons, test-only: reached only from tests/browser (local opt-in). No product project references it; the architecture policy refuses its use in product C# source." + }, + "system.componentmodel.annotations/5.0.0": { + "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==", + "licence": "MIT", + "nuspecSha256": "81b52aa34460e41f850b163546fa6e72e007499e2b8ed7396c217944b2ec778f", + "source": "https://api.nuget.org/v3-flatcontainer/system.componentmodel.annotations/5.0.0/system.componentmodel.annotations.nuspec", + "classification": "MIT component-model annotations package required by Microsoft.Playwright for .NET; test-only closure of the local opt-in browser tests (WEB.40 step 3)." + }, + "system.io.abstractions/17.0.24": { + "contentHash": "hA7bacntMiZv1Yf9xtjwl/GP3GT1mG84QxhAk7ijAUD0pJhJaVVwXScE13vMpXnNtlaRDW6SeyZdWg2j2qrh4w==", + "licence": "MIT", + "nuspecSha256": "c9d330f98c4f078404906c2705a4f47b142a0aac500022e1813939cbb276e251", + "source": "https://api.nuget.org/v3-flatcontainer/system.io.abstractions/17.0.24/system.io.abstractions.nuspec", + "classification": "Transitive MIT file-system abstraction of Deque.AxeCore.Playwright, test-only: reached only from tests/browser (local opt-in). Its net6.0 dependency group is empty, so no further package is restored for net10.0." + }, + "system.security.accesscontrol/6.0.1": { + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==", + "licence": "MIT", + "nuspecSha256": "2fb3325fbcea2d5eee1eb5666fd391fac8498f7acefe34f922aaceafea5e9df3", + "source": "https://api.nuget.org/v3-flatcontainer/system.security.accesscontrol/6.0.1/system.security.accesscontrol.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.analyzers/2.1.0": { + "contentHash": "X7QXEcZQGz0G/HL4HUyK+aAvNa/IMGbOCnFIq4jD/Evktq12xANKwzOUr7b08vCmC1LXu/47qHWOdjm3KfaJ0A==", + "licence": "Apache-2.0", + "nuspecSha256": "8d8fb2ff8c8a1505020e13d190a093af179b994eebce42ce03457ed9497e7b68", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.analyzers/2.1.0/xunit.analyzers.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.assert/4.0.1": { + "contentHash": "nC7d3cY06Oo7hkdwWPZkBR0Ud75xNhgx0P7i0GmMMZC3puCGlc4szFiT30biH03IN/eDnr8h+nv3A1UD17bk/A==", + "licence": "Apache-2.0", + "nuspecSha256": "3f2aef448f6c8e12cb7fa5d6ba111ea5311f530862a3e5fd9a63f153bec1f3af", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.assert/4.0.1/xunit.v3.assert.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.common/4.0.1": { + "contentHash": "Qf25TVdadDQYf9zVxSd7L9RNQhuP0jCUHmo96XTmIKLxGefylIEV63jRbXBPFzdXmV09TCdcurk+AQ9LiWS2Hg==", + "licence": "Apache-2.0", + "nuspecSha256": "bdf4e653bae8f75b5392928a37bf108fcad4d2a5308524420828e82f3e79ded5", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.common/4.0.1/xunit.v3.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.core.mtp-v2/4.0.1": { + "contentHash": "7qfTfrIfS2wybpSVyRLmhXHbSudD2eNw66LfukixmKsRCTcfvOrLsx0qGL/lObcEh3Z2F4SIXgoFnLIiB8yfjQ==", + "licence": "Apache-2.0", + "nuspecSha256": "51c8cc20af9bb6071e1df8454579c9048597f5c169db3470dc5057be9844f1da", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.core.mtp-v2/4.0.1/xunit.v3.core.mtp-v2.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.extensibility.core/4.0.1": { + "contentHash": "J0d5OfFcp920nZxCRIvU14+TZhiSPQR0wvqEMZ3MRiJLPu1VmYlcRNkmcSW0i1DwkB81gAHc+fHUQf/cU64MYg==", + "licence": "Apache-2.0", + "nuspecSha256": "0def7df8a35e99e328a1b8799e169592b961ee06a3f7aac556a42ee8464cecf8", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.extensibility.core/4.0.1/xunit.v3.extensibility.core.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.mtp-v2/4.0.1": { + "contentHash": "s88KiWwDDYgOWV3A+ViJiqCe9cLU/Rt6Gb5TfOC7Abv/HKSoj4IHVPcOQk7jPt7HhZHHEts5IuaVpB30eO5B1w==", + "licence": "Apache-2.0", + "nuspecSha256": "9134992ab09b0b8545f7fa31f6ab3c84a659b58493c7afca586a4977ee8998e2", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.mtp-v2/4.0.1/xunit.v3.mtp-v2.nuspec", + "classification": "Direct xUnit v3 Microsoft.Testing.Platform package for the tests/** projects; test-only and never in a published output." + }, + "xunit.v3.runner.common/4.0.1": { + "contentHash": "p9AyfBpj2e5Iws8B57SbLiSngg7mEdvegHInKR4T2cjt2mis9h8htVgCnmu//agZO4zri6p6VQOj5EkHHTy3gA==", + "licence": "Apache-2.0", + "nuspecSha256": "2c80dbdeee78193f042e5b5e32c06d3b1d828659fb156c132f82574a5330799f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.common/4.0.1/xunit.v3.runner.common.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + }, + "xunit.v3.runner.inproc.console/4.0.1": { + "contentHash": "Hqwfd6ehMIhPmVWUQ2dgmknzuLFTWeyp8ES1q3D4YR5bQVyiXDcaIoaFwqAz2zgLr49WaW4Mz7VVncP7u/Q8/Q==", + "licence": "Apache-2.0", + "nuspecSha256": "621fc57c6e5ab43360c5933415c98727f36abf658d1a7d9793159a2b827d4a5f", + "source": "https://api.nuget.org/v3-flatcontainer/xunit.v3.runner.inproc.console/4.0.1/xunit.v3.runner.inproc.console.nuspec", + "classification": "Transitive dependency of the xUnit v3 or bUnit test closure, reached only from tests/**; test-only and never in a published output." + } + }, + "nugetPackDownloads": { + "microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12": { + "contentHash": "7RAMnN/ZL6fjSocFEGSsIeGjZeT2uShNo0+JkkGf7b70i80YLc6rsDVovpifvWhmw8HWOqzXUlLOAcUTCFtzGA==", + "licence": "MIT", + "nuspecSha256": "6eb69422b052d9619c40ba521b5e1f0c52ebcbc3394c4a76ac232494ac924439", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.netcore.app.runtime.mono.browser-wasm/10.0.12/microsoft.netcore.app.runtime.mono.browser-wasm.nuspec", + "declaredBy": "implicit: added by the .NET 10.0.401 SDK for Microsoft.NET.Sdk.BlazorWebAssembly projects (ArcForges.Web.App) at the SDK bundled runtime version 10.0.12; restored to the NuGet cache and recorded in obj/project.assets.json, not in packages.lock.json", + "classification": "Mono browser-wasm runtime pack, the WebAssembly runtime for the standalone Blazor profiles. Build-time download only, never shipped as a Web source. RunAOTCompilation is false, so no MonoAOT pack is admitted." + }, + "microsoft.net.runtime.webassembly.sdk/10.0.12": { + "contentHash": "U7JTnY0XDO4c1fJH4XU3nyNMbf5lmJy9wo/mIWk9TAY4BfB5KKkJoOVr+Ab4VY52A3pVFc+4kMnbwPbhGgpMqQ==", + "licence": "MIT", + "nuspecSha256": "954d8efea4619301fcb2321397f6ca63b7fa44ceecf1509cd9130dc1677013ee", + "source": "https://api.nuget.org/v3-flatcontainer/microsoft.net.runtime.webassembly.sdk/10.0.12/microsoft.net.runtime.webassembly.sdk.nuspec", + "declaredBy": "ArcForges.Web.App.csproj PackageDownload Version [10.0.12]", + "classification": "WebAssembly SDK targets pack at the SDK bundled runtime version. Build-time download only." + } + } +} diff --git a/eng/provenance/files.json b/eng/provenance/files.json index 2b20fed..692aef7 100644 --- a/eng/provenance/files.json +++ b/eng/provenance/files.json @@ -29,6 +29,7 @@ "eng/policy/dependency-reviews/web-40-admission-r14.json", "eng/policy/dependency-reviews/web-40-admission-r15.json", "eng/policy/dependency-reviews/web-40-admission-r16.json", + "eng/policy/dependency-reviews/web-40-admission-r17.json", "NuGet.config", "README.md", "SECURITY.md",