From 8f615909d5d472237f7e29b19ecef76ed34d2a2e Mon Sep 17 00:00:00 2001 From: devlooped-bot Date: Tue, 9 Jul 2024 00:57:19 +0000 Subject: [PATCH] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20files=20with=20dotn?= =?UTF-8?q?et-file=20sync=20#=20devlooped/oss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add common sponsors metadata to assemblies https://github.com/devlooped/oss/commit/0789bf0 - Update assembly metadata format for Funding.GitHub https://github.com/devlooped/oss/commit/5801de0 - SponsorLink metadata will be opt-in only by analyzer projects https://github.com/devlooped/oss/commit/c618ea8 - Improve default value for GenerateDocumentationFile https://github.com/devlooped/oss/commit/b76de49 --- .github/workflows/sponsor.yml | 24 -- .netconfig | 208 +---------- src/Directory.Build.props | 2 - src/Directory.Build.targets | 7 + src/SponsorLink/Analyzer/Analyzer.csproj | 37 -- .../Analyzer/Properties/launchSettings.json | 11 - .../Analyzer/StatusReportingAnalyzer.cs | 45 --- .../Analyzer/StatusReportingGenerator.cs | 20 - .../buildTransitive/SponsorableLib.targets | 7 - src/SponsorLink/Directory.Build.props | 47 --- src/SponsorLink/Directory.Build.targets | 8 - src/SponsorLink/Library/Library.csproj | 25 -- src/SponsorLink/Library/MyClass.cs | 5 - src/SponsorLink/Library/Resources.resx | 123 ------ src/SponsorLink/Library/readme.md | 5 - src/SponsorLink/SponsorLink.Tests.targets | 38 -- src/SponsorLink/SponsorLink.targets | 191 ---------- .../SponsorLink/AppDomainDictionary.cs | 36 -- .../SponsorLink/DiagnosticsManager.cs | 226 ----------- src/SponsorLink/SponsorLink/ManifestStatus.cs | 25 -- src/SponsorLink/SponsorLink/Resources.es.resx | 163 -------- src/SponsorLink/SponsorLink/Resources.resx | 164 -------- src/SponsorLink/SponsorLink/SponsorLink.cs | 207 ----------- .../SponsorLink/SponsorLink.csproj | 79 ---- .../SponsorLink/SponsorLinkAnalyzer.cs | 89 ----- src/SponsorLink/SponsorLink/SponsorStatus.cs | 25 -- .../SponsorLink/SponsorableLib.targets | 60 --- src/SponsorLink/SponsorLink/Tracing.cs | 49 --- .../Devlooped.Sponsors.targets | 118 ------ src/SponsorLink/SponsorLink/sponsorable.md | 5 - src/SponsorLink/SponsorLinkAnalyzer.sln | 43 --- src/SponsorLink/Tests/.netconfig | 17 - src/SponsorLink/Tests/Attributes.cs | 59 --- src/SponsorLink/Tests/Extensions.cs | 56 --- src/SponsorLink/Tests/JsonOptions.cs | 70 ---- src/SponsorLink/Tests/Resources.Designer.cs | 63 ---- src/SponsorLink/Tests/Resources.resx | 101 ----- src/SponsorLink/Tests/Sample.cs | 69 ---- src/SponsorLink/Tests/SponsorLinkTests.cs | 126 ------- src/SponsorLink/Tests/SponsorableManifest.cs | 351 ------------------ src/SponsorLink/Tests/Tests.csproj | 69 ---- src/SponsorLink/jwk.ps1 | 1 - src/SponsorLink/readme.md | 34 -- 43 files changed, 11 insertions(+), 3097 deletions(-) delete mode 100644 .github/workflows/sponsor.yml delete mode 100644 src/SponsorLink/Analyzer/Analyzer.csproj delete mode 100644 src/SponsorLink/Analyzer/Properties/launchSettings.json delete mode 100644 src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs delete mode 100644 src/SponsorLink/Analyzer/StatusReportingGenerator.cs delete mode 100644 src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets delete mode 100644 src/SponsorLink/Directory.Build.props delete mode 100644 src/SponsorLink/Directory.Build.targets delete mode 100644 src/SponsorLink/Library/Library.csproj delete mode 100644 src/SponsorLink/Library/MyClass.cs delete mode 100644 src/SponsorLink/Library/Resources.resx delete mode 100644 src/SponsorLink/Library/readme.md delete mode 100644 src/SponsorLink/SponsorLink.Tests.targets delete mode 100644 src/SponsorLink/SponsorLink.targets delete mode 100644 src/SponsorLink/SponsorLink/AppDomainDictionary.cs delete mode 100644 src/SponsorLink/SponsorLink/DiagnosticsManager.cs delete mode 100644 src/SponsorLink/SponsorLink/ManifestStatus.cs delete mode 100644 src/SponsorLink/SponsorLink/Resources.es.resx delete mode 100644 src/SponsorLink/SponsorLink/Resources.resx delete mode 100644 src/SponsorLink/SponsorLink/SponsorLink.cs delete mode 100644 src/SponsorLink/SponsorLink/SponsorLink.csproj delete mode 100644 src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs delete mode 100644 src/SponsorLink/SponsorLink/SponsorStatus.cs delete mode 100644 src/SponsorLink/SponsorLink/SponsorableLib.targets delete mode 100644 src/SponsorLink/SponsorLink/Tracing.cs delete mode 100644 src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets delete mode 100644 src/SponsorLink/SponsorLink/sponsorable.md delete mode 100644 src/SponsorLink/SponsorLinkAnalyzer.sln delete mode 100644 src/SponsorLink/Tests/.netconfig delete mode 100644 src/SponsorLink/Tests/Attributes.cs delete mode 100644 src/SponsorLink/Tests/Extensions.cs delete mode 100644 src/SponsorLink/Tests/JsonOptions.cs delete mode 100644 src/SponsorLink/Tests/Resources.Designer.cs delete mode 100644 src/SponsorLink/Tests/Resources.resx delete mode 100644 src/SponsorLink/Tests/Sample.cs delete mode 100644 src/SponsorLink/Tests/SponsorLinkTests.cs delete mode 100644 src/SponsorLink/Tests/SponsorableManifest.cs delete mode 100644 src/SponsorLink/Tests/Tests.csproj delete mode 100644 src/SponsorLink/jwk.ps1 delete mode 100644 src/SponsorLink/readme.md diff --git a/.github/workflows/sponsor.yml b/.github/workflows/sponsor.yml deleted file mode 100644 index 1d484d3..0000000 --- a/.github/workflows/sponsor.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: sponsor 💜 -on: - issues: - types: [opened, edited, reopened] - pull_request: - types: [opened, edited, synchronize, reopened] - -jobs: - sponsor: - runs-on: ubuntu-latest - continue-on-error: true - env: - token: ${{ secrets.GH_TOKEN }} - if: ${{ !endsWith(github.event.sender.login, '[bot]') && !endsWith(github.event.sender.login, 'bot') }} - steps: - - name: 🤘 checkout - if: env.token != '' - uses: actions/checkout@v4 - - - name: 💜 sponsor - if: env.token != '' - uses: devlooped/actions-sponsor@main - with: - token: ${{ env.token }} diff --git a/.netconfig b/.netconfig index 8a13e65..51d3732 100644 --- a/.netconfig +++ b/.netconfig @@ -69,11 +69,6 @@ [file ".github/workflows/publish.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml skip -[file ".github/workflows/sponsor.yml"] - url = https://github.com/devlooped/oss/blob/main/.github/workflows/sponsor.yml - sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9 - etag = 0849ee61af6daee29615f9632173b4e82da5bfa9d78ff28907e9408bd5acde4d - weak [file ".github/workflows/test/action.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/test/action.yml sha = 9a1b07589b9bde93bc12528e9325712a32dec418 @@ -101,206 +96,11 @@ weak [file "src/Directory.Build.props"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props - sha = 14deaea5cecc64df51781d29891a2f67caf8be16 - etag = f177eb767aaa6a347da43ff7ff419c9a0736c562cb171e17ded8007a1945a8b0 + sha = b76de49afb376aa48eb172963ed70663b59b31d3 + etag = c8b56f3860cc7ccb8773b7bd6189f5c7a6e3a2c27e9104c1ee201fbdc5af9873 weak [file "src/Directory.Build.targets"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets - sha = 1bf1eacc7ac3920d52c8e7045bfa34abc7c05302 - etag = 7cb1421f00d9f6f4c00f0ca98e485dcadb927cfa6b3f0b5d4fb212525d2ce9c0 - weak -[file "src/SponsorLink/Analyzer/Analyzer.csproj"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/Analyzer.csproj - sha = 93df7c7ec34f83ae58efbf213624d5ea31fe3c41 - etag = f76e33fde812244a275b95c8815101f6f87d144a5305a2c1f0f631f770d91920 - weak -[file "src/SponsorLink/Analyzer/Properties/launchSettings.json"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/Properties/launchSettings.json - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 6c59ab4d008e3221e316c9e3b6e0da155b892680d48cdc400a39d53cb9a12aac - weak -[file "src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = 27d85baa35ecd8be15105ec370a44a68d6ba611eb2616c070e0375b706a04a17 - weak -[file "src/SponsorLink/Analyzer/StatusReportingGenerator.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/StatusReportingGenerator.cs - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = 61c31eb675aada69a0506dba4d32b472fbfbe715bbcbe14cd8e7424a88eb7479 - weak -[file "src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets - sha = 717ddb136f83f1d9f55723d7155949ce808e9990 - etag = 2f679d203aa27b2de0b5b6bcb04490bc2251aea5fd1310cf238fcaeaa82d646b - weak -[file "src/SponsorLink/Directory.Build.props"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Directory.Build.props - sha = 3b943f5aa59f33141d1c0fffcb215446d594ad53 - etag = 0c7737411744012078642dbfc174af3f2ac7dc9f7b8ea4423981ae38753a5be4 - weak -[file "src/SponsorLink/Directory.Build.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Directory.Build.targets - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 9938f29c3573bf8bdb9686e1d9884dee177256b1d5dd7ee41472dd64bfbdd92d - weak -[file "src/SponsorLink/Library/Library.csproj"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/Library.csproj - sha = 93df7c7ec34f83ae58efbf213624d5ea31fe3c41 - etag = 56233a536fb38edd75f66f6a9a9e6044eb227a0b58fb791495ff88e43649feb7 - weak -[file "src/SponsorLink/Library/MyClass.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/MyClass.cs - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = b5b3ccd6cd14bb90dd9702b9d7e52cc22c11e601c039617738d688f9fd45d49b - weak -[file "src/SponsorLink/Library/Resources.resx"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/Resources.resx - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = aff6051733d22982e761f2b414173aafeab40e0a76a142e2b33025dced213eb2 - weak -[file "src/SponsorLink/Library/readme.md"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Library/readme.md - sha = 55124bc610b2dcad9efb343bdffc79c959170593 - etag = 5002ac8c5bbeee60c13937a32c1b6c1a5dbf0065617c8f2550e6eca6fded256d - weak -[file "src/SponsorLink/SponsorLink.Tests.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink.Tests.targets - sha = ba1310c46580419960da85aa4db1196449606a10 - etag = 5be3b99c0049cbe98c305f5af2fd482a3a995960c3a1fc6dd2bffcbf3b7e4d52 - weak -[file "src/SponsorLink/SponsorLink.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink.targets - sha = 5813f21a2de409b5b56d059034b1617e29fbe2fd - etag = 1aa2218536333b7ed887f276a5e380fa91c833607134d6c8939feb36cbc07c1c - weak -[file "src/SponsorLink/SponsorLink/AppDomainDictionary.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/AppDomainDictionary.cs - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 4a70f86e73f951bca95618c221d821e38a31ef9092af4ac61447eab845671a28 - weak -[file "src/SponsorLink/SponsorLink/DiagnosticsManager.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/DiagnosticsManager.cs - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = 8b7a3bbf5f9db0b32d4465c796541786a55d572badaa0e16fc210409b5b1786e - weak -[file "src/SponsorLink/SponsorLink/ManifestStatus.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/ManifestStatus.cs - sha = b2a11faac6c1c300bce8c1d45f95b585c19f2953 - etag = e46848f83c0436ba33a1c09a4060ad627a74db41bab66bb37ca40fce8a6532a7 - weak -[file "src/SponsorLink/SponsorLink/Resources.es.resx"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/Resources.es.resx - sha = c879f25bf483086725c8a29f104555644e6ee542 - etag = c0a05bb5efedf8e30a73ab96678579ad33832e4a4aec75d3b596b47f248c23f5 - weak -[file "src/SponsorLink/SponsorLink/Resources.resx"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/Resources.resx - sha = c879f25bf483086725c8a29f104555644e6ee542 - etag = fcb46a86511cb7996e8dcd1f4e283cea9cd51610b094ac49a7396301730814b0 - weak -[file "src/SponsorLink/SponsorLink/SponsorLink.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.cs - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = bd5a52b6e24f7a6893292b3ee9a5179f9355f1a608ae072113e29af09f833518 - weak -[file "src/SponsorLink/SponsorLink/SponsorLink.csproj"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLink.csproj - sha = 068140b6855afb9cf9f4ab02002d80ca11389314 - etag = e48d567ebf51fa1877d010ce3f0b51ab5415848939bc356979f2f575f67a75eb - weak -[file "src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = 10c8cc73614f114d1732a1deceb66e28f8758b15bb8a3d578e17d4003dfd5334 - weak -[file "src/SponsorLink/SponsorLink/SponsorStatus.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorStatus.cs - sha = 4fca946c3201d90d30e2183f699c850dcc1bf8d5 - etag = 9a5f6f35c38c34b77796925d80addc998e204bc112fcd5fc124030060390e7c2 - weak -[file "src/SponsorLink/SponsorLink/SponsorableLib.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/SponsorableLib.targets - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 2f923a97081481a6a264d63c8ff70ce5ba65c3dbaf7ea078cbe1388fb0868e1c - weak -[file "src/SponsorLink/SponsorLink/Tracing.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/Tracing.cs - sha = 08a8488036f0a8e42a62af400f37b54165ad771a - etag = 29d6c0362f4c47eedfebea5018d563adb04a8f7b30da87495c5c8a4561e2c4ed - weak -[file "src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets - sha = 717ddb136f83f1d9f55723d7155949ce808e9990 - etag = b27f6cbb67b8e12541b2c3fe914d071ddd051bac2e895c73495667b65829385e - weak -[file "src/SponsorLink/SponsorLink/sponsorable.md"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLink/sponsorable.md - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 9c275d50705a2e661f0f86f1ae5e555c0033a05e86e12f936283a5b5ef47ae77 - weak -[file "src/SponsorLink/SponsorLinkAnalyzer.sln"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/SponsorLinkAnalyzer.sln - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = fc2928c9b303d81ff23891ee791a859b794d9f2d4b9f4e81b9ed15e5b74db487 - weak -[file "src/SponsorLink/Tests/.netconfig"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/.netconfig - sha = 068140b6855afb9cf9f4ab02002d80ca11389314 - etag = 0323e19eb4582113dd409853ba83e9845069bf35733ed84a0bdc9fb6990502a9 - weak -[file "src/SponsorLink/Tests/Attributes.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Attributes.cs - sha = a0ae7272f31c766ebb129ea38c11c01df93b6b5d - etag = 1d7c17a2c9424db73746112c338a39e0000134ac878b398e2aa88f7ea5c0c488 - weak -[file "src/SponsorLink/Tests/Extensions.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Extensions.cs - sha = 068140b6855afb9cf9f4ab02002d80ca11389314 - etag = 9e51b7e6540fae140490a5283b1e67ce071bd18a267bc2ae0b35c7248261aed1 - weak -[file "src/SponsorLink/Tests/JsonOptions.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/JsonOptions.cs - sha = 068140b6855afb9cf9f4ab02002d80ca11389314 - etag = 17799725ad9b24eb5998365962c30b9a487bddadca37c616e35b76b8c9eb161a - weak -[file "src/SponsorLink/Tests/Resources.Designer.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Resources.Designer.cs - sha = c879f25bf483086725c8a29f104555644e6ee542 - etag = 69404ac09238930893fdbc225ae7839b14957e129b4c05f1ef0e7afcc4c91d63 - weak -[file "src/SponsorLink/Tests/Resources.resx"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Resources.resx - sha = c879f25bf483086725c8a29f104555644e6ee542 - etag = 13d1bb8b0de32a8c9b5dbdc806a036ed89d423cd7c0be187b8c56055c9bf7783 - weak -[file "src/SponsorLink/Tests/Sample.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Sample.cs - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = 8d32d6b062061672f37acd9360cdeb0d5fc87e15f2f65f355c2a1ee98e8da21c - weak -[file "src/SponsorLink/Tests/SponsorLinkTests.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/SponsorLinkTests.cs - sha = d74f5111504a0fae6e5a1e68ca92bf7afddb3254 - etag = 1fa41250bd984e8aa840a966d34ce0e94f2111d1422d7f50b864c38364fcf4a4 - weak -[file "src/SponsorLink/Tests/SponsorableManifest.cs"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/SponsorableManifest.cs - sha = 7febebc3ad82adcd6de9a0f151a36473d458d61b - etag = 310cf1b2245e7f4c2863e019f48087f6a0b57644407ab38447804d8ada2647a9 - weak -[file "src/SponsorLink/Tests/Tests.csproj"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/Tests/Tests.csproj - sha = 5009784040109cb405cddc8404d00c0d9290f40d - etag = be8bfd0877cd974a2f940eba4a9e697c4061db57e97efca24f9092a73a644d9e - weak -[file "src/SponsorLink/jwk.ps1"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/jwk.ps1 - sha = c4830fc3b1aa78ec98d1d2ea4fed86ef0b7b803c - etag = f399e05ecb56adaf41d2545171f299a319142b17dd09fc38e452ca8c5d13bd0d - weak -[file "src/SponsorLink/readme.md"] - url = https://github.com/devlooped/oss/blob/main/src/SponsorLink/readme.md - sha = 827a1d18bf0245978d81bcd3d52e9e6f1584d1ef - etag = 079b4aedba2aa9851e609b569f25c55db8d5922e3dbb1adc22611ce4d6cfe465 + sha = 33a20db26e47589769284817b271ce67ea9ccfd8 + etag = 1a3a0151b5771ee97ed8351254ff4c18a0ff568e0df5c33c6830f069bfbb067b weak diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 1648dcd..381c383 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -46,8 +46,6 @@ Release - true - false Latest diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 0cb1e4e..20d7f0b 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -4,6 +4,13 @@ CI;$(DefineConstants) + + + + false + false + true + true diff --git a/src/SponsorLink/Analyzer/Analyzer.csproj b/src/SponsorLink/Analyzer/Analyzer.csproj deleted file mode 100644 index f65390a..0000000 --- a/src/SponsorLink/Analyzer/Analyzer.csproj +++ /dev/null @@ -1,37 +0,0 @@ - - - - SponsorableLib.Analyzers - netstandard2.0 - true - analyzers/dotnet/roslyn4.0 - true - $(MSBuildThisFileDirectory)..\SponsorLink.targets - true - disable - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/Properties/launchSettings.json b/src/SponsorLink/Analyzer/Properties/launchSettings.json deleted file mode 100644 index de45107..0000000 --- a/src/SponsorLink/Analyzer/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "SponsorableLib": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\Tests\\Tests.csproj", - "environmentVariables": { - "SPONSORLINK_TRACE": "true" - } - } - } -} \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs b/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs deleted file mode 100644 index d7a1fd7..0000000 --- a/src/SponsorLink/Analyzer/StatusReportingAnalyzer.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using Devlooped.Sponsors; -using Humanizer; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using static Devlooped.Sponsors.SponsorLink; - -namespace Analyzer; - -[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] -public class StatusReportingAnalyzer : DiagnosticAnalyzer -{ - public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(new DiagnosticDescriptor( - "SL001", "Report Sponsoring Status", "Reports sponsoring status determined by SponsorLink", "Sponsors", - DiagnosticSeverity.Warning, true)); - - public override void Initialize(AnalysisContext context) - { - context.EnableConcurrentExecution(); - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - - context.RegisterCompilationAction(c => - { - var installed = c.Options.AdditionalFiles.Where(x => - { - var options = c.Options.AnalyzerConfigOptionsProvider.GetOptions(x); - // In release builds, we'll have a single such item, since we IL-merge the analyzer. - return options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && - options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && - itemType == "Analyzer" && - packageId == "SponsorableLib"; - }).Select(x => File.GetLastWriteTime(x.Path)).OrderByDescending(x => x).FirstOrDefault(); - - var status = Diagnostics.GetOrSetStatus(() => c.Options); - - if (installed != default) - Tracing.Trace($"Status: {status}, Installed: {(DateTime.Now - installed).Humanize()} ago"); - else - Tracing.Trace($"Status: {status}, unknown install time"); - }); - } -} \ No newline at end of file diff --git a/src/SponsorLink/Analyzer/StatusReportingGenerator.cs b/src/SponsorLink/Analyzer/StatusReportingGenerator.cs deleted file mode 100644 index 0a13b1c..0000000 --- a/src/SponsorLink/Analyzer/StatusReportingGenerator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Devlooped.Sponsors; -using Microsoft.CodeAnalysis; -using static Devlooped.Sponsors.SponsorLink; - -namespace Analyzer; - -[Generator] -public class StatusReportingGenerator : IIncrementalGenerator -{ - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterSourceOutput( - context.GetSponsorManifests(), - (spc, source) => - { - var status = Diagnostics.GetOrSetStatus(source); - spc.AddSource("StatusReporting.cs", $"// Status: {status}"); - }); - } -} diff --git a/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets b/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets deleted file mode 100644 index 37585e8..0000000 --- a/src/SponsorLink/Analyzer/buildTransitive/SponsorableLib.targets +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/Directory.Build.props b/src/SponsorLink/Directory.Build.props deleted file mode 100644 index 8afa061..0000000 --- a/src/SponsorLink/Directory.Build.props +++ /dev/null @@ -1,47 +0,0 @@ - - - - false - latest - true - annotations - true - - false - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)bin')) - - https://pkg.kzu.app/index.json;https://api.nuget.org/v3/index.json - $(PackageOutputPath);$(RestoreSources) - - - $([System.DateTime]::Parse("2024-03-15")) - $([System.DateTime]::UtcNow.Subtract($(Epoc)).TotalDays) - $([System.Math]::Truncate($(TotalDays))) - $([System.Math]::Floor($([MSBuild]::Divide($([System.DateTime]::UtcNow.TimeOfDay.TotalSeconds), 10)))) - 42.$(Days).$(Seconds) - - SponsorableLib - - - - - - - - - - - - diff --git a/src/SponsorLink/Directory.Build.targets b/src/SponsorLink/Directory.Build.targets deleted file mode 100644 index 4ce4c80..0000000 --- a/src/SponsorLink/Directory.Build.targets +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/Library/Library.csproj b/src/SponsorLink/Library/Library.csproj deleted file mode 100644 index 6e79399..0000000 --- a/src/SponsorLink/Library/Library.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - SponsorableLib - netstandard2.0 - true - SponsorableLib - Sample library incorporating SponsorLink checks - true - true - - - - - - - - - - - - - - - diff --git a/src/SponsorLink/Library/MyClass.cs b/src/SponsorLink/Library/MyClass.cs deleted file mode 100644 index 7b7f6f5..0000000 --- a/src/SponsorLink/Library/MyClass.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace SponsorableLib; - -public class MyClass -{ -} diff --git a/src/SponsorLink/Library/Resources.resx b/src/SponsorLink/Library/Resources.resx deleted file mode 100644 index 636fedc..0000000 --- a/src/SponsorLink/Library/Resources.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Bar - - \ No newline at end of file diff --git a/src/SponsorLink/Library/readme.md b/src/SponsorLink/Library/readme.md deleted file mode 100644 index ba4ce37..0000000 --- a/src/SponsorLink/Library/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -# Sponsorable Library - -Example of a library that is available for sponsorship and leverages -[SponsorLink](https://github.com/devlooped/SponsorLink) to remind users -in an IDE (VS/Rider). diff --git a/src/SponsorLink/SponsorLink.Tests.targets b/src/SponsorLink/SponsorLink.Tests.targets deleted file mode 100644 index 1ca1eb6..0000000 --- a/src/SponsorLink/SponsorLink.Tests.targets +++ /dev/null @@ -1,38 +0,0 @@ - - - - - true - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink.targets b/src/SponsorLink/SponsorLink.targets deleted file mode 100644 index 4678d5d..0000000 --- a/src/SponsorLink/SponsorLink.targets +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - true - - true - - true - - CoreResGen;$(CoreCompileDependsOn) - - - $(Product) - - $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", "")) - - 15 - - - - - - - - - - - - SponsorLink\%(RecursiveDir)%(Filename)%(Extension) - - - SponsorLink\%(RecursiveDir)%(Filename)%(Extension) - - - SponsorLink\%(RecursiveDir)%(Filename)%(Extension) - - - SponsorLink\%(PackagePath) - - - - - - false - - - false - - - false - - - false - - - - - - - - - - - - - - - - - - - - namespace Devlooped.Sponsors%3B - -partial class SponsorLink -{ - public partial class Funding - { - public const string Product = "$(FundingProduct)"%3B - public const string Prefix = "$(FundingPrefix)"%3B - public const int Grace = $(FundingGrace)%3B - } -} - - - - - - - - - - - - - - - - - - - - - - $([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine('$(MSBuildProjectDirectory)','$(AssemblyOriginatorKeyFile)')))) - /keyfile:"$(AbsoluteAssemblyOriginatorKeyFile)" /delaysign - $(ILRepackArgs) /internalize - $(ILRepackArgs) /union - - $(ILRepackArgs) @(LibDir -> '/lib:"%(Identity)."', ' ') - $(ILRepackArgs) /out:"@(IntermediateAssembly -> '%(FullPath)')" - $(ILRepackArgs) "@(IntermediateAssembly -> '%(FullPath)')" - $(ILRepackArgs) @(MergedAssemblies -> '"%(FullPath)"', ' ') - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)devlooped.jwk')) - - - - - - - - - - - - - - - - true - - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/AppDomainDictionary.cs b/src/SponsorLink/SponsorLink/AppDomainDictionary.cs deleted file mode 100644 index 05cc949..0000000 --- a/src/SponsorLink/SponsorLink/AppDomainDictionary.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -#nullable enable -using System; - -namespace Devlooped.Sponsors; - -/// -/// A helper class to store and retrieve values from the current -/// as typed named values. -/// -/// -/// This allows tools that run within the same app domain to share state, such as -/// MSBuild tasks or Roslyn analyzers. -/// -static class AppDomainDictionary -{ - /// - /// Gets the value associated with the specified name, or creates a new one if it doesn't exist. - /// - public static TValue Get(string name) where TValue : notnull, new() - { - var data = AppDomain.CurrentDomain.GetData(name); - if (data is TValue firstTry) - return firstTry; - - lock (AppDomain.CurrentDomain) - { - if (AppDomain.CurrentDomain.GetData(name) is TValue secondTry) - return secondTry; - - var newValue = new TValue(); - AppDomain.CurrentDomain.SetData(name, newValue); - return newValue; - } - } -} \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs b/src/SponsorLink/SponsorLink/DiagnosticsManager.cs deleted file mode 100644 index 96e7e14..0000000 --- a/src/SponsorLink/SponsorLink/DiagnosticsManager.cs +++ /dev/null @@ -1,226 +0,0 @@ -// -#nullable enable -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.IO.MemoryMappedFiles; -using System.Linq; -using System.Threading; -using Humanizer; -using Humanizer.Localisation; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using static Devlooped.Sponsors.SponsorLink; - -namespace Devlooped.Sponsors; - -/// -/// Manages diagnostics for the SponsorLink analyzer so that there are no duplicates -/// when multiple projects share the same product name (i.e. ThisAssembly). -/// -class DiagnosticsManager -{ - static readonly Guid appDomainDiagnosticsKey = new(0x8d0e2670, 0xe6c4, 0x45c8, 0x81, 0xba, 0x5a, 0x36, 0x81, 0xd3, 0x65, 0x3e); - - public static Dictionary KnownDescriptors { get; } = new() - { - // Requires: - // - // - { SponsorStatus.Unknown, CreateUnknown([.. Sponsorables.Keys], Funding.Product, Funding.Prefix) }, - { SponsorStatus.Sponsor, CreateSponsor([.. Sponsorables.Keys], Funding.Prefix) }, - { SponsorStatus.Expiring, CreateExpiring([.. Sponsorables.Keys], Funding.Prefix) }, - { SponsorStatus.Expired, CreateExpired([.. Sponsorables.Keys], Funding.Prefix) }, - }; - - /// - /// Acceses the diagnostics dictionary for the current . - /// - ConcurrentDictionary Diagnostics - => AppDomainDictionary.Get>(appDomainDiagnosticsKey.ToString()); - - /// - /// Attemps to remove a diagnostic for the given product. - /// - /// The product diagnostic that might have been pushed previously. - /// The removed diagnostic, or if none was previously pushed. - public void ReportOnce(Action report, string product = Funding.Product) - { - if (Diagnostics.TryRemove(product, out var diagnostic)) - { - // Ensure only one such diagnostic is reported per product for the entire process, - // so that we can avoid polluting the error list with duplicates across multiple projects. - var id = string.Concat(Process.GetCurrentProcess().Id, product, diagnostic.Id); - using var mutex = new Mutex(false, "mutex" + id); - mutex.WaitOne(); - using var mmf = MemoryMappedFile.CreateOrOpen(id, 1); - using var accessor = mmf.CreateViewAccessor(); - if (accessor.ReadByte(0) == 0) - { - accessor.Write(0, 1); - report(diagnostic); - Tracing.Trace($"👈{diagnostic.Severity.ToString().ToLowerInvariant()}:{Process.GetCurrentProcess().Id}:{Process.GetCurrentProcess().ProcessName}:{product}:{diagnostic.Id}"); - } - } - } - - /// - /// Gets the status of the given product based on a previously stored diagnostic. - /// To ensure the value is always set before returning, use . - /// This method is safe to use (and would get a non-null value) in analyzers that run after CompilationStartAction(see - /// https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md under Ordering of actions). - /// - /// Optional that was reported, if any. - public SponsorStatus? GetStatus() - { - // NOTE: the SponsorLinkAnalyzer.SetStatus uses diagnostic properties to store the - // kind of diagnostic as a simple string instead of the enum. We do this so that - // multiple analyzers or versions even across multiple products, which all would - // have their own enum, can still share the same diagnostic kind. - if (Diagnostics.TryGetValue(Funding.Product, out var diagnostic) && - diagnostic.Properties.TryGetValue(nameof(SponsorStatus), out var value)) - { - // Switch on value matching DiagnosticKind names - return value switch - { - nameof(SponsorStatus.Unknown) => SponsorStatus.Unknown, - nameof(SponsorStatus.Sponsor) => SponsorStatus.Sponsor, - nameof(SponsorStatus.Expiring) => SponsorStatus.Expiring, - nameof(SponsorStatus.Expired) => SponsorStatus.Expired, - _ => null, - }; - } - - return null; - } - - /// - /// Gets the status of the , or sets it from - /// the given set of if not already set. - /// - public SponsorStatus GetOrSetStatus(ImmutableArray manifests) - => GetOrSetStatus(() => manifests); - - /// - /// Gets the status of the , or sets it from - /// the given analyzer if not already set. - /// - public SponsorStatus GetOrSetStatus(Func options) - => GetOrSetStatus(() => options().GetSponsorManifests()); - - SponsorStatus GetOrSetStatus(Func> getManifests) - { - if (GetStatus() is { } status) - return status; - - if (!SponsorLink.TryRead(out var claims, getManifests().Select(text => - (text.GetText()?.ToString() ?? "", Sponsorables[Path.GetFileNameWithoutExtension(text.Path)]))) || - claims.GetExpiration() is not DateTime exp) - { - // report unknown, either unparsed manifest or one with no expiration (which we never emit). - Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Unknown], null, - properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Unknown)), - Funding.Product, Sponsorables.Keys.Humanize(Resources.Or))); - return SponsorStatus.Unknown; - } - else if (exp < DateTime.Now) - { - // report expired or expiring soon if still within the configured days of grace period - if (exp.AddDays(Funding.Grace) < DateTime.Now) - { - // report expiring soon - Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Expiring], null, - properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expiring)))); - return SponsorStatus.Expiring; - } - else - { - // report expired - Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Expired], null, - properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Expired)))); - return SponsorStatus.Expired; - } - } - else - { - // report sponsor - Push(Diagnostic.Create(KnownDescriptors[SponsorStatus.Sponsor], null, - properties: ImmutableDictionary.Create().Add(nameof(SponsorStatus), nameof(SponsorStatus.Sponsor)), - Funding.Product)); - return SponsorStatus.Sponsor; - } - } - - /// - /// Pushes a diagnostic for the given product. - /// - /// The same diagnostic that was pushed, for chained invocations. - Diagnostic Push(Diagnostic diagnostic, string product = Funding.Product) - { - // We only expect to get one warning per sponsorable+product - // combination, and first one to set wins. - if (Diagnostics.TryAdd(product, diagnostic)) - { - // Reset the process-wide flag for this diagnostic. - var id = string.Concat(Process.GetCurrentProcess().Id, product, diagnostic.Id); - using var mutex = new Mutex(false, "mutex" + id); - mutex.WaitOne(); - using var mmf = MemoryMappedFile.CreateOrOpen(id, 1); - using var accessor = mmf.CreateViewAccessor(); - accessor.Write(0, 0); - Tracing.Trace($"👉{diagnostic.Severity.ToString().ToLowerInvariant()}:{Process.GetCurrentProcess().Id}:{Process.GetCurrentProcess().ProcessName}:{product}:{diagnostic.Id}"); - } - - return diagnostic; - } - - internal static DiagnosticDescriptor CreateSponsor(string[] sponsorable, string prefix) => new( - $"{prefix}100", - Resources.Sponsor_Title, - Resources.Sponsor_Message, - "SponsorLink", - DiagnosticSeverity.Info, - isEnabledByDefault: true, - description: Resources.Sponsor_Description, - helpLinkUri: "https://github.com/devlooped#sponsorlink", - "DoesNotSupportF1Help"); - - internal static DiagnosticDescriptor CreateUnknown(string[] sponsorable, string product, string prefix) => new( - $"{prefix}101", - Resources.Unknown_Title, - Resources.Unknown_Message, - "SponsorLink", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: string.Format(CultureInfo.CurrentCulture, Resources.Unknown_Description, - sponsorable.Humanize(x => $"https://github.com/sponsors/{x}"), - string.Join(" ", sponsorable)), - helpLinkUri: "https://github.com/devlooped#sponsorlink", - WellKnownDiagnosticTags.NotConfigurable); - - internal static DiagnosticDescriptor CreateExpiring(string[] sponsorable, string prefix) => new( - $"{prefix}103", - Resources.Expiring_Title, - Resources.Expiring_Message, - "SponsorLink", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: string.Format(CultureInfo.CurrentCulture, Resources.Expiring_Description, string.Join(" ", sponsorable)), - helpLinkUri: "https://github.com/devlooped#autosync", - "DoesNotSupportF1Help", WellKnownDiagnosticTags.NotConfigurable); - - internal static DiagnosticDescriptor CreateExpired(string[] sponsorable, string prefix) => new( - $"{prefix}104", - Resources.Expired_Title, - Resources.Expired_Message, - "SponsorLink", - DiagnosticSeverity.Warning, - isEnabledByDefault: true, - description: string.Format(CultureInfo.CurrentCulture, Resources.Expired_Description, string.Join(" ", sponsorable)), - helpLinkUri: "https://github.com/devlooped#autosync", - "DoesNotSupportF1Help", WellKnownDiagnosticTags.NotConfigurable); -} diff --git a/src/SponsorLink/SponsorLink/ManifestStatus.cs b/src/SponsorLink/SponsorLink/ManifestStatus.cs deleted file mode 100644 index 0960e5a..0000000 --- a/src/SponsorLink/SponsorLink/ManifestStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -namespace Devlooped.Sponsors; - -/// -/// The resulting status from validation. -/// -public enum ManifestStatus -{ - /// - /// The manifest couldn't be read at all. - /// - Unknown, - /// - /// The manifest was read and is valid (not expired and properly signed). - /// - Valid, - /// - /// The manifest was read but has expired. - /// - Expired, - /// - /// The manifest was read, but its signature is invalid. - /// - Invalid, -} diff --git a/src/SponsorLink/SponsorLink/Resources.es.resx b/src/SponsorLink/SponsorLink/Resources.es.resx deleted file mode 100644 index ec1b5c1..0000000 --- a/src/SponsorLink/SponsorLink/Resources.es.resx +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Patrocinar los proyectos en que dependes asegura que se mantengan activos, y que recibas el apoyo que necesitas. También es muy económico y está disponible en todo el mundo! -Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posteriormente 'sponsor sync {1}'. - - - Por favor considere apoyar {0} patrocinando @{1} 🙏 - - - Estado de patrocinio desconocido - - - Funcionalidades exclusivas para patrocinadores pueden no estar disponibles. Ejecuta 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática. - - - El estado de patrocino ha expirado y la sincronización automática no está habilitada. - - - El estado de patrocino ha expirado - - - Eres un verdadero héroe. Tu patrocinio ayuda a mantener el proyecto vivo y próspero 🙏. - - - Gracias por apoyar a {0} con tu patrocinio 💟! - - - Eres un patrocinador del proyecto, eres lo máximo 💟! - - - El estado de patrocino ha expirado y estás en un período de gracia. Ejecuta 'sponsor sync {0}' y, opcionalmente, habilita la sincronización automática. - - - El estado de patrocino necesita actualización periódica y la sincronización automática no está habilitada. - - - El estado de patrocino ha expirado y el período de gracia terminará pronto - - - y - - - o - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/Resources.resx b/src/SponsorLink/SponsorLink/Resources.resx deleted file mode 100644 index e12a0e5..0000000 --- a/src/SponsorLink/SponsorLink/Resources.resx +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Sponsoring projects you depend on ensures they remain active, and that you get the support you need. It's also super affordable and available worldwide! -Please consider supporting the project by sponsoring at {0} and running 'sponsor sync {1}' afterwards. - Unknown sponsor description - - - Please consider supporting {0} by sponsoring @{1} 🙏 - - - Unknown sponsor status - - - Sponsor-only features may be disabled. Please run 'sponsor sync {0}' and optionally enable automatic sync. - - - Sponsor status has expired and automatic sync has not been enabled. - - - Sponsor status expired - - - You are a true hero. Your sponsorship helps keep the project alive and thriving 🙏. - - - Thank you for supporting {0} with your sponsorship 💟! - - - You are a sponsor of the project, you rock 💟! - - - Sponsor status has expired and you are in the grace period. Please run 'sponsor sync {0}' and optionally enable automatic sync. - - - Sponsor status needs periodic updating and automatic sync has not been enabled. - - - Sponsor status expired, grace period ending soon - - - and - - - or - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs deleted file mode 100644 index b3b1cf3..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLink.cs +++ /dev/null @@ -1,207 +0,0 @@ -// -#nullable enable -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Security.Claims; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; - -namespace Devlooped.Sponsors; - -static partial class SponsorLink -{ - public static Dictionary Sponsorables { get; } = typeof(SponsorLink).Assembly - .GetCustomAttributes() - .Where(x => x.Key.StartsWith("Funding.GitHub.")) - .Select(x => new { Key = x.Key[15..], x.Value }) - .ToDictionary(x => x.Key, x => x.Value); - - /// - /// Whether the current process is running in an IDE, either - /// or . - /// - public static bool IsEditor => IsVisualStudio || IsRider; - - /// - /// Whether the current process is running as part of an active Visual Studio instance. - /// - public static bool IsVisualStudio => - Environment.GetEnvironmentVariable("ServiceHubLogSessionKey") != null || - Environment.GetEnvironmentVariable("VSAPPIDNAME") != null; - - /// - /// Whether the current process is running as part of an active Rider instance. - /// - public static bool IsRider => - Environment.GetEnvironmentVariable("RESHARPER_FUS_SESSION") != null || - Environment.GetEnvironmentVariable("IDEA_INITIAL_DIRECTORY") != null; - - /// - /// Manages the sharing and reporting of diagnostics across the source generator - /// and the diagnostic analyzer, to avoid doing the online check more than once. - /// - public static DiagnosticsManager Diagnostics { get; } = new(); - - /// - /// Gets the expiration date from the principal, if any. - /// - /// - /// Whichever "exp" claim is the latest, or if none found. - /// - public static DateTime? GetExpiration(this ClaimsPrincipal principal) - // get all "exp" claims, parse them and return the latest one or null if none found - => principal.FindAll("exp") - .Select(c => c.Value) - .Select(long.Parse) - .Select(DateTimeOffset.FromUnixTimeSeconds) - .Max().DateTime is var exp && exp == DateTime.MinValue ? null : exp; - - /// - /// Gets all sponsor manifests from the provided analyzer options. - /// - public static ImmutableArray GetSponsorManifests(this AnalyzerOptions? options) - => options == null ? ImmutableArray.Create() : options.AdditionalFiles - .Where(x => - options.AnalyzerConfigOptionsProvider.GetOptions(x).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) && - itemType == "SponsorManifest" && - Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(x.Path))) - .ToImmutableArray(); - - /// - /// Gets all sponsor manifests from the provided analyzer options. - /// - public static IncrementalValueProvider> GetSponsorManifests(this IncrementalGeneratorInitializationContext context) - => context.AdditionalTextsProvider.Combine(context.AnalyzerConfigOptionsProvider) - .Where(source => - { - var (text, options) = source; - return options.GetOptions(text).TryGetValue("build_metadata.SponsorManifest.ItemType", out var itemType) && - itemType == "SponsorManifest" && - Sponsorables.ContainsKey(Path.GetFileNameWithoutExtension(text.Path)); - }) - .Select((source, c) => source.Left) - .Collect(); - - /// - /// Reads all manifests, validating their signatures. - /// - /// The combined principal with all identities (and their claims) from each provided and valid JWT - /// The tokens to read and their corresponding JWK for signature verification. - /// if at least one manifest can be successfully read and is valid. - /// otherwise. - public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, params (string jwt, string jwk)[] values) - => TryRead(out principal, values.AsEnumerable()); - - /// - /// Reads all manifests, validating their signatures. - /// - /// The combined principal with all identities (and their claims) from each provided and valid JWT - /// The tokens to read and their corresponding JWK for signature verification. - /// if at least one manifest can be successfully read and is valid. - /// otherwise. - public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, IEnumerable<(string jwt, string jwk)> values) - { - principal = null; - - foreach (var value in values) - { - if (string.IsNullOrWhiteSpace(value.jwt) || string.IsNullOrEmpty(value.jwk)) - continue; - - if (Validate(value.jwt, value.jwk, out var token, out var identity, false) == ManifestStatus.Valid && identity != null) - { - if (principal == null) - principal = new JwtRolesPrincipal(identity); - else - principal.AddIdentity(identity); - } - } - - return principal != null; - } - - /// - /// Validates the manifest signature and optional expiration. - /// - /// The JWT to validate. - /// The key to validate the manifest signature with. - /// Except when returning , returns the security token read from the JWT, even if signature check failed. - /// The associated claims, only when return value is not . - /// Whether to check for expiration. - /// The status of the validation. - public static ManifestStatus Validate(string jwt, string jwk, out SecurityToken? token, out ClaimsIdentity? identity, bool validateExpiration) - { - token = default; - identity = default; - - SecurityKey key; - try - { - key = JsonWebKey.Create(jwk); - } - catch (ArgumentException) - { - return ManifestStatus.Unknown; - } - - var handler = new JsonWebTokenHandler { MapInboundClaims = false }; - - if (!handler.CanReadToken(jwt)) - return ManifestStatus.Unknown; - - var validation = new TokenValidationParameters - { - RequireExpirationTime = false, - ValidateLifetime = false, - ValidateAudience = false, - ValidateIssuer = false, - ValidateIssuerSigningKey = true, - IssuerSigningKey = key, - RoleClaimType = "roles", - NameClaimType = "sub", - }; - - var result = handler.ValidateTokenAsync(jwt, validation).Result; - if (result.Exception != null) - { - if (result.Exception is SecurityTokenInvalidSignatureException) - { - var jwtToken = handler.ReadJsonWebToken(jwt); - token = jwtToken; - identity = new ClaimsIdentity(jwtToken.Claims); - return ManifestStatus.Invalid; - } - else - { - var jwtToken = handler.ReadJsonWebToken(jwt); - token = jwtToken; - identity = new ClaimsIdentity(jwtToken.Claims); - return ManifestStatus.Invalid; - } - } - - token = result.SecurityToken; - identity = new ClaimsIdentity(result.ClaimsIdentity.Claims, "JWT"); - - if (validateExpiration && token.ValidTo == DateTime.MinValue) - return ManifestStatus.Invalid; - - // The sponsorable manifest does not have an expiration time. - if (validateExpiration && token.ValidTo < DateTimeOffset.UtcNow) - return ManifestStatus.Expired; - - return ManifestStatus.Valid; - } - - class JwtRolesPrincipal(ClaimsIdentity identity) : ClaimsPrincipal([identity]) - { - public override bool IsInRole(string role) => HasClaim("roles", role) || base.IsInRole(role); - } -} diff --git a/src/SponsorLink/SponsorLink/SponsorLink.csproj b/src/SponsorLink/SponsorLink/SponsorLink.csproj deleted file mode 100644 index 740b146..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLink.csproj +++ /dev/null @@ -1,79 +0,0 @@ - - - - netstandard2.0 - SponsorLink - disable - false - CoreResGen;$(CoreCompileDependsOn) - - - - - $(Product) - - $([System.Text.RegularExpressions.Regex]::Replace("$(FundingProduct)", "[^A-Z]", "")) - - 21 - - - - - - - - - - - - - - - - - - - - - - - - namespace Devlooped.Sponsors%3B - -partial class SponsorLink -{ - public partial class Funding - { - public const string Product = "$(FundingProduct)"%3B - public const string Prefix = "$(FundingPrefix)"%3B - public const int Grace = $(FundingGrace)%3B - } -} - - - - - - - - - - - - - - - - - - - - - $([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\$(BaseIntermediateOutputPath)devlooped.jwk')) - - - - - - - diff --git a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs b/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs deleted file mode 100644 index 0cf507f..0000000 --- a/src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs +++ /dev/null @@ -1,89 +0,0 @@ -// -#nullable enable -using System; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using static Devlooped.Sponsors.SponsorLink; - -namespace Devlooped.Sponsors; - -/// -/// Links the sponsor status for the current compilation. -/// -[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] -public class SponsorLinkAnalyzer : DiagnosticAnalyzer -{ - public override ImmutableArray SupportedDiagnostics { get; } = DiagnosticsManager.KnownDescriptors.Values.ToImmutableArray(); - -#pragma warning disable RS1026 // Enable concurrent execution - public override void Initialize(AnalysisContext context) -#pragma warning restore RS1026 // Enable concurrent execution - { -#if !DEBUG - // Only enable concurrent execution in release builds, otherwise debugging is quite annoying. - context.EnableConcurrentExecution(); -#endif - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - -#pragma warning disable RS1013 // Start action has no registered non-end actions - // We do this so that the status is set at compilation start so we can use it - // across all other analyzers. We report only on finish because multiple - // analyzers can report the same diagnostic and we want to avoid duplicates. - context.RegisterCompilationStartAction(ctx => - { - // Setting the status early allows other analyzers to potentially check for it. - var status = Diagnostics.GetOrSetStatus(() => ctx.Options); - - // Never report any diagnostic unless we're in an editor. - if (IsEditor) - { - ctx.RegisterCompilationEndAction(ctx => - { - // NOTE: for multiple projects with the same product name, we only report one diagnostic, - // so it's expected to NOT get a diagnostic back. Also, we don't want to report - // multiple diagnostics for each project in a solution that uses the same product. - Diagnostics.ReportOnce(diagnostic => - { - // For unknown (never sync'ed), only report if install grace period is over - if (status == SponsorStatus.Unknown) - { - var noGrace = ctx.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.SponsorLinkNoInstallGrace", out var value) && - bool.TryParse(value, out var skipCheck) && skipCheck; - - // NOTE: we'll always report if noGrace is set to true, regardless of install time, for - // testing purposes. This can be achieved via MSBuild with: - // - // true - // - // - // - // - if (noGrace == false) - { - var installed = ctx.Options.AdditionalFiles.Where(x => - { - var options = ctx.Options.AnalyzerConfigOptionsProvider.GetOptions(x); - // In release builds, we'll have a single such item, since we IL-merge the analyzer. - return options.TryGetValue("build_metadata.Analyzer.ItemType", out var itemType) && - options.TryGetValue("build_metadata.Analyzer.NuGetPackageId", out var packageId) && - itemType == "Analyzer" && - packageId == Funding.Product; - }).Select(x => File.GetLastWriteTime(x.Path)).OrderByDescending(x => x).FirstOrDefault(); - - // NOTE: if we can't determine install time, we'll always report. - if (installed != default && installed.AddDays(Funding.Grace) > DateTime.Now) - return; - } - } - - ctx.ReportDiagnostic(diagnostic); - }); - }); - } - }); -#pragma warning restore RS1013 // Start action has no registered non-end actions - } -} diff --git a/src/SponsorLink/SponsorLink/SponsorStatus.cs b/src/SponsorLink/SponsorLink/SponsorStatus.cs deleted file mode 100644 index 6cdbc90..0000000 --- a/src/SponsorLink/SponsorLink/SponsorStatus.cs +++ /dev/null @@ -1,25 +0,0 @@ -// -namespace Devlooped.Sponsors; - -/// -/// The determined sponsoring status. -/// -public enum SponsorStatus -{ - /// - /// Sponsorship status is unknown. - /// - Unknown, - /// - /// The sponsors manifest is expired but within the grace period. - /// - Expiring, - /// - /// The sponsors manifest is expired and outside the grace period. - /// - Expired, - /// - /// The user is sponsoring. - /// - Sponsor, -} diff --git a/src/SponsorLink/SponsorLink/SponsorableLib.targets b/src/SponsorLink/SponsorLink/SponsorableLib.targets deleted file mode 100644 index 8311ca6..0000000 --- a/src/SponsorLink/SponsorLink/SponsorableLib.targets +++ /dev/null @@ -1,60 +0,0 @@ - - - - - $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)sponsorable.md)) - - - - - - - - - - $(WarningsNotAsErrors);LIB001;LIB002;LIB003;LIB004;LIB005 - - $(BaseIntermediateOutputPath)autosync.stamp - - $(HOME) - $(USERPROFILE) - - true - $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink')) - - - - - - - - - - - - - - - - - - - - - - - - - - %(GitRoot.FullPath) - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/Tracing.cs b/src/SponsorLink/SponsorLink/Tracing.cs deleted file mode 100644 index ad5d9b3..0000000 --- a/src/SponsorLink/SponsorLink/Tracing.cs +++ /dev/null @@ -1,49 +0,0 @@ -// -#nullable enable -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.CompilerServices; -using System.Text; - -namespace Devlooped.Sponsors; - -static class Tracing -{ - public static void Trace([CallerMemberName] string? message = null, [CallerFilePath] string? filePath = null, [CallerLineNumber] int lineNumber = 0) - { - var trace = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SPONSORLINK_TRACE")); -#if DEBUG - trace = true; -#endif - - if (!trace) - return; - - var line = new StringBuilder() - .Append($"[{DateTime.Now:O}]") - .Append($"[{Process.GetCurrentProcess().ProcessName}:{Process.GetCurrentProcess().Id}]") - .Append($" {message} ") - .AppendLine($" -> {filePath}({lineNumber})") - .ToString(); - - var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sponsorlink"); - Directory.CreateDirectory(dir); - - var tries = 0; - // Best-effort only - while (tries < 10) - { - try - { - File.AppendAllText(Path.Combine(dir, "trace.log"), line); - Debugger.Log(0, "SponsorLink", line); - return; - } - catch (IOException) - { - tries++; - } - } - } -} diff --git a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets deleted file mode 100644 index 9f843e2..0000000 --- a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets +++ /dev/null @@ -1,118 +0,0 @@ - - - - - $([System.DateTime]::Now.ToString("yyyy-MM-yy")) - - $(BaseIntermediateOutputPath)autosync-$(Today).stamp - - $(BaseIntermediateOutputPath)autosync.stamp - - $(HOME) - $(USERPROFILE) - - $([System.IO.Path]::GetFullPath('$(UserProfileHome)/.sponsorlink')) - - $([System.IO.Path]::Combine('$(SponsorLinkHome)', '.netconfig')) - - - - - - - - - - - - - - - - SL_CollectDependencies;SL_CollectSponsorableAnalyzer - $(SLDependsOn);SL_CheckAutoSync;SL_ReadAutoSyncEnabled;SL_SyncSponsors - - - - - - - - - - - - - - - - - - - - - - - %(SponsorablePackageId.Identity) - - - - - - - - - - - - - - - - - - - - - - - - - %(SLConfigAutoSync.Identity) - true - false - - - - - - - - $([System.IO.File]::ReadAllText($(AutoSyncStampFile)).Trim()) - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/SponsorLink/SponsorLink/sponsorable.md b/src/SponsorLink/SponsorLink/sponsorable.md deleted file mode 100644 index c023c25..0000000 --- a/src/SponsorLink/SponsorLink/sponsorable.md +++ /dev/null @@ -1,5 +0,0 @@ -# Why Sponsor - -Well, why not? It's super cheap :) - -This could even be partially auto-generated from FUNDING.yml and what-not. \ No newline at end of file diff --git a/src/SponsorLink/SponsorLinkAnalyzer.sln b/src/SponsorLink/SponsorLinkAnalyzer.sln deleted file mode 100644 index be206b1..0000000 --- a/src/SponsorLink/SponsorLinkAnalyzer.sln +++ /dev/null @@ -1,43 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.10.34928.147 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzer", "Analyzer\Analyzer.csproj", "{584984D6-926B-423D-9416-519613423BAE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Library", "Library\Library.csproj", "{598CD398-A172-492C-8367-827D43276029}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{EA02494C-6ED4-47A0-8D43-20F50BE8554F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SponsorLink", "SponsorLink\SponsorLink.csproj", "{B91C7E99-3D2E-4FDF-B017-9123E810197F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {584984D6-926B-423D-9416-519613423BAE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {584984D6-926B-423D-9416-519613423BAE}.Release|Any CPU.Build.0 = Release|Any CPU - {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {598CD398-A172-492C-8367-827D43276029}.Debug|Any CPU.Build.0 = Debug|Any CPU - {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.ActiveCfg = Release|Any CPU - {598CD398-A172-492C-8367-827D43276029}.Release|Any CPU.Build.0 = Release|Any CPU - {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EA02494C-6ED4-47A0-8D43-20F50BE8554F}.Release|Any CPU.Build.0 = Release|Any CPU - {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B91C7E99-3D2E-4FDF-B017-9123E810197F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {1DDA0EFF-BEF6-49BB-8AA8-D71FE1CD3E6F} - EndGlobalSection -EndGlobal diff --git a/src/SponsorLink/Tests/.netconfig b/src/SponsorLink/Tests/.netconfig deleted file mode 100644 index 092c205..0000000 --- a/src/SponsorLink/Tests/.netconfig +++ /dev/null @@ -1,17 +0,0 @@ -[config] - root = true -[file "SponsorableManifest.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/SponsorableManifest.cs - sha = 5a4cad3a084f53afe34a6b75e4f3a084a0f1bf9e - etag = 9a07c856d06e0cde629fce3ec014f64f9adfd5ae5805a35acf623eba0ee045c1 - weak -[file "JsonOptions.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/JsonOptions.cs - sha = 80ea1bfe47049ef6c6ed4f424dcf7febb729cbba - etag = 17799725ad9b24eb5998365962c30b9a487bddadca37c616e35b76b8c9eb161a - weak -[file "Extensions.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/src/Core/Extensions.cs - sha = c455f6fa1a4d404181d076d7f3362345c8ed7df2 - etag = 9e51b7e6540fae140490a5283b1e67ce071bd18a267bc2ae0b35c7248261aed1 - weak \ No newline at end of file diff --git a/src/SponsorLink/Tests/Attributes.cs b/src/SponsorLink/Tests/Attributes.cs deleted file mode 100644 index aa5f48d..0000000 --- a/src/SponsorLink/Tests/Attributes.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Xunit; - -public class SecretsFactAttribute : FactAttribute -{ - public SecretsFactAttribute(params string[] secrets) - { - var configuration = new ConfigurationBuilder() - .AddUserSecrets() - .Build(); - - var missing = new HashSet(); - - foreach (var secret in secrets) - { - if (string.IsNullOrEmpty(configuration[secret])) - missing.Add(secret); - } - - if (missing.Count > 0) - Skip = "Missing user secrets: " + string.Join(',', missing); - } -} - -public class LocalFactAttribute : SecretsFactAttribute -{ - public LocalFactAttribute(params string[] secrets) : base(secrets) - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) - Skip = "Non-CI test"; - } -} - -public class CIFactAttribute : FactAttribute -{ - public CIFactAttribute() - { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) - Skip = "CI-only test"; - } -} - -public class LocalTheoryAttribute : TheoryAttribute -{ - public LocalTheoryAttribute() - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) - Skip = "Non-CI test"; - } -} - -public class CITheoryAttribute : TheoryAttribute -{ - public CITheoryAttribute() - { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) - Skip = "CI-only test"; - } -} \ No newline at end of file diff --git a/src/SponsorLink/Tests/Extensions.cs b/src/SponsorLink/Tests/Extensions.cs deleted file mode 100644 index 4063f78..0000000 --- a/src/SponsorLink/Tests/Extensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace Devlooped.Sponsors; - -static class Extensions -{ - public static HashCode Add(this HashCode hash, params object[] items) - { - foreach (var item in items) - hash.Add(item); - - return hash; - } - - - public static HashCode AddRange(this HashCode hash, IEnumerable items) - { - foreach (var item in items) - hash.Add(item); - - return hash; - } - - public static bool ThumbprintEquals(this SecurityKey key, RSA rsa) => key.ThumbprintEquals(new RsaSecurityKey(rsa)); - - public static bool ThumbprintEquals(this RSA rsa, SecurityKey key) => key.ThumbprintEquals(rsa); - - public static bool ThumbprintEquals(this SecurityKey first, SecurityKey second) - { - var expectedKey = JsonWebKeyConverter.ConvertFromSecurityKey(second); - var actualKey = JsonWebKeyConverter.ConvertFromSecurityKey(first); - return expectedKey.ComputeJwkThumbprint().AsSpan().SequenceEqual(actualKey.ComputeJwkThumbprint()); - } - - public static Array Cast(this Array array, Type elementType) - { - //Convert the object list to the destination array type. - var result = Array.CreateInstance(elementType, array.Length); - Array.Copy(array, result, array.Length); - return result; - } - - public static void Assert(this ILogger logger, [DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression(nameof(condition))] string? message = default, params object?[] args) - { - if (!condition) - { - //Debug.Assert(condition, message); - logger.LogError(message, args); - throw new InvalidOperationException(message); - } - } -} diff --git a/src/SponsorLink/Tests/JsonOptions.cs b/src/SponsorLink/Tests/JsonOptions.cs deleted file mode 100644 index b2349b0..0000000 --- a/src/SponsorLink/Tests/JsonOptions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using Microsoft.IdentityModel.Tokens; - -namespace Devlooped.Sponsors; - -static partial class JsonOptions -{ - public static JsonSerializerOptions Default { get; } = -#if NET6_0_OR_GREATER - new(JsonSerializerDefaults.Web) -#else - new() -#endif - { - AllowTrailingCommas = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReadCommentHandling = JsonCommentHandling.Skip, -#if NET6_0_OR_GREATER - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull, -#endif - WriteIndented = true, - Converters = - { - new JsonStringEnumConverter(allowIntegerValues: false), -#if NET6_0_OR_GREATER - new DateOnlyJsonConverter() -#endif - } - }; - - public static JsonSerializerOptions JsonWebKey { get; } = new(JsonSerializerOptions.Default) - { - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault | JsonIgnoreCondition.WhenWritingNull, - TypeInfoResolver = new DefaultJsonTypeInfoResolver - { - Modifiers = - { - info => - { - if (info.Type != typeof(JsonWebKey)) - return; - - foreach (var prop in info.Properties) - { - // Don't serialize empty lists, makes for more concise JWKs - prop.ShouldSerialize = (obj, value) => - value is not null && - (value is not IList list || list.Count > 0); - } - } - } - } - }; - - -#if NET6_0_OR_GREATER - public class DateOnlyJsonConverter : JsonConverter - { - public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => DateOnly.Parse(reader.GetString()?[..10] ?? "", CultureInfo.InvariantCulture); - - public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture)); - } -#endif -} diff --git a/src/SponsorLink/Tests/Resources.Designer.cs b/src/SponsorLink/Tests/Resources.Designer.cs deleted file mode 100644 index 7824a60..0000000 --- a/src/SponsorLink/Tests/Resources.Designer.cs +++ /dev/null @@ -1,63 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Tests { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Tests.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - } -} diff --git a/src/SponsorLink/Tests/Resources.resx b/src/SponsorLink/Tests/Resources.resx deleted file mode 100644 index 4fdb1b6..0000000 --- a/src/SponsorLink/Tests/Resources.resx +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/src/SponsorLink/Tests/Sample.cs b/src/SponsorLink/Tests/Sample.cs deleted file mode 100644 index 3ea4a32..0000000 --- a/src/SponsorLink/Tests/Sample.cs +++ /dev/null @@ -1,69 +0,0 @@ -extern alias Analyzer; -using System; -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using Analyzer::Devlooped.Sponsors; -using Microsoft.CodeAnalysis; -using Xunit; -using Xunit.Abstractions; - -namespace Tests; - -public class Sample(ITestOutputHelper output) -{ - [Theory] - [InlineData("es-AR", SponsorStatus.Unknown)] - [InlineData("es-AR", SponsorStatus.Expiring)] - [InlineData("es-AR", SponsorStatus.Expired)] - [InlineData("es-AR", SponsorStatus.Sponsor)] - [InlineData("en", SponsorStatus.Unknown)] - [InlineData("en", SponsorStatus.Expiring)] - [InlineData("en", SponsorStatus.Expired)] - [InlineData("en", SponsorStatus.Sponsor)] - [InlineData("", SponsorStatus.Unknown)] - [InlineData("", SponsorStatus.Expiring)] - [InlineData("", SponsorStatus.Expired)] - [InlineData("", SponsorStatus.Sponsor)] - public void Test(string culture, SponsorStatus kind) - { - Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = - culture == "" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(culture); - - var diag = GetDescriptor(["foo"], "bar", "FB", kind); - - output.WriteLine(diag.Title.ToString()); - output.WriteLine(diag.MessageFormat.ToString()); - output.WriteLine(diag.Description.ToString()); - } - - [Fact] - public void RenderSponsorables() - { - Assert.NotEmpty(SponsorLink.Sponsorables); - - foreach (var pair in SponsorLink.Sponsorables) - { - output.WriteLine($"{pair.Key} = {pair.Value}"); - // Read the JWK - var jsonWebKey = Microsoft.IdentityModel.Tokens.JsonWebKey.Create(pair.Value); - - Assert.NotNull(jsonWebKey); - - using var key = RSA.Create(new RSAParameters - { - Modulus = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.N), - Exponent = Microsoft.IdentityModel.Tokens.Base64UrlEncoder.DecodeBytes(jsonWebKey.E), - }); - } - } - - DiagnosticDescriptor GetDescriptor(string[] sponsorable, string product, string prefix, SponsorStatus status) => status switch - { - SponsorStatus.Unknown => DiagnosticsManager.CreateUnknown(sponsorable, product, prefix), - SponsorStatus.Sponsor => DiagnosticsManager.CreateSponsor(sponsorable, prefix), - SponsorStatus.Expiring => DiagnosticsManager.CreateExpiring(sponsorable, prefix), - SponsorStatus.Expired => DiagnosticsManager.CreateExpired(sponsorable, prefix), - _ => throw new NotImplementedException(), - }; -} \ No newline at end of file diff --git a/src/SponsorLink/Tests/SponsorLinkTests.cs b/src/SponsorLink/Tests/SponsorLinkTests.cs deleted file mode 100644 index 7625e2c..0000000 --- a/src/SponsorLink/Tests/SponsorLinkTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -extern alias Analyzer; -using System.Security.Cryptography; -using System.Text.Json; -using Analyzer::Devlooped.Sponsors; -using Devlooped.Sponsors; -using Microsoft.IdentityModel.Tokens; -using Xunit; - -namespace Devlooped.Tests; - -public class SponsorLinkTests -{ - // We need to convert to jwk string since the analyzer project has merged the JWT assembly and types. - public static string ToJwk(SecurityKey key) - => JsonSerializer.Serialize( - JsonWebKeyConverter.ConvertFromSecurityKey(key), - JsonOptions.JsonWebKey); - - [Fact] - public void ValidateSponsorable() - { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwt = manifest.ToJwt(); - var jwk = ToJwk(manifest.SecurityKey); - - // NOTE: sponsorable manifest doesn't have expiration date. - var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false); - - Assert.Equal(ManifestStatus.Valid, status); - } - - [Fact] - public void ValidateWrongKey() - { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwt = manifest.ToJwt(); - var jwk = ToJwk(new RsaSecurityKey(RSA.Create())); - - var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false); - - Assert.Equal(ManifestStatus.Invalid, status); - - // We should still be a able to read the data, knowing it may have been tampered with. - Assert.NotNull(principal); - Assert.NotNull(token); - } - - [Fact] - public void ValidateExpiredSponsor() - { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwk = ToJwk(manifest.SecurityKey); - var sponsor = manifest.Sign([], expiration: TimeSpan.Zero); - - // Will be expired after this. - Thread.Sleep(1000); - - var status = SponsorLink.Validate(sponsor, jwk, out var token, out var principal, true); - - Assert.Equal(ManifestStatus.Expired, status); - - // We should still be a able to read the data, even if expired (but not tampered with). - Assert.NotNull(principal); - Assert.NotNull(token); - } - - [Fact] - public void ValidateUnknownFormat() - { - var manifest = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); - var jwk = ToJwk(manifest.SecurityKey); - - var status = SponsorLink.Validate("asdfasdf", jwk, out var token, out var principal, false); - - Assert.Equal(ManifestStatus.Unknown, status); - - // Nothing could be read at all. - Assert.Null(principal); - Assert.Null(token); - } - - [Fact] - public void TryRead() - { - var fooSponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/foo")], "ASDF1234"); - var barSponsorable = SponsorableManifest.Create(new Uri("https://bar.com"), [new Uri("https://github.com/sponsors/bar")], "GHJK5678"); - - // Org sponsor and member of team - var fooSponsor = fooSponsorable.Sign([new("sub", "kzu"), new("email", "me@foo.com"), new("roles", "org"), new("roles", "team")], expiration: TimeSpan.FromDays(30)); - // Org + personal sponsor - var barSponsor = barSponsorable.Sign([new("sub", "kzu"), new("email", "me@bar.com"), new("roles", "org"), new("roles", "user")], expiration: TimeSpan.FromDays(30)); - - Assert.True(SponsorLink.TryRead(out var principal, [(fooSponsor, ToJwk(fooSponsorable.SecurityKey)), (barSponsor, ToJwk(barSponsorable.SecurityKey))])); - - // Can check role across both JWTs - Assert.True(principal.IsInRole("org")); - Assert.True(principal.IsInRole("team")); - Assert.True(principal.IsInRole("user")); - - Assert.True(principal.HasClaim("sub", "kzu")); - Assert.True(principal.HasClaim("email", "me@foo.com")); - Assert.True(principal.HasClaim("email", "me@bar.com")); - } - - [LocalFact] - public void ValidateCachedManifest() - { - var path = Environment.ExpandEnvironmentVariables("%userprofile%\\.sponsorlink\\github\\devlooped.jwt"); - if (!File.Exists(path)) - return; - - var jwt = File.ReadAllText(path); - - var status = SponsorLink.Validate(jwt, - """ - { - "e": "AQAB", - "kty": "RSA", - "n": "5inhv8QymaDBOihNi1eY-6-hcIB5qSONFZxbxxXAyOtxAdjFCPM-94gIZqM9CDrX3pyg1lTJfml_a_FZSU9dB1ii5mSX_mNHBFXn1_l_gi1ErdbkIF5YbW6oxWFxf3G5mwVXwnPfxHTyQdmWQ3YJR-A3EB4kaFwLqA6Ha5lb2ObGpMTQJNakD4oTAGDhqHMGhu6PupGq5ie4qZcQ7N8ANw8xH7nicTkbqEhQABHWOTmLBWq5f5F6RYGF8P7cl0IWl_w4YcIZkGm2vX2fi26F9F60cU1v13GZEVDTXpJ9kzvYeM9sYk6fWaoyY2jhE51qbv0B0u6hScZiLREtm3n7ClJbIGXhkUppFS2JlNaX3rgQ6t-4LK8gUTyLt3zDs2H8OZyCwlCpfmGmdsUMkm1xX6t2r-95U3zywynxoWZfjBCJf41leM9OMKYwNWZ6LQMyo83HWw1PBIrX4ZLClFwqBcSYsXDyT8_ZLd1cdYmPfmtllIXxZhLClwT5qbCWv73V" - } - """ - , out var token, out var principal, false); - - Assert.Equal(ManifestStatus.Valid, status); - } -} diff --git a/src/SponsorLink/Tests/SponsorableManifest.cs b/src/SponsorLink/Tests/SponsorableManifest.cs deleted file mode 100644 index d65d0fb..0000000 --- a/src/SponsorLink/Tests/SponsorableManifest.cs +++ /dev/null @@ -1,351 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text.Json; -using Microsoft.IdentityModel.JsonWebTokens; -using Microsoft.IdentityModel.Tokens; - -namespace Devlooped.Sponsors; - -/// -/// The serializable manifest of a sponsorable user, as persisted -/// in the .github/sponsorlink.jwt file. -/// -public class SponsorableManifest -{ - /// - /// Overall manifest status. - /// - public enum Status - { - /// - /// SponsorLink manifest is invalid. - /// - Invalid, - /// - /// The manifest has an audience that doesn't match the sponsorable account. - /// - AccountMismatch, - /// - /// SponsorLink manifest not found for the given account, so it's not supported. - /// - NotFound, - /// - /// Manifest was successfully fetched and validated. - /// - OK, - } - - /// - /// Creates a new manifest with a new RSA key pair. - /// - public static SponsorableManifest Create(Uri issuer, Uri[] audience, string clientId) - { - var rsa = RSA.Create(3072); - return new SponsorableManifest(issuer, audience, clientId, new RsaSecurityKey(rsa)); - } - - public static async Task<(Status, SponsorableManifest?)> FetchAsync(string sponsorable, string? branch, HttpClient? http = default) - { - // Try to detect sponsorlink manifest in the sponsorable .github repo - var url = $"https://github.com/{sponsorable}/.github/raw/{branch ?? "main"}/sponsorlink.jwt"; - var disposeHttp = http == null; - - // Manifest should be public, so no need for any special HTTP client. - try - { - var response = await (http ?? new HttpClient()).GetAsync(url); - if (!response.IsSuccessStatusCode) - return (Status.NotFound, default); - - var jwt = await response.Content.ReadAsStringAsync(); - if (!TryRead(jwt, out var manifest, out _)) - return (Status.Invalid, default); - - // Manifest audience should match the sponsorable account to avoid weird issues? - if (sponsorable != manifest.Sponsorable) - return (Status.AccountMismatch, default); - - return (Status.OK, manifest); - } - finally - { - if (disposeHttp) - http?.Dispose(); - } - } - - /// - /// Parses a JWT into a . - /// - /// The JWT containing the sponsorable information. - /// The parsed manifest, if not required claims are missing. - /// The missing required claim, if any. - /// A validated manifest. - public static bool TryRead(string jwt, [NotNullWhen(true)] out SponsorableManifest? manifest, out string? missingClaim) - { - var handler = new JsonWebTokenHandler - { - MapInboundClaims = false, - SetDefaultTimesOnTokenCreation = false, - }; - missingClaim = null; - manifest = default; - - if (!handler.CanReadToken(jwt)) - return false; - - var token = handler.ReadJsonWebToken(jwt); - var issuer = token.Issuer; - - if (token.Audiences.FirstOrDefault(x => x.StartsWith("https://github.com/")) is null) - { - missingClaim = "aud"; - return false; - } - - if (token.Claims.FirstOrDefault(c => c.Type == "client_id")?.Value is not string clientId) - { - missingClaim = "client_id"; - return false; - } - - if (token.Claims.FirstOrDefault(c => c.Type == "sub_jwk")?.Value is not string jwk) - { - missingClaim = "sub_jwk"; - return false; - } - - var key = new JsonWebKeySet { Keys = { JsonWebKey.Create(jwk) } }.GetSigningKeys().First(); - manifest = new SponsorableManifest(new Uri(issuer), token.Audiences.Select(x => new Uri(x)).ToArray(), clientId, key); - - return true; - } - - int hashcode; - string clientId; - string issuer; - - public SponsorableManifest(Uri issuer, Uri[] audience, string clientId, SecurityKey publicKey) - { - this.clientId = clientId; - this.issuer = issuer.AbsoluteUri; - Audience = audience.Select(a => a.AbsoluteUri.TrimEnd('/')).ToArray(); - SecurityKey = publicKey; - Sponsorable = audience.Where(x => x.Host == "github.com").Select(x => x.Segments.LastOrDefault()?.TrimEnd('/')).FirstOrDefault() ?? - throw new ArgumentException("At least one of the intended audience must be a GitHub sponsors URL."); - - // Force hash code to be computed - ClientId = clientId; - } - - /// - /// Converts (and optionally signs) the manifest into a JWT. Never exports the private key. - /// - /// Optional credentials when signing the resulting manifest. Defaults to the if it has a private key. - /// The JWT manifest. - public string ToJwt(SigningCredentials? signing = default) - { - var jwk = JsonWebKeyConverter.ConvertFromSecurityKey(SecurityKey); - - // Automatically sign if the manifest was created with a private key - if (SecurityKey is RsaSecurityKey rsa && rsa.PrivateKeyStatus == PrivateKeyStatus.Exists) - { - signing ??= new SigningCredentials(rsa, SecurityAlgorithms.RsaSha256); - - // Ensure we never serialize the private key - jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(new RsaSecurityKey(rsa.Rsa.ExportParameters(false))); - } - - var claims = - new[] { new Claim(JwtRegisteredClaimNames.Iss, Issuer) } - .Concat(Audience.Select(x => new Claim(JwtRegisteredClaimNames.Aud, x))) - .Concat( - [ - // See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6 - new("client_id", ClientId), - // standard claim, serialized as a JSON string, not an encoded JSON object - new("sub_jwk", JsonSerializer.Serialize(jwk, JsonOptions.JsonWebKey), JsonClaimValueTypes.Json), - ]); - - var handler = new JsonWebTokenHandler - { - MapInboundClaims = false, - SetDefaultTimesOnTokenCreation = false, - }; - - return handler.CreateToken(new SecurityTokenDescriptor - { - IssuedAt = DateTime.UtcNow, - Subject = new ClaimsIdentity(claims), - SigningCredentials = signing, - }); - } - - /// - /// Sign the JWT claims with the provided RSA key. - /// - public string Sign(IEnumerable claims, RSA rsa, TimeSpan? expiration = default) - => Sign(claims, new RsaSecurityKey(rsa), expiration); - - public string Sign(IEnumerable claims, RsaSecurityKey? key = default, TimeSpan? expiration = default) - { - var rsa = key ?? SecurityKey as RsaSecurityKey; - if (rsa?.PrivateKeyStatus != PrivateKeyStatus.Exists) - throw new NotSupportedException("No private key found or specified to sign the manifest."); - - var signing = new SigningCredentials(rsa, SecurityAlgorithms.RsaSha256); - - var expirationDate = expiration != null ? - DateTime.UtcNow.Add(expiration.Value) : - // Expire the first day of the next month - new DateTime( - DateTime.UtcNow.AddMonths(1).Year, - DateTime.UtcNow.AddMonths(1).Month, 1, - // Use current time so they don't expire all at the same time - DateTime.UtcNow.Hour, - DateTime.UtcNow.Minute, - DateTime.UtcNow.Second, - DateTime.UtcNow.Millisecond, - DateTimeKind.Utc); - - // Removed as we set IssuedAt = DateTime.UtcNow - var tokenClaims = claims.Where(x => x.Type != JwtRegisteredClaimNames.Iat && x.Type != JwtRegisteredClaimNames.Exp).ToList(); - - if (tokenClaims.Find(c => c.Type == JwtRegisteredClaimNames.Iss) is { } issuer) - { - if (issuer.Value != Issuer) - throw new ArgumentException($"The received claims contain an incompatible 'iss' claim. If present, the claim must contain the value '{Issuer}' but was '{issuer.Value}'."); - } - else - { - tokenClaims.Insert(0, new(JwtRegisteredClaimNames.Iss, Issuer)); - } - - if (tokenClaims.Find(c => c.Type == "client_id") is { } clientId) - { - if (clientId.Value != ClientId) - throw new ArgumentException($"The received claims contain an incompatible 'client_id' claim. If present, the claim must contain the value '{ClientId}' but was '{clientId.Value}'."); - } - else - { - tokenClaims.Add(new("client_id", ClientId)); - } - - // Avoid duplicating audience claims - foreach (var audience in Audience) - { - // Always compare ignoring trailing / - if (tokenClaims.Find(c => c.Type == JwtRegisteredClaimNames.Aud && c.Value.TrimEnd('/') == audience.TrimEnd('/')) == null) - tokenClaims.Insert(1, new(JwtRegisteredClaimNames.Aud, audience)); - } - - // Don't allow mismatches of public manifest key and the one used to sign, to avoid - // weird run-time errors verifiying manifests that were signed with a different key. - if (!rsa.ThumbprintEquals(SecurityKey)) - throw new ArgumentException($"Cannot sign with a private key that does not match the manifest public key."); - - return new JsonWebTokenHandler - { - MapInboundClaims = false, - SetDefaultTimesOnTokenCreation = false, - }.CreateToken(new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(tokenClaims), - IssuedAt = DateTime.UtcNow, - Expires = expirationDate, - SigningCredentials = signing, - }); - } - - public ClaimsIdentity Validate(string jwt, out SecurityToken? token) - { - var validation = new TokenValidationParameters - { - RequireExpirationTime = true, - // NOTE: setting this to false allows checking sponsorships even when the manifest is expired. - // This might be useful if package authors want to extend the manifest lifetime beyond the default - // 30 days and issue a warning on expiration, rather than an error and a forced sync. - // If this is not set (or true), a SecurityTokenExpiredException exception will be thrown. - ValidateLifetime = false, - RequireAudience = true, - // At least one of the audiences must match the manifest audiences - AudienceValidator = (audiences, _, _) => Audience.Intersect(audiences.Select(x => x.TrimEnd('/'))).Any(), - // We don't validate the issuer in debug builds, to allow testing with localhost-run backend. -#if DEBUG - ValidateIssuer = false, -#else - ValidIssuer = Issuer, -#endif - IssuerSigningKey = SecurityKey, - }; - - var result = new JsonWebTokenHandler - { - MapInboundClaims = false, - SetDefaultTimesOnTokenCreation = false, - }.ValidateTokenAsync(jwt, validation).Result; - - token = result.SecurityToken; - return result.ClaimsIdentity; - } - - /// - /// Gets the GitHub sponsorable account. - /// - public string Sponsorable { get; } - - /// - /// The web endpoint that issues signed JWT to authenticated users. - /// - /// - /// See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1 - /// - public string Issuer - { - get => issuer; - internal set - { - issuer = value; - var thumb = JsonWebKeyConverter.ConvertFromSecurityKey(SecurityKey).ComputeJwkThumbprint(); - hashcode = new HashCode().Add(Issuer, ClientId, Convert.ToBase64String(thumb)).AddRange(Audience).ToHashCode(); - } - } - - /// - /// The audience for the JWT, which includes the sponsorable account and potentially other sponsoring platforms. - /// - /// - /// See https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3 - /// - public string[] Audience { get; } - - /// - /// The OAuth client ID (i.e. GitHub OAuth App ID) that is used to - /// authenticate the user. - /// - /// - /// See https://www.rfc-editor.org/rfc/rfc8693.html#name-client_id-client-identifier - /// - public string ClientId - { - get => clientId; - internal set - { - clientId = value; - var thumb = JsonWebKeyConverter.ConvertFromSecurityKey(SecurityKey).ComputeJwkThumbprint(); - hashcode = new HashCode().Add(Issuer, ClientId, Convert.ToBase64String(thumb)).AddRange(Audience).ToHashCode(); - } - } - - /// - /// Public key in a format that can be used to verify JWT signatures. - /// - public SecurityKey SecurityKey { get; } - - /// - public override int GetHashCode() => hashcode; - - /// - public override bool Equals(object? obj) => obj is SponsorableManifest other && GetHashCode() == other.GetHashCode(); -} diff --git a/src/SponsorLink/Tests/Tests.csproj b/src/SponsorLink/Tests/Tests.csproj deleted file mode 100644 index 5082c97..0000000 --- a/src/SponsorLink/Tests/Tests.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - net8.0 - true - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - - - - - - %(GitRoot.FullPath) - - - - - - - - - - - - - - - - - true - - - - - \ No newline at end of file diff --git a/src/SponsorLink/jwk.ps1 b/src/SponsorLink/jwk.ps1 deleted file mode 100644 index c66f56f..0000000 --- a/src/SponsorLink/jwk.ps1 +++ /dev/null @@ -1 +0,0 @@ -curl https://raw.githubusercontent.com/devlooped/.github/main/sponsorlink.jwt --silent | jq -R 'split(".") | .[1] | @base64d | fromjson' | jq '.sub_jwk' \ No newline at end of file diff --git a/src/SponsorLink/readme.md b/src/SponsorLink/readme.md deleted file mode 100644 index cb651a1..0000000 --- a/src/SponsorLink/readme.md +++ /dev/null @@ -1,34 +0,0 @@ -# SponsorLink .NET Analyzer - -This is one opinionated implementation of [SponsorLink](https://devlooped.com/SponsorLink) -for .NET projects leveraging Roslyn analyzers. - -It is intended for use by [devlooped](https://github.com/devlooped) projects, but can be -used as a template for other sponsorables as well. Supporting arbitrary sponsoring scenarios -is out of scope though, since we just use GitHub sponsors for now. - -## Usage - -A project initializing from this template repo via [dotnet-file](https://github.com/devlooped/dotnet-file) -will have all the sources cloned under `src\SponsorLink`. - -Including the analyzer and targets in a project involves two steps. - -1. Create an analyzer project and add the following property: - -```xml - - ... - $(MSBuildThisFileDirectory)..\SponsorLink\SponsorLink.targets - -``` - -2. Add a `buildTransitive\[PackageId].targets` file with the following import: - -```xml - - - -``` - -As long as NuGetizer is used, the right packaging will be done automatically. \ No newline at end of file