diff --git a/.editorconfig b/.editorconfig index 4cab270..3ba6837 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,6 +30,8 @@ indent_size = 2 # Dotnet code style settings: [*.{cs,vb}] +tab_width = 4 + # Sort using and Import directives with System.* appearing first dotnet_sort_system_directives_first = true # Avoid "this." and "Me." if not necessary @@ -57,6 +59,9 @@ dotnet_style_require_accessibility_modifiers = omit_if_default:error # IDE0040: Add accessibility modifiers dotnet_diagnostic.IDE0040.severity = error +# IDE1100: Error reading content of source file 'Project.TargetFrameworkMoniker' (i.e. from ThisAssembly) +dotnet_diagnostic.IDE1100.severity = none + [*.cs] # Top-level files are definitely OK csharp_using_directive_placement = outside_namespace:silent diff --git a/.gitattributes b/.gitattributes index 4f89148..3095556 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ # normalize by default * text=auto encoding=UTF-8 *.sh text eol=lf +*.sbn eol=lf # These are windows specific files which we may as well ensure are # always crlf on checkout diff --git a/.github/actions/dotnet/action.yml b/.github/actions/dotnet/action.yml new file mode 100644 index 0000000..c876ae3 --- /dev/null +++ b/.github/actions/dotnet/action.yml @@ -0,0 +1,35 @@ +name: ⚙ dotnet +description: Configures dotnet if the repo/org defines the DOTNET custom property + +runs: + using: composite + steps: + - name: 🔎 dotnet + id: dotnet + shell: bash + run: | + VERSIONS=$(gh api repos/${{ github.repository }}/properties/values | jq -r '.[] | select(.property_name == "DOTNET") | .value') + # Remove extra whitespace from VERSIONS + VERSIONS=$(echo "$VERSIONS" | tr -s ' ' | tr -d ' ') + # Convert comma-separated to newline-separated + NEWLINE_VERSIONS=$(echo "$VERSIONS" | tr ',' '\n') + # Validate versions + while IFS= read -r version; do + if ! [[ $version =~ ^[0-9]+(\.[0-9]+(\.[0-9]+)?)?(\.x)?$ ]]; then + echo "Error: Invalid version format: $version" + exit 1 + fi + done <<< "$NEWLINE_VERSIONS" + # Write multiline output to $GITHUB_OUTPUT + { + echo 'versions<> $GITHUB_OUTPUT + + - name: ⚙ dotnet + if: steps.dotnet.outputs.versions != '' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + ${{ steps.dotnet.outputs.versions }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c95eb73..11c5d7d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,6 +24,11 @@ updates: Extensions: patterns: - "Microsoft.Extensions*" + exclude-patterns: + - "Microsoft.Extensions.AI*" + ExtensionsAI: + patterns: + - "Microsoft.Extensions.AI*" Web: patterns: - "Microsoft.AspNetCore*" @@ -38,3 +43,6 @@ updates: ProtoBuf: patterns: - "protobuf-*" + Spectre: + patterns: + - "Spectre.Console*" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6653044..d6bd793 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,6 +28,7 @@ env: GH_TOKEN: ${{ secrets.GH_TOKEN }} MSBUILDTERMINALLOGGER: auto Configuration: ${{ github.event.inputs.configuration || 'Release' }} + SLEET_FEED_URL: ${{ vars.SLEET_FEED_URL }} defaults: run: @@ -65,12 +66,7 @@ jobs: fetch-depth: 0 - name: ⚙ dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 8.x - 9.x + uses: devlooped/actions-dotnet-env@v1 - name: 🙏 build run: dotnet build -m:1 -bl:build.binlog @@ -104,6 +100,9 @@ jobs: submodules: recursive fetch-depth: 0 + - name: ⚙ dotnet + uses: devlooped/actions-dotnet-env@v1 + - name: ✓ ensure format run: | dotnet format whitespace --verify-no-changes -v:diag --exclude ~/.nuget diff --git a/.github/workflows/dotnet-env.yml b/.github/workflows/dotnet-env.yml new file mode 100644 index 0000000..a76d0fd --- /dev/null +++ b/.github/workflows/dotnet-env.yml @@ -0,0 +1,44 @@ +name: dotnet-env +on: + workflow_dispatch: + push: + branches: + - main + paths: + - '**/*.*proj' + +jobs: + which-dotnet: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: 🤖 defaults + uses: devlooped/actions-bot@v1 + with: + name: ${{ secrets.BOT_NAME }} + email: ${{ secrets.BOT_EMAIL }} + gh_token: ${{ secrets.GH_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: 🤘 checkout + uses: actions/checkout@v4 + with: + token: ${{ env.GH_TOKEN }} + + - name: 🤌 dotnet + uses: devlooped/actions-which-dotnet@v1 + + - name: ✍ pull request + uses: peter-evans/create-pull-request@v7 + with: + base: main + branch: which-dotnet + delete-branch: true + labels: dependencies + title: "⚙ Update dotnet versions" + body: "Update dotnet versions" + commit-message: "Update dotnet versions" + token: ${{ env.GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dotnet-file-core.yml b/.github/workflows/dotnet-file-core.yml index 1230687..76aa6df 100644 --- a/.github/workflows/dotnet-file-core.yml +++ b/.github/workflows/dotnet-file-core.yml @@ -2,6 +2,13 @@ name: dotnet-file-core on: workflow_call: + secrets: + BOT_NAME: + required: false + BOT_EMAIL: + required: false + GH_TOKEN: + required: false env: DOTNET_NOLOGO: true diff --git a/.github/workflows/dotnet-file.yml b/.github/workflows/dotnet-file.yml index 083f24d..402f6ba 100644 --- a/.github/workflows/dotnet-file.yml +++ b/.github/workflows/dotnet-file.yml @@ -12,5 +12,10 @@ env: jobs: run: + permissions: + contents: write uses: devlooped/oss/.github/workflows/dotnet-file-core.yml@main - secrets: inherit \ No newline at end of file + secrets: + BOT_NAME: ${{ secrets.BOT_NAME }} + BOT_EMAIL: ${{ secrets.BOT_EMAIL }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/includes.yml b/.github/workflows/includes.yml index d787ccb..b591d40 100644 --- a/.github/workflows/includes.yml +++ b/.github/workflows/includes.yml @@ -5,8 +5,9 @@ on: branches: - 'main' paths: - - '**.md' + - '**.md' - '!changelog.md' + - 'osmfeula.txt' jobs: includes: @@ -31,14 +32,33 @@ jobs: - name: +Mᐁ includes uses: devlooped/actions-includes@v1 + - name: 📝 OSMF EULA + shell: pwsh + run: | + $file = "osmfeula.txt" + $props = "src/Directory.Build.props" + if (-not (test-path $file) -or -not (test-path $props)) { + exit 0 + } + + $product = dotnet msbuild $props -getproperty:Product + if (-not $product) { + write-error 'To use OSMF EULA, ensure the $(Product) property is set in Directory.props' + exit 1 + } + + ((get-content -raw $file) -replace '\$product\$',$product).trim() | set-content $file + - name: ✍ pull request uses: peter-evans/create-pull-request@v6 with: - add-paths: '**.md' + add-paths: | + **.md + osmfeula.txt base: main branch: markdown-includes delete-branch: true - labels: docs + labels: dependencies author: ${{ env.BOT_AUTHOR }} committer: ${{ env.BOT_AUTHOR }} commit-message: +Mᐁ includes diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8b065d7..03e57d9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,8 @@ env: VersionLabel: ${{ github.ref }} GH_TOKEN: ${{ secrets.GH_TOKEN }} MSBUILDTERMINALLOGGER: auto - + SLEET_FEED_URL: https://api.nuget.org/v3/index.json + jobs: publish: runs-on: ${{ vars.PUBLISH_AGENT || 'ubuntu-latest' }} @@ -27,12 +28,7 @@ jobs: fetch-depth: 0 - name: ⚙ dotnet - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 8.x - 9.x + uses: devlooped/actions-dotnet-env@v1 - name: 🙏 build run: dotnet build -m:1 -bl:build.binlog diff --git a/.gitignore b/.gitignore index 2ac54a7..0fe79fb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ BenchmarkDotNet.Artifacts .genaiscript .idea local.settings.json +.env *.suo *.sdf diff --git a/.netconfig b/.netconfig index 39eed16..d45e19e 100644 --- a/.netconfig +++ b/.netconfig @@ -27,23 +27,23 @@ skip [file ".editorconfig"] url = https://github.com/devlooped/oss/blob/main/.editorconfig - sha = e81ab754b366d52d92bd69b24bef1d5b1c610634 - etag = 7298c6450967975a8782b5c74f3071e1910fc59686e48f9c9d5cd7c68213cf59 + sha = a62c45934ac2952f2f5d54d8aea4a7ebc1babaff + etag = b5e919b472a52d4b522f86494f0f2c0ba74a6d9601454e20e4cbaf744317ff62 weak [file ".gitattributes"] url = https://github.com/devlooped/oss/blob/main/.gitattributes - sha = 5f92a68e302bae675b394ef343114139c075993e - etag = 338ba6d92c8d1774363396739c2be4257bfc58026f4b0fe92cb0ae4460e1eff7 + sha = 4a9aa321c4982b83c185cf8dffed181ff84667d5 + etag = 09cad18280ed04b67f7f87591e5481510df04d44c3403231b8af885664d8fd58 weak [file ".github/dependabot.yml"] url = https://github.com/devlooped/oss/blob/main/.github/dependabot.yml - sha = 49661dbf0720cde93eb5569be7523b5912351560 - etag = c147ea2f3431ca0338c315c4a45b56ee233c4d30f8d6ab698d0e1980a257fd6a + sha = e733294084fb3e75d517a2e961e87df8faae7dc6 + etag = 3bf8d9214a15c049ca5cfe80d212a8cbe4753b8a638a9804ef73d34c7def9618 weak [file ".github/workflows/build.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml - sha = 06e898ccba692566ebf845fa7c8833ac6c318c0a - etag = 0a4b3f0a875cd8c9434742b4046558aecf610d3fa3d490cfd2099266e95e9195 + sha = 56c2b8532c2f86235a0f5bd00ba6eba126f199cf + etag = bf99c19427f4372ecfe38ec56aa8c411058684fb717da5661f17ac00388b3602 weak [file ".github/workflows/changelog.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml @@ -52,18 +52,18 @@ weak [file ".github/workflows/dotnet-file.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file.yml - sha = 59aaf432369b5ea597831d4feec5a6ac4024c2e3 - etag = 1374e3f8c9b7af69c443605c03f7262300dcb7d783738d9eb9fe84268ed2d10c + sha = 8fa147d4799d73819040736c399d0b1db2c2d86c + etag = 1ca805a23656e99c03f9d478dba8ccef6e571f5de2ac0e9bb7e3c5216c99a694 weak [file ".github/workflows/publish.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml - sha = 06e898ccba692566ebf845fa7c8833ac6c318c0a - etag = 2f64f75ad01f735fd05290370fb8a826111ac8dd7e74ce04226bb627a54a62ba + sha = 56c2b8532c2f86235a0f5bd00ba6eba126f199cf + etag = 2ef43521627aa3a91dd55bdc2856ec0c6a93b42485d4fe9d6b181f9ee42c8e18 weak [file ".gitignore"] url = https://github.com/devlooped/oss/blob/main/.gitignore - sha = e0be248fff1d39133345283b8227372b36574b75 - etag = c449ec6f76803e1891357ca2b8b4fcb5b2e5deeff8311622fd92ca9fbf1e6575 + sha = 3776526342afb3f57da7e80f2095e5fdca3c31c9 + etag = 11767f73556aa4c6c8bcc153b77ee8e8114f99fa3b885b0a7d66d082f91e77b3 weak [file "Directory.Build.rsp"] url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp @@ -87,13 +87,13 @@ weak [file "src/Directory.Build.props"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props - sha = b76de49afb376aa48eb172963ed70663b59b31d3 - etag = c8b56f3860cc7ccb8773b7bd6189f5c7a6e3a2c27e9104c1ee201fbdc5af9873 + sha = 0ff8b7b79a82112678326d1dc5543ed890311366 + etag = 3ebde0a8630d526b80f15801179116e17a857ff880a4442e7db7b075efa4fd63 weak [file "src/Directory.Build.targets"] url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets - sha = a8b208093599263b7f2d1fe3854634c588ea5199 - etag = 19087699f05396205e6b050d999a43b175bd242f6e8fac86f6df936310178b03 + sha = 4339749ef4b8f66def75931df09ef99c149f8421 + etag = 8b4492765755c030c4c351e058a92f53ab493cab440c1c0ef431f6635c4dae0e weak [file "src/kzu.snk"] url = https://github.com/devlooped/oss/blob/main/src/kzu.snk @@ -107,8 +107,8 @@ weak [file ".github/workflows/includes.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml - sha = 85829f2510f335f4a411867f3dbaaa116c3ab3de - etag = 086f6b6316cc6ea7089c0dcc6980be519e6ed6e6201e65042ef41b82634ec0ee + sha = 2d1fb4ed52b63689f2b20b994512ebac28721243 + etag = 34ade86f020dea717c6a27ad7dcd0069c35be2832c58b0ba961278a1efe34089 weak [file ".github/workflows/combine-prs.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/combine-prs.yml @@ -144,9 +144,9 @@ url = https://github.com/devlooped/SponsorLink/tree/main/samples/dotnet/ [file "src/SponsorLink/Analyzer/Analyzer.csproj"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Analyzer/Analyzer.csproj - sha = e55425333883c4470d745f8fee70bdf204c292ee + sha = 8f0e6216360f3f8700b4845f3ec2310aabd996f3 - etag = 8aa140018fcfbd889c11da36c8c21b5cfb5730c07aa3317d734b118cfa60b416 + etag = 671a82f0f6770a990f9364ecf321eeea75bd6092f98c009039af02df172152df weak [file "src/SponsorLink/Analyzer/GraceApiAnalyzer.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Analyzer/GraceApiAnalyzer.cs @@ -192,9 +192,9 @@ weak [file "src/SponsorLink/Library/Library.csproj"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Library/Library.csproj - sha = f74ea7a8c7f81c5bceefb3ed7ef4249b1d8574a3 + sha = 0f551e3be564625ee4d078649c55363bf35954ba - etag = 592707adba548606ec50ced6e424be4cbfe34f18bf01555a19b29fa61efa416a + etag = 1ba2df85e2aae342f575b9ea08c38b2117f43c131b24d38082d1d4394716f3d0 weak [file "src/SponsorLink/Library/MyClass.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Library/MyClass.cs @@ -216,15 +216,15 @@ weak [file "src/SponsorLink/SponsorLink.Analyzer.Tests.targets"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink.Analyzer.Tests.targets - sha = df44ccc14cc11b5674c55aca9ba8596bdbcf8438 + sha = 8a4082211918b604ad95ef0f3da3cd414747c46a - etag = a3e9cbcc227dd56a7bed236eaded136f1b80f9f36a4fabce8be695ee844bf881 + etag = ac4e82c24d5a812eb7a1ad20d2d076b7aeedddd90c8196eaea0c227693a2ede6 weak [file "src/SponsorLink/SponsorLink.Analyzer.targets"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink.Analyzer.targets - sha = fb82cf346cea86140a51ae49b9bc730d72f7c7ac + sha = 8a4082211918b604ad95ef0f3da3cd414747c46a - etag = 284f794d03adabf10ac5e25ef87d257821a82eac112efe65d6fe23d675f9af7f + etag = b75dd01945453c3ccd9eb96f65959ff1607a2cf11226fac5014b01b7cb6314d7 weak [file "src/SponsorLink/SponsorLink/AnalyzerOptionsExtensions.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/AnalyzerOptionsExtensions.cs @@ -244,12 +244,6 @@ etag = a5d79dbc0ed9fac4fb1879fb3790b9ebab18e47c14c454554ce9f53f21487bb5 weak -[file "src/SponsorLink/SponsorLink/ManifestStatus.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/ManifestStatus.cs - sha = f47528874a6d9192b5546f84b455f5ccc474a707 - - etag = e46848f83c0436ba33a1c09a4060ad627a74db41bab66bb37ca40fce8a6532a7 - weak [file "src/SponsorLink/SponsorLink/Resources.es-AR.resx"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/Resources.es-AR.resx sha = 586398c3e650495f36601ecc8983a14ed745e058 @@ -258,27 +252,27 @@ weak [file "src/SponsorLink/SponsorLink/Resources.es.resx"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/Resources.es.resx - sha = 29921560c73bb91c2a21a21800daf0b250773598 + sha = 21d8dac3077c75cd07d7cc7f9e10f2620afce834 - etag = feb9dc86e4d9c0c4a294cd6e03c5b914943e8d206b88a125abd1b0f882ddb247 + etag = 89a7bb797aeacca43e043196a00eea91f282df4caf9bbe937749026a03f707ad weak [file "src/SponsorLink/SponsorLink/Resources.resx"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/Resources.resx - sha = 29921560c73bb91c2a21a21800daf0b250773598 + sha = 21d8dac3077c75cd07d7cc7f9e10f2620afce834 - etag = 7665a3be17cd224b1c413ade6a9c1c5a822dace1e7f9daae33a2e52d8bca15bb + etag = 8902652b8907de2fbccf73f3738d0fce503fc667a084171d6b88bf3373e559e7 weak [file "src/SponsorLink/SponsorLink/SponsorLink.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorLink.cs - sha = 3f72a9fd35274a659dd380a7d5b747d71b9732a1 + sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3 - etag = 616598e0ecb6d2ce97660aa6ac049e2a31a1c953669743b7b612b61d40c37706 + etag = 402e2beb11cf64c07be3d0fc3e89115fd09fc24133c08a8951bf0e784909c510 weak [file "src/SponsorLink/SponsorLink/SponsorLink.csproj"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorLink.csproj - sha = 0d22f1ee7d7afc93e11060887de0e1773884978e + sha = e8ec200934a3b3788c2e31d7022c717f5fd152fa - etag = dbf30ffb9baa63e45a4c821bc1433e4289b9af84855c2a306eaa116874a1c9f2 + etag = 1a58baf82b1813f68610272aa6161a18a70d5c619154734039a0d48fce6d735a weak [file "src/SponsorLink/SponsorLink/SponsorLinkAnalyzer.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorLinkAnalyzer.cs @@ -306,9 +300,9 @@ weak [file "src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/buildTransitive/Devlooped.Sponsors.targets - sha = d7090c1dbcb20c68b99486a6dc53d86b8d9b06bb + sha = 697e210b68c7d6f0ececca7673d13f4309df6cd7 - etag = e992b97517c9bcc6c9e927832bc13fac3036fa6d4ecaad893caf320b3c582aee + etag = e2cb4d1bbf4096f4b3fcfa0b20abccb33520442b656f19e01e5da928fd927da8 weak [file "src/SponsorLink/SponsorLink/sponsorable.md"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/sponsorable.md @@ -330,9 +324,9 @@ weak [file "src/SponsorLink/Tests/AnalyzerTests.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/AnalyzerTests.cs - sha = 29921560c73bb91c2a21a21800daf0b250773598 + sha = 697e210b68c7d6f0ececca7673d13f4309df6cd7 - etag = 219df696a47a58d9de377166c87fbb199c84c33d3b7a0f7ae349543df050a583 + etag = 44ef3022d2ebe1251896542b697baa9dcef9b9805b68845ccc9d0ff0181ba9d1 weak [file "src/SponsorLink/Tests/Attributes.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/Attributes.cs @@ -364,12 +358,6 @@ etag = 1875555adb7eab21acf1e730b6baeb8c095d9f6f9f07303a87ad9c16e0f6490d weak -[file "src/SponsorLink/Tests/SponsorLinkTests.cs"] - url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/SponsorLinkTests.cs - sha = f47528874a6d9192b5546f84b455f5ccc474a707 - - etag = 1fa41250bd984e8aa840a966d34ce0e94f2111d1422d7f50b864c38364fcf4a4 - weak [file "src/SponsorLink/Tests/SponsorableManifest.cs"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/SponsorableManifest.cs sha = f47528874a6d9192b5546f84b455f5ccc474a707 @@ -378,9 +366,9 @@ weak [file "src/SponsorLink/Tests/Tests.csproj"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/Tests.csproj - sha = 0d22f1ee7d7afc93e11060887de0e1773884978e + sha = e8ec200934a3b3788c2e31d7022c717f5fd152fa - etag = 5db4da024e4ecfb90be14feb4db952efa2109ee2ec84e715921291808d57b749 + etag = eb34fc9fe25b0169f069ff692379a19c59673727d8abb6f45816012661329df5 weak [file "src/SponsorLink/Tests/keys/kzu.key"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/keys/kzu.key @@ -432,12 +420,37 @@ weak [file "src/SponsorLink/readme.md"] url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/readme.md - sha = 7407f5b3461213ae764f53ee93651a34487e458c + sha = 697e210b68c7d6f0ececca7673d13f4309df6cd7 - etag = 50937c64732bb2b97ddc67cc7b7b2d091c51390c9f5f2b5fdcfe9f1becb5d838 + etag = 3f3bb07d204d2539d90a28145653c4b48c1f373d7186b39d2593338cebcd3299 weak [file ".github/workflows/dotnet-file-core.yml"] url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file-core.yml - sha = 875284ba5d565f529aba2f5d24ab8ed27c1d1c79 - etag = 8de1d974bf73b1945b5c8be684c3a0b7364114a0d795c9d68837aed9b3aff331 + sha = af171b7a87382ee665ba6fbaeb5f38a3551e1c23 + etag = 5ce370f52933ab2a4cd50f2b410e842fc5eab23088db2bf98b6c4d4ccdc9022b + weak +[file ".github/actions/dotnet/action.yml"] + url = https://github.com/devlooped/oss/blob/main/.github/actions/dotnet/action.yml + sha = f2b690ce307acb76c5b8d7faec1a5b971a93653e + etag = 27ea11baa2397b3ec9e643a935832da97719c4e44215cfd135c49cad4c29373f + weak +[file ".github/workflows/dotnet-env.yml"] + url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-env.yml + sha = 77e83f238196d2723640abef0c7b6f43994f9747 + etag = fcb9759a96966df40dcd24906fd328ddec05953b7e747a6bb8d0d1e4c3865274 + weak +[file "src/nuget.config"] + url = https://github.com/devlooped/oss/blob/main/src/nuget.config + sha = 032439dbf180fca0539a5bd3a019f18ab3484b76 + etag = da7c0104131bd474b52fc9bc9f9bda6470e24ae38d4fb9f5c4f719bc01370ab5 + weak +[file "src/SponsorLink/SponsorLink/SponsorManifest.cs"] + url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/SponsorLink/SponsorManifest.cs + sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3 + etag = 55ef89e8441156541c1c74a50675b7f56633b56493031f0ffa877460839e3536 + weak +[file "src/SponsorLink/Tests/SponsorManifestTests.cs"] + url = https://github.com/devlooped/SponsorLink/blob/main/samples/dotnet/Tests/SponsorManifestTests.cs + sha = a755e4be0f7cb73cfde208857e28f7cfeba2dcc3 + etag = 82ae1c417265f2e136544980b4f687a1cc2c1bfb24df93d354c259053550f4a3 weak diff --git a/readme.md b/readme.md index c49ca97..dd71961 100644 --- a/readme.md +++ b/readme.md @@ -130,43 +130,42 @@ The versioning scheme for packages is: # Sponsors -[![Clarius Org](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/clarius.png "Clarius Org")](https://github.com/clarius) -[![MFB Technologies, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MFB-Technologies-Inc.png "MFB Technologies, Inc.")](https://github.com/MFB-Technologies-Inc) -[![Torutek](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/torutek-gh.png "Torutek")](https://github.com/torutek-gh) -[![DRIVE.NET, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/drivenet.png "DRIVE.NET, Inc.")](https://github.com/drivenet) -[![Keith Pickford](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Keflon.png "Keith Pickford")](https://github.com/Keflon) -[![Thomas Bolon](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/tbolon.png "Thomas Bolon")](https://github.com/tbolon) -[![Kori Francis](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/kfrancis.png "Kori Francis")](https://github.com/kfrancis) -[![Toni Wenzel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/twenzel.png "Toni Wenzel")](https://github.com/twenzel) -[![Uno Platform](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/unoplatform.png "Uno Platform")](https://github.com/unoplatform) -[![Dan Siegel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/dansiegel.png "Dan Siegel")](https://github.com/dansiegel) -[![Reuben Swartz](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/rbnswartz.png "Reuben Swartz")](https://github.com/rbnswartz) -[![Jacob Foshee](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jfoshee.png "Jacob Foshee")](https://github.com/jfoshee) -[![](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Mrxx99.png "")](https://github.com/Mrxx99) -[![Eric Johnson](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/eajhnsn1.png "Eric Johnson")](https://github.com/eajhnsn1) -[![Ix Technologies B.V.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/IxTechnologies.png "Ix Technologies B.V.")](https://github.com/IxTechnologies) -[![David JENNI](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/davidjenni.png "David JENNI")](https://github.com/davidjenni) -[![Jonathan ](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Jonathan-Hickey.png "Jonathan ")](https://github.com/Jonathan-Hickey) -[![Charley Wu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/akunzai.png "Charley Wu")](https://github.com/akunzai) -[![Jakob Tikjøb Andersen](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jakobt.png "Jakob Tikjøb Andersen")](https://github.com/jakobt) -[![Tino Hager](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/tinohager.png "Tino Hager")](https://github.com/tinohager) -[![Ken Bonny](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/KenBonny.png "Ken Bonny")](https://github.com/KenBonny) -[![Simon Cropp](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/SimonCropp.png "Simon Cropp")](https://github.com/SimonCropp) -[![agileworks-eu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/agileworks-eu.png "agileworks-eu")](https://github.com/agileworks-eu) -[![sorahex](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/sorahex.png "sorahex")](https://github.com/sorahex) -[![Zheyu Shen](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/arsdragonfly.png "Zheyu Shen")](https://github.com/arsdragonfly) -[![Vezel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/vezel-dev.png "Vezel")](https://github.com/vezel-dev) -[![ChilliCream](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/ChilliCream.png "ChilliCream")](https://github.com/ChilliCream) -[![4OTC](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/4OTC.png "4OTC")](https://github.com/4OTC) -[![Vincent Limo](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/v-limo.png "Vincent Limo")](https://github.com/v-limo) -[![Jordan S. Jones](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jordansjones.png "Jordan S. Jones")](https://github.com/jordansjones) -[![domischell](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/DominicSchell.png "domischell")](https://github.com/DominicSchell) +[![Clarius Org](https://avatars.githubusercontent.com/u/71888636?v=4&s=39 "Clarius Org")](https://github.com/clarius) +[![MFB Technologies, Inc.](https://avatars.githubusercontent.com/u/87181630?v=4&s=39 "MFB Technologies, Inc.")](https://github.com/MFB-Technologies-Inc) +[![SandRock](https://avatars.githubusercontent.com/u/321868?u=99e50a714276c43ae820632f1da88cb71632ec97&v=4&s=39 "SandRock")](https://github.com/sandrock) +[![DRIVE.NET, Inc.](https://avatars.githubusercontent.com/u/15047123?v=4&s=39 "DRIVE.NET, Inc.")](https://github.com/drivenet) +[![Keith Pickford](https://avatars.githubusercontent.com/u/16598898?u=64416b80caf7092a885f60bb31612270bffc9598&v=4&s=39 "Keith Pickford")](https://github.com/Keflon) +[![Thomas Bolon](https://avatars.githubusercontent.com/u/127185?u=7f50babfc888675e37feb80851a4e9708f573386&v=4&s=39 "Thomas Bolon")](https://github.com/tbolon) +[![Kori Francis](https://avatars.githubusercontent.com/u/67574?u=3991fb983e1c399edf39aebc00a9f9cd425703bd&v=4&s=39 "Kori Francis")](https://github.com/kfrancis) +[![Uno Platform](https://avatars.githubusercontent.com/u/52228309?v=4&s=39 "Uno Platform")](https://github.com/unoplatform) +[![Reuben Swartz](https://avatars.githubusercontent.com/u/724704?u=2076fe336f9f6ad678009f1595cbea434b0c5a41&v=4&s=39 "Reuben Swartz")](https://github.com/rbnswartz) +[![Jacob Foshee](https://avatars.githubusercontent.com/u/480334?v=4&s=39 "Jacob Foshee")](https://github.com/jfoshee) +[![](https://avatars.githubusercontent.com/u/33566379?u=bf62e2b46435a267fa246a64537870fd2449410f&v=4&s=39 "")](https://github.com/Mrxx99) +[![Eric Johnson](https://avatars.githubusercontent.com/u/26369281?u=41b560c2bc493149b32d384b960e0948c78767ab&v=4&s=39 "Eric Johnson")](https://github.com/eajhnsn1) +[![David JENNI](https://avatars.githubusercontent.com/u/3200210?v=4&s=39 "David JENNI")](https://github.com/davidjenni) +[![Jonathan ](https://avatars.githubusercontent.com/u/5510103?u=98dcfbef3f32de629d30f1f418a095bf09e14891&v=4&s=39 "Jonathan ")](https://github.com/Jonathan-Hickey) +[![Charley Wu](https://avatars.githubusercontent.com/u/574719?u=ea7c743490c83e8e4b36af76000f2c71f75d636e&v=4&s=39 "Charley Wu")](https://github.com/akunzai) +[![Ken Bonny](https://avatars.githubusercontent.com/u/6417376?u=569af445b6f387917029ffb5129e9cf9f6f68421&v=4&s=39 "Ken Bonny")](https://github.com/KenBonny) +[![Simon Cropp](https://avatars.githubusercontent.com/u/122666?v=4&s=39 "Simon Cropp")](https://github.com/SimonCropp) +[![agileworks-eu](https://avatars.githubusercontent.com/u/5989304?v=4&s=39 "agileworks-eu")](https://github.com/agileworks-eu) +[![Zheyu Shen](https://avatars.githubusercontent.com/u/4067473?v=4&s=39 "Zheyu Shen")](https://github.com/arsdragonfly) +[![Vezel](https://avatars.githubusercontent.com/u/87844133?v=4&s=39 "Vezel")](https://github.com/vezel-dev) +[![ChilliCream](https://avatars.githubusercontent.com/u/16239022?v=4&s=39 "ChilliCream")](https://github.com/ChilliCream) +[![4OTC](https://avatars.githubusercontent.com/u/68428092?v=4&s=39 "4OTC")](https://github.com/4OTC) +[![Vincent Limo](https://avatars.githubusercontent.com/devlooped-user?s=39 "Vincent Limo")](https://github.com/v-limo) +[![domischell](https://avatars.githubusercontent.com/u/66068846?u=0a5c5e2e7d90f15ea657bc660f175605935c5bea&v=4&s=39 "domischell")](https://github.com/DominicSchell) +[![Justin Wendlandt](https://avatars.githubusercontent.com/u/1068431?u=f7715ed6a8bf926d96ec286f0f1c65f94bf86928&v=4&s=39 "Justin Wendlandt")](https://github.com/jwendl) +[![Adrian Alonso](https://avatars.githubusercontent.com/u/2027083?u=129cf516d99f5cb2fd0f4a0787a069f3446b7522&v=4&s=39 "Adrian Alonso")](https://github.com/adalon) +[![Michael Hagedorn](https://avatars.githubusercontent.com/u/61711586?u=8f653dfcb641e8c18cc5f78692ebc6bb3a0c92be&v=4&s=39 "Michael Hagedorn")](https://github.com/Eule02) +[![](https://avatars.githubusercontent.com/devlooped-user?s=39 "")](https://github.com/henkmartijn) +[![torutek](https://avatars.githubusercontent.com/u/33917059?v=4&s=39 "torutek")](https://github.com/torutek) +[![mccaffers](https://avatars.githubusercontent.com/u/16667079?u=739e110e62a75870c981640447efa5eb2cb3bc8f&v=4&s=39 "mccaffers")](https://github.com/mccaffers) +[![Christoph Hochstätter](https://avatars.githubusercontent.com/u/17645550?u=01bbdcb84d03cac26260f1c951e046d24a324591&v=4&s=39 "Christoph Hochstätter")](https://github.com/christoh) +[![ADS Fund](https://avatars.githubusercontent.com/u/202042116?v=4&s=39 "ADS Fund")](https://github.com/ADS-Fund) - -[![Sponsor this project](https://raw.githubusercontent.com/devlooped/sponsors/main/sponsor.png "Sponsor this project")](https://github.com/sponsors/devlooped) -  +[![Sponsor this project](https://avatars.githubusercontent.com/devlooped-sponsor?s=118 "Sponsor this project")](https://github.com/sponsors/devlooped) [Learn more about GitHub Sponsors](https://github.com/sponsors) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 381c383..29281ee 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,4 +1,4 @@ - + @@ -20,6 +20,7 @@ Daniel Cazzulino + Devlooped Copyright (C) Daniel Cazzulino and Contributors. All rights reserved. false MIT @@ -42,6 +43,10 @@ true + + false + + true @@ -126,11 +131,22 @@ <_VersionLabel>$(_VersionLabel.Replace('/merge', '')) <_VersionLabel>$(_VersionLabel.Replace('/', '-')) + + <_VersionLabel>$(_VersionLabel.Replace('_', '-')) $(_VersionLabel) $(_VersionLabel) + + + true + 42.42.0 + $(VersionSuffix).$(GITHUB_RUN_NUMBER) @@ -153,6 +169,18 @@ + + + 1.0.0 + $(VersionPrefix)-$(VersionSuffix) + $(VersionPrefix) + + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 6232750..083afa6 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -165,6 +165,9 @@ @(_GitSourceRoot) + + $([System.IO.Path]::GetFileNameWithoutExtension($(PrivateRepositoryUrl))) + $(ProductFromUrl) @@ -175,9 +178,9 @@ Condition="'$(SourceControlInformationFeatureSupported)' == 'true' And '$(IsPackable)' == 'true'"> - $(RepositoryUrl) + $(RepositoryUrl.Replace('.git', '')) $(Description) - $(RepositoryUrl)/blob/main/changelog.md + $(RepositoryUrl.Replace('.git', ''))/blob/main/changelog.md diff --git a/src/SponsorLink/Analyzer/Analyzer.csproj b/src/SponsorLink/Analyzer/Analyzer.csproj index ef41b20..d6063e3 100644 --- a/src/SponsorLink/Analyzer/Analyzer.csproj +++ b/src/SponsorLink/Analyzer/Analyzer.csproj @@ -14,13 +14,10 @@ - + - - - - - + + diff --git a/src/SponsorLink/Library/Library.csproj b/src/SponsorLink/Library/Library.csproj index 3ad022a..39424e3 100644 --- a/src/SponsorLink/Library/Library.csproj +++ b/src/SponsorLink/Library/Library.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/SponsorLink/SponsorLink.Analyzer.Tests.targets b/src/SponsorLink/SponsorLink.Analyzer.Tests.targets index 1ce67f6..4687e1e 100644 --- a/src/SponsorLink/SponsorLink.Analyzer.Tests.targets +++ b/src/SponsorLink/SponsorLink.Analyzer.Tests.targets @@ -7,12 +7,12 @@ - + - + diff --git a/src/SponsorLink/SponsorLink.Analyzer.targets b/src/SponsorLink/SponsorLink.Analyzer.targets index 9aae475..6a78464 100644 --- a/src/SponsorLink/SponsorLink.Analyzer.targets +++ b/src/SponsorLink/SponsorLink.Analyzer.targets @@ -84,15 +84,15 @@ - - + + - - + + 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 index b9ff562..17a7de0 100644 --- a/src/SponsorLink/SponsorLink/Resources.es.resx +++ b/src/SponsorLink/SponsorLink/Resources.es.resx @@ -169,6 +169,9 @@ Por favor considera apoyar el proyecto patrocinando en {0} y ejecutando posterio Eres un contribuidor al proyecto, eres lo máximo 💟! + + El uso de {0} sin warnings en el editor requiere un patrocinio activo. Ver mas en {1}. + 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}'. diff --git a/src/SponsorLink/SponsorLink/Resources.resx b/src/SponsorLink/SponsorLink/Resources.resx index 7b1dfa9..93b75bc 100644 --- a/src/SponsorLink/SponsorLink/Resources.resx +++ b/src/SponsorLink/SponsorLink/Resources.resx @@ -171,7 +171,7 @@ Please consider supporting the project by sponsoring at {0} and running 'sponsor You are a contributor to the project, you rock 💟! - Editor usage of {0} requires an active sponsorship. Learn more at {1}. + Editor usage of {0} without warnings requires an active sponsorship. Learn more at {1}. 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! diff --git a/src/SponsorLink/SponsorLink/SponsorLink.cs b/src/SponsorLink/SponsorLink/SponsorLink.cs index eec50c8..2c1e6db 100644 --- a/src/SponsorLink/SponsorLink/SponsorLink.cs +++ b/src/SponsorLink/SponsorLink/SponsorLink.cs @@ -165,82 +165,4 @@ public static bool TryRead([NotNullWhen(true)] out ClaimsPrincipal? principal, I 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 index cf62d1b..25a474c 100644 --- a/src/SponsorLink/SponsorLink/SponsorLink.csproj +++ b/src/SponsorLink/SponsorLink/SponsorLink.csproj @@ -24,11 +24,11 @@ - - + + - - + + diff --git a/src/SponsorLink/SponsorLink/SponsorManifest.cs b/src/SponsorLink/SponsorLink/SponsorManifest.cs new file mode 100644 index 0000000..b4aa9d7 --- /dev/null +++ b/src/SponsorLink/SponsorLink/SponsorManifest.cs @@ -0,0 +1,160 @@ +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security.Claims; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; + +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, +} + +/// +/// Represents the sponsorship status of a user. +/// +/// The status. +/// The principal potentially containing roles validated from the manifest. +/// The security token from the validated manifest. +public record SponsorManifest(ManifestStatus Status, ClaimsPrincipal Principal, SecurityToken? SecurityToken) +{ + /// + /// Whether the manifest is . + /// + public bool IsValid => Status == ManifestStatus.Valid; +} + +static partial class SponsorLink +{ + /// + /// Reads the local manifest (if present) for the specified sponsorable account and validates it + /// against the given JWK key. + /// + /// The sponsorable account to read. + /// The public key to validate the signature on the manifest JWT if found. + /// Whether to validate the manifest expiration. If , + /// an expired manifest will be reported as . The expiration date + /// can be checked in that case via the . + /// A manifest that represents the user status. + public static SponsorManifest GetManifest(string sponsorable, string jwk, bool validateExpiration = true) + { + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + ".sponsorlink", "github", sponsorable + ".jwt"); + + if (!File.Exists(path)) + return new SponsorManifest(ManifestStatus.Unknown, new ClaimsPrincipal(), null); + + return ParseManifest(File.ReadAllText(path), jwk, validateExpiration); + } + + internal static SponsorManifest ParseManifest(string jwt, string jwk, bool validateExpiration) + { + var status = Validate(jwt, jwk, out var token, out var identity, validateExpiration); + + if (status == ManifestStatus.Unknown || identity == null) + return new SponsorManifest(status, new ClaimsPrincipal(), token); + + return new SponsorManifest(status, new JwtRolesPrincipal(identity), token); + } + + /// + /// 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.IsValid || 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/buildTransitive/Devlooped.Sponsors.targets b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets index 0bc5a45..eb4c61b 100644 --- a/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets +++ b/src/SponsorLink/SponsorLink/buildTransitive/Devlooped.Sponsors.targets @@ -61,13 +61,13 @@ - %(FundingPackageId.Identity) + <_FundingPackageId>%(FundingPackageId.Identity) - + diff --git a/src/SponsorLink/Tests/AnalyzerTests.cs b/src/SponsorLink/Tests/AnalyzerTests.cs index 4424b14..6192541 100644 --- a/src/SponsorLink/Tests/AnalyzerTests.cs +++ b/src/SponsorLink/Tests/AnalyzerTests.cs @@ -213,7 +213,7 @@ public async Task WhenMultipleAnalyzers_ThenReportsOnce() .Where(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var _)); Assert.NotEmpty(diagnostics); - Assert.Single(diagnostics.Where(x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value))); + Assert.Single(diagnostics, x => x.Properties.TryGetValue(nameof(SponsorStatus), out var value)); } [Fact] diff --git a/src/SponsorLink/Tests/SponsorLinkTests.cs b/src/SponsorLink/Tests/SponsorManifestTests.cs similarity index 63% rename from src/SponsorLink/Tests/SponsorLinkTests.cs rename to src/SponsorLink/Tests/SponsorManifestTests.cs index 7625e2c..7895a81 100644 --- a/src/SponsorLink/Tests/SponsorLinkTests.cs +++ b/src/SponsorLink/Tests/SponsorManifestTests.cs @@ -8,7 +8,7 @@ namespace Devlooped.Tests; -public class SponsorLinkTests +public class SponsorManifestTests { // We need to convert to jwk string since the analyzer project has merged the JWT assembly and types. public static string ToJwk(SecurityKey key) @@ -19,64 +19,67 @@ public static string ToJwk(SecurityKey key) [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); + var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwt = sponsorable.ToJwt(); + var jwk = ToJwk(sponsorable.SecurityKey); // NOTE: sponsorable manifest doesn't have expiration date. - var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false); + var manifest = SponsorLink.ParseManifest(jwt, jwk, false); - Assert.Equal(ManifestStatus.Valid, status); + Assert.True(manifest.IsValid); + Assert.Equal(ManifestStatus.Valid, manifest.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 sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwt = sponsorable.ToJwt(); var jwk = ToJwk(new RsaSecurityKey(RSA.Create())); - var status = SponsorLink.Validate(jwt, jwk, out var token, out var principal, false); + var manifest = SponsorLink.ParseManifest(jwt, jwk, false); - Assert.Equal(ManifestStatus.Invalid, status); + Assert.Equal(ManifestStatus.Invalid, manifest.Status); // We should still be a able to read the data, knowing it may have been tampered with. - Assert.NotNull(principal); - Assert.NotNull(token); + Assert.NotNull(manifest.Principal); + Assert.NotNull(manifest.SecurityToken); } [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); + var sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwk = ToJwk(sponsorable.SecurityKey); + var sponsor = sponsorable.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); + var manifest = SponsorLink.ParseManifest(sponsor, jwk, true); - Assert.Equal(ManifestStatus.Expired, status); + Assert.Equal(ManifestStatus.Expired, manifest.Status); // We should still be a able to read the data, even if expired (but not tampered with). - Assert.NotNull(principal); - Assert.NotNull(token); + Assert.NotNull(manifest.Principal); + Assert.NotNull(manifest.SecurityToken); } [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 sponsorable = SponsorableManifest.Create(new Uri("https://foo.com"), [new Uri("https://github.com/sponsors/bar")], "ASDF1234"); + var jwk = ToJwk(sponsorable.SecurityKey); - var status = SponsorLink.Validate("asdfasdf", jwk, out var token, out var principal, false); + var manifest = SponsorLink.ParseManifest("asdfasdf", jwk, false); - Assert.Equal(ManifestStatus.Unknown, status); + Assert.Equal(ManifestStatus.Unknown, manifest.Status); // Nothing could be read at all. - Assert.Null(principal); - Assert.Null(token); + Assert.False(manifest.IsValid); + Assert.NotNull(manifest.Principal); + Assert.Null(manifest.Principal.Identity); + Assert.Null(manifest.SecurityToken); } [Fact] @@ -111,7 +114,7 @@ public void ValidateCachedManifest() var jwt = File.ReadAllText(path); - var status = SponsorLink.Validate(jwt, + var manifest = SponsorLink.ParseManifest(jwt, """ { "e": "AQAB", @@ -119,8 +122,8 @@ public void ValidateCachedManifest() "n": "5inhv8QymaDBOihNi1eY-6-hcIB5qSONFZxbxxXAyOtxAdjFCPM-94gIZqM9CDrX3pyg1lTJfml_a_FZSU9dB1ii5mSX_mNHBFXn1_l_gi1ErdbkIF5YbW6oxWFxf3G5mwVXwnPfxHTyQdmWQ3YJR-A3EB4kaFwLqA6Ha5lb2ObGpMTQJNakD4oTAGDhqHMGhu6PupGq5ie4qZcQ7N8ANw8xH7nicTkbqEhQABHWOTmLBWq5f5F6RYGF8P7cl0IWl_w4YcIZkGm2vX2fi26F9F60cU1v13GZEVDTXpJ9kzvYeM9sYk6fWaoyY2jhE51qbv0B0u6hScZiLREtm3n7ClJbIGXhkUppFS2JlNaX3rgQ6t-4LK8gUTyLt3zDs2H8OZyCwlCpfmGmdsUMkm1xX6t2r-95U3zywynxoWZfjBCJf41leM9OMKYwNWZ6LQMyo83HWw1PBIrX4ZLClFwqBcSYsXDyT8_ZLd1cdYmPfmtllIXxZhLClwT5qbCWv73V" } """ - , out var token, out var principal, false); + , false); - Assert.Equal(ManifestStatus.Valid, status); + Assert.Equal(ManifestStatus.Valid, manifest.Status); } } diff --git a/src/SponsorLink/Tests/Tests.csproj b/src/SponsorLink/Tests/Tests.csproj index a56aa30..df8bc07 100644 --- a/src/SponsorLink/Tests/Tests.csproj +++ b/src/SponsorLink/Tests/Tests.csproj @@ -10,14 +10,14 @@ - - - - - + + + + + - + diff --git a/src/SponsorLink/readme.md b/src/SponsorLink/readme.md index ca6d5e3..a502452 100644 --- a/src/SponsorLink/readme.md +++ b/src/SponsorLink/readme.md @@ -35,4 +35,19 @@ Including the analyzer and targets in a project involves two steps. ``` +3. Set the package id(s) that will be checked for funding in the analyzer, such as: + +```xml + + SponsorableLib;SponsorableLib.Core + +``` + + The default analyzer will report a diagnostic for sponsorship status only + if the project being compiled as a direct package reference to one of the + specified package ids. + + This property defaults to `$(PackageId)` if present. Otherwise, it defaults + to `$(FundingProduct)`, which in turn defaults to `$(Product)` if not provided. + As long as NuGetizer is used, the right packaging will be done automatically. \ No newline at end of file diff --git a/src/nuget.config b/src/nuget.config new file mode 100644 index 0000000..1c18c96 --- /dev/null +++ b/src/nuget.config @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + +