diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..568b2cd
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @AuthPlane/authplane-eng
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
new file mode 100644
index 0000000..1a15cbf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -0,0 +1,68 @@
+---
+name: Bug Report
+about: Report a bug in the Authplane .NET SDK or an adapter
+title: "[Bug] "
+labels: bug
+assignees: ""
+---
+
+## Description
+
+A clear description of the bug.
+
+## Affected Package
+
+- [ ] `Authplane.Sdk` (core)
+- [ ] `Authplane.Mcp`
+
+## Steps to Reproduce
+
+1. Add the package reference to your `.csproj` ...
+2. Configure / call ...
+3. Observe ...
+
+Minimal reproducible code snippet:
+
+```csharp
+// paste here
+```
+
+## Expected Behavior
+
+What you expected to happen.
+
+## Actual Behavior
+
+What actually happened. Include stack trace, HTTP status codes, and relevant log output.
+
+## Environment
+
+- **Package version:** (e.g., `Authplane.Sdk 1.2.3`)
+- **Target framework:** (e.g., `net8.0`, `net10.0`)
+- **.NET SDK version:** (`dotnet --info`)
+- **OS:** (e.g., Ubuntu 22.04, macOS 14, Windows 11)
+- **Framework (if adapter):** (e.g., ASP.NET Core 10, ModelContextProtocol.AspNetCore 1.0.0)
+- **Authplane `authserver` version / issuer:** (if relevant)
+
+## Configuration
+
+Relevant SDK configuration (redact secrets):
+
+```csharp
+// paste here
+```
+
+## Logs
+
+
+Relevant logs
+
+```
+(paste relevant logs here — redact any sensitive data, especially tokens)
+```
+
+
+
+## Additional Context
+
+Any other relevant information.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100644
index 0000000..6171b32
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,37 @@
+---
+name: Feature Request
+about: Suggest a new feature for the Authplane .NET SDK or an adapter
+title: "[Feature] "
+labels: enhancement
+assignees: ""
+---
+
+## Affected Package
+
+- [ ] `Authplane.Sdk` (core)
+- [ ] `Authplane.Mcp`
+- [ ] New package
+
+## Problem
+
+Describe the problem this feature would solve. What are you trying to do that you can't do today?
+
+## Proposed Solution
+
+Describe your proposed solution. How should this work? Include a rough API sketch if possible:
+
+```csharp
+// example usage
+```
+
+## Alternatives Considered
+
+What alternatives have you considered? Why is this approach better?
+
+## Use Case
+
+Describe the use case. Who benefits from this feature? Is this blocking integration with a specific framework or service?
+
+## Additional Context
+
+Any other relevant information, links to RFCs or specs, or examples from other projects.
diff --git a/.github/ISSUE_TEMPLATE/mcp-compatibility.md b/.github/ISSUE_TEMPLATE/mcp-compatibility.md
new file mode 100644
index 0000000..7f945a6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/mcp-compatibility.md
@@ -0,0 +1,76 @@
+---
+name: MCP Compatibility Report
+about: Report compatibility of an MCP client or server with the Authplane .NET adapter
+title: "[Compat] "
+labels: compatibility, mcp
+assignees: ""
+---
+
+## Adapter
+
+- [ ] `Authplane.Mcp` (MCP .NET adapter)
+
+## MCP Library Version
+
+- **Library:** (e.g., `ModelContextProtocol.AspNetCore`)
+- **Version:**
+- **Transport:** (e.g., streamable-http, stdio)
+
+## MCP Client (if reporting a client-side issue)
+
+- **Client:** (e.g., Claude Code, MCP Inspector, Cursor)
+- **Version:**
+- **Platform:** (macOS / Linux / Windows)
+
+## Authplane SDK Version
+
+- `Authplane.Sdk`:
+- `Authplane.Mcp`:
+- `authserver` (issuer):
+- **Target framework / .NET SDK version:** (`dotnet --info`)
+
+## Description
+
+Brief summary of the compatibility observation.
+
+## Compatibility Scenarios
+
+Check each that was tested. Mark pass / fail / skip.
+
+- [ ] **JWT validation** — protected tool accepts valid bearer token
+- [ ] **Scope enforcement** — tool-specific scope required and checked
+- [ ] **DPoP-bound tokens** — proof-of-possession verified end-to-end (if applicable)
+- [ ] **Token refresh** — client refreshes without losing session
+- [ ] **Metadata discovery** — adapter surfaces `WWW-Authenticate` / protected-resource metadata correctly
+- [ ] **Error handling** — expired, revoked, malformed tokens produce the expected error shape
+
+## Reproduction Steps
+
+1. Install adapter ...
+2. Configure MCP server with Authplane middleware ...
+3. Connect client ...
+4. Observe results
+
+## Logs
+
+
+Server logs (adapter)
+
+```
+(paste relevant logs here)
+```
+
+
+
+
+Client logs
+
+```
+(paste relevant logs here)
+```
+
+
+
+## Additional Context
+
+Screenshots, network traces, spec references, or upstream issues.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..9c06d7e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,48 @@
+version: 2
+
+updates:
+ # NuGet — the solution at the root resolves both packable projects and the
+ # test projects together, so a single ecosystem block covers the tree.
+ - package-ecosystem: nuget
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ labels:
+ - dependencies
+ - dotnet
+ commit-message:
+ prefix: "deps"
+ open-pull-requests-limit: 10
+ groups:
+ # Only minor + patch updates are bundled. Majors fall out of the
+ # group and each gets its own PR so breaking changes are reviewed
+ # one at a time.
+ nuget-minor-patch:
+ patterns:
+ - "*"
+ update-types:
+ - "minor"
+ - "patch"
+
+ # GitHub Actions
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ labels:
+ - dependencies
+ - ci
+ commit-message:
+ prefix: "ci"
+ open-pull-requests-limit: 5
+ groups:
+ # Same approach as the nuget group: bundle minor + patch, let
+ # majors open as individual PRs.
+ actions-minor-patch:
+ patterns:
+ - "*"
+ update-types:
+ - "minor"
+ - "patch"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..ea7c587
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,33 @@
+## Summary
+
+Brief description of what this PR does and why.
+
+## Linked Issue
+
+
+
+## Changes
+
+-
+
+## Affected Projects
+
+- [ ] `Authplane.Sdk` (core)
+- [ ] `Authplane.Mcp`
+- [ ] None (infra / docs / CI only)
+
+## Test Plan
+
+How was this tested? Include relevant test names or manual verification steps.
+
+## Checklist
+
+- [ ] `dotnet build Authplane.slnx --configuration Release` passes
+- [ ] `dotnet format Authplane.slnx --verify-no-changes` is clean
+- [ ] `dotnet test` passes for affected projects
+- [ ] Coverage thresholds met (80 line / 70 branch)
+- [ ] Tests added for new functionality
+- [ ] Documentation updated (if applicable)
+- [ ] `CHANGELOG.md` entry added under `[Unreleased]` (if user-facing)
+- [ ] New workflow actions are SHA-pinned (`pinact run` after changes)
+- [ ] No token values, secrets, or key material in logs or test fixtures
diff --git a/.github/workflows/backport-fixes.yml b/.github/workflows/backport-fixes.yml
new file mode 100644
index 0000000..55cf10c
--- /dev/null
+++ b/.github/workflows/backport-fixes.yml
@@ -0,0 +1,131 @@
+name: Backport fixes to default branch
+
+# Thin wrapper around scripts/backport-fixes.sh for maintainers who
+# prefer dispatching from the Actions UI. Behavior:
+#
+# - Happy path (no conflicts): runs the script, pushes the backport
+# branch, opens a PR against the target.
+# - Any conflict: cherry-pick fails, the workflow fails loudly. Re-run
+# the script locally to finish:
+# scripts/backport-fixes.sh --from --to
+# and resolve conflicts via git's native cherry-pick state machine.
+#
+# fromBranch can be a branch (release/v*, hotfix/v*) or a tag (vX.Y.Z).
+# After the release workflow runs, the release branch is deleted — use
+# the tag as the fromBranch instead.
+
+on:
+ workflow_dispatch:
+ inputs:
+ fromBranch:
+ description: 'Source ref on origin (branch or tag, e.g. release/v0.6.0, hotfix/v0.5.1, or v0.6.0). Do not include origin/.'
+ required: true
+ type: string
+ toBranch:
+ description: 'Target branch on origin. Leave empty to use the repository default branch.'
+ required: false
+ type: string
+ default: ''
+
+concurrency:
+ group: backport-${{ inputs.fromBranch }}-to-${{ inputs.toBranch }}
+ cancel-in-progress: false
+
+# Least-privilege default; the job re-grants the write scopes it needs.
+permissions:
+ contents: read
+
+jobs:
+ backport:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Resolve target branch
+ id: target
+ run: |
+ to="${{ inputs.toBranch }}"
+ if [[ -z "$to" ]]; then
+ to="${{ github.event.repository.default_branch }}"
+ fi
+ echo "to=$to" >> "$GITHUB_OUTPUT"
+
+ - name: Check out repo with full history
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+
+ - name: Configure git author
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ # The script exits non-zero on any cherry-pick conflict. That failure
+ # propagates here and halts the workflow before push/PR.
+ - name: Run backport script
+ id: run
+ run: |
+ ./scripts/backport-fixes.sh \
+ --from "${{ inputs.fromBranch }}" \
+ --to "${{ steps.target.outputs.to }}"
+ echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> "$GITHUB_OUTPUT"
+
+ - name: Push backport branch
+ if: startsWith(steps.run.outputs.branch, 'backport/')
+ run: |
+ git push origin "${{ steps.run.outputs.branch }}"
+
+ - name: Open pull request
+ if: startsWith(steps.run.outputs.branch, 'backport/')
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ from="${{ inputs.fromBranch }}"
+ to="${{ steps.target.outputs.to }}"
+ branch="${{ steps.run.outputs.branch }}"
+ title="chore: backport from $from to $to"
+ body=$(cat <> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 80c4190..c0bd70d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,7 +4,7 @@ on:
pull_request:
push:
branches:
- - develop
+ - main
jobs:
quality:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..7fe8634
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,73 @@
+name: CodeQL
+
+# Static application security testing (SAST) for the C# sources across both
+# packable projects (Authplane.Sdk, Authplane.Mcp). Runs CodeQL's
+# security-extended query suite on pull requests, pushes to main, and a weekly
+# schedule, uploading results to the GitHub Security tab.
+#
+# Closes the OpenSSF Scorecard "SAST" gap: CodeQL statically analyzes the
+# first-party source, complementing the dependency-vulnerability scanning
+# (the security workflow) that runs separately.
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ schedule:
+ # Wednesdays 06:00 UTC
+ - cron: "0 6 * * 3"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ name: Analyze (csharp)
+ # Code scanning (SARIF upload) requires GitHub Code Security, which is
+ # only available on public repos. Gate on visibility so the job only
+ # runs when the repo is public.
+ if: ${{ github.event.repository.visibility == 'public' }}
+ runs-on: ubuntu-latest
+ permissions:
+ # Required for CodeQL to upload its SARIF results to code scanning.
+ security-events: write
+ contents: read
+ actions: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
+ with:
+ # 8.0.x alongside 10.0.x: the projects multi-target `net8.0;net10.0`,
+ # so the extractor needs the 8.0 reference packs to compile that leg.
+ dotnet-version: |
+ 8.0.x
+ 10.0.x
+
+ # build-mode: manual — C# is compiled, so the extractor must observe a
+ # real build. Autobuild guesses at the solution layout; we drive the
+ # compile explicitly below instead.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+ with:
+ languages: csharp
+ build-mode: manual
+ queries: security-extended
+
+ - name: Build solution
+ # Compile every project so the extractor sees all types. Tests are
+ # excluded from the analysis target on purpose: they would also pull
+ # the external conformance catalog, which this workflow does not clone.
+ run: dotnet build Authplane.slnx --configuration Release
+
+ - name: Perform CodeQL analysis
+ uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+ with:
+ category: "/language:csharp"
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 81260b0..e6113ff 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -12,7 +12,7 @@ on:
- cron: "0 6 * * 2"
push:
branches:
- - develop
+ - main
workflow_dispatch:
permissions: read-all
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index 9df42c0..7100d79 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -15,7 +15,7 @@ on:
- ".github/workflows/security.yml"
push:
branches:
- - develop
+ - main
paths:
- "**/*.csproj"
- "**/Directory.Build.props"
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 3aad59a..20348bc 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -30,7 +30,7 @@ jobs:
This issue has been automatically marked as stale because it has
not had activity in 60 days. It will be closed in 30 days if no
further activity occurs. Add a comment or the `pinned` label to
- keep it open. See [CONTRIBUTING.md](../blob/develop/CONTRIBUTING.md).
+ keep it open. See [CONTRIBUTING.md](../blob/main/CONTRIBUTING.md).
stale-pr-message: |
This PR has been automatically marked as stale because it has
not had activity in 30 days. It will be closed in 30 days if no
diff --git a/.github/workflows/workflows-lint.yml b/.github/workflows/workflows-lint.yml
new file mode 100644
index 0000000..dfadf36
--- /dev/null
+++ b/.github/workflows/workflows-lint.yml
@@ -0,0 +1,64 @@
+name: Lint workflows
+
+# Catches workflow YAML / shell-in-`run:` regressions at PR time so a
+# typo can't reach a release tag and surface only when a publish run
+# fails. Scoped to changes under `.github/workflows/**` to keep CI
+# overhead off unrelated PRs.
+
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/**"
+ push:
+ branches:
+ - main
+ paths:
+ - ".github/workflows/**"
+
+permissions:
+ contents: read
+
+jobs:
+ actionlint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ # Pulls the matching actionlint binary release from GitHub Releases
+ # via the upstream download script. The script is fetched by commit
+ # SHA (not a mutable tag) and sha256-verified before it runs — this
+ # closes Scorecard's "downloadThenRun not pinned by hash" gap. The
+ # script then checksum-verifies the actionlint binary it pulls from
+ # the matching release.
+ #
+ # To bump: change ACTIONLINT_VERSION, set ACTIONLINT_SCRIPT_SHA to the
+ # commit the new tag points at (`gh api repos/rhysd/actionlint/commits/vX.Y.Z -q .sha`),
+ # and update ACTIONLINT_SCRIPT_SHA256 to that file's sha256.
+ #
+ # Install dir is passed explicitly as the script's second positional
+ # arg so the workflow doesn't couple to the script's internal default
+ # of $PWD (which happens to be $GITHUB_WORKSPACE after checkout —
+ # a coincidence, not a contract).
+ - name: Install actionlint
+ env:
+ ACTIONLINT_VERSION: "1.7.7"
+ ACTIONLINT_SCRIPT_SHA: "03d0035246f3e81f36aed592ffb4bebf33a03106"
+ ACTIONLINT_SCRIPT_SHA256: "221d1d16c03e4e4fcd867de34104e8d479bdce20ccdfa553b9a5c0dc29bf6af2"
+ ACTIONLINT_INSTALL_DIR: ${{ runner.temp }}/actionlint
+ run: |
+ mkdir -p "${ACTIONLINT_INSTALL_DIR}"
+ script="${ACTIONLINT_INSTALL_DIR}/download-actionlint.bash"
+ curl -fsSL -o "${script}" \
+ "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_SCRIPT_SHA}/scripts/download-actionlint.bash"
+ echo "${ACTIONLINT_SCRIPT_SHA256} ${script}" | sha256sum -c -
+ bash "${script}" "${ACTIONLINT_VERSION}" "${ACTIONLINT_INSTALL_DIR}"
+ echo "${ACTIONLINT_INSTALL_DIR}" >> "${GITHUB_PATH}"
+ "${ACTIONLINT_INSTALL_DIR}/actionlint" -version
+
+ # `-shellcheck=shellcheck` makes the shellcheck dependency explicit
+ # rather than relying on actionlint's implicit lookup against the
+ # runner image's $PATH; if the Ubuntu image ever drops shellcheck the
+ # job fails loudly instead of silently degrading.
+ - name: Run actionlint
+ run: actionlint -color -shellcheck=shellcheck
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4dd3695..5f18b16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -100,7 +100,7 @@ See `demo/README.md` for the demo prerequisites.
## Pull Request Guidelines
-- **Branching:** branch off `develop`. Use `ISSUE-ID-short-description` style; the issue tracker auto-detects the prefix.
+- **Branching:** branch off `main`. Use `ISSUE-ID-short-description` style; the issue tracker auto-detects the prefix.
- **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) format. Reference the tracked issue in the commit body or footer (e.g. `(ISSUE-ID)`).
- **PR template:** describe the change, link the issue, list the test plan.
- **CI:** every PR must pass `dotnet build`, `dotnet test`, `dotnet format --verify-no-changes`, and the conformance suite. Coverage thresholds (80 line / 70 branch) are enforced.
@@ -109,7 +109,7 @@ See `demo/README.md` for the demo prerequisites.
## CI / Workflow Expectations
- GitHub Actions are SHA-pinned via [pinact](https://github.com/suzuki-shunsuke/pinact); the manifest is `.pinact.yaml`. To upgrade an action: bump the tag, then `pinact run` to refresh the SHA.
-- The workflow file is `.github/workflows/ci.yml`. Changes to it require a PR (no direct pushes to `develop`).
+- The workflow file is `.github/workflows/ci.yml`. Changes to it require a PR (no direct pushes to `main`).
## Code of Conduct
diff --git a/README.md b/README.md
index f3302a7..11125ef 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Authplane .NET SDK
-[](https://github.com/AuthPlane/cs-sdk/actions/workflows/ci.yml)
+[](https://github.com/AuthPlane/cs-sdk/actions/workflows/ci.yml)
[](LICENSE)
OAuth 2.1 JWT validation and token operations for .NET resource servers, with a first-class adapter for [Model Context Protocol](https://modelcontextprotocol.io/) servers.
diff --git a/RELEASE_SETUP.md b/RELEASE_SETUP.md
index 493ceef..9fff315 100644
--- a/RELEASE_SETUP.md
+++ b/RELEASE_SETUP.md
@@ -70,17 +70,17 @@ For patches to an older minor line (not the current default-branch line):
- the line is strictly older than the default branch's current line.
2. **Land the fix on `hotfix/vX.Y.Z`** — cherry-pick or commit directly. Add a `## [X.Y.Z]` CHANGELOG entry.
3. **Dispatch the release** — same as above, from the `hotfix/v*` branch.
-4. **Backport to the default branch** — after publication, port the fix back so the default branch line carries it too. *The dedicated Backport workflow has not been added yet — use the manual cherry-pick fallback below until it lands. Tracked separately.*
+4. **Backport to the default branch** — after publication, port the fix back so the default branch line carries it too. Dispatch **Backport fixes to default branch** from the Actions tab with `fromBranch=vX.Y.Z`; the release branch is deleted by `release.yml`, so use the tag. The workflow wraps `scripts/backport-fixes.sh`, pushes the backport branch and opens the PR. On a cherry-pick conflict it fails loudly — finish locally with the same script.
Manual cherry-pick fallback:
```bash
git fetch --tags origin
- git checkout -b backport/ origin/develop
+ git checkout -b backport/ origin/main
git cherry-pick vX.Y.Z~..vX.Y.Z # adjust the range to the hotfix commits
# resolve any conflicts, then:
git push origin backport/
- gh pr create --base develop --title 'chore(backport): port vX.Y.Z fixes to develop' \
+ gh pr create --base main --title 'chore(backport): port vX.Y.Z fixes to main' \
--body 'Cherry-picked from tag vX.Y.Z. See RELEASE_SETUP.md §6.'
```
@@ -113,6 +113,6 @@ NuGet does not support atomic multi-package uploads. If `publish-nuget.yml` publ
No `--target` — the tag already points at the correct commit on the (now deleted) source branch.
-4. If any commits on the source branch need to reach the default branch, port them back via the manual cherry-pick fallback documented in §6.4 (the dedicated **Backport fixes** workflow has not been added yet).
+4. If any commits on the source branch need to reach the default branch, port them back with the **Backport fixes** workflow (§6.4), or the manual cherry-pick fallback documented there.
The git tag is already live, so re-running `release.yml` is not an option (the tag-exists pre-flight refuses).
diff --git a/scripts/backport-fixes.sh b/scripts/backport-fixes.sh
new file mode 100755
index 0000000..5e82a47
--- /dev/null
+++ b/scripts/backport-fixes.sh
@@ -0,0 +1,227 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Cherry-pick commits from a release/hotfix branch (or a release tag) to a
+# local backport branch off the target. Does NOT push, create PRs, or
+# touch remotes beyond `git fetch`.
+#
+# Conflicts use git's native cherry-pick state machine — resolve, then
+# `git cherry-pick --continue` (or --skip / --abort). Re-running this
+# script is not needed after a conflict; git's sequencer handles it.
+#
+# Uses `git cherry` for patch-ID-based matching, so commits already
+# cherry-picked to the target (under different SHAs) are correctly
+# detected and excluded.
+
+usage() {
+ cat <<'EOF'
+Usage:
+ backport-fixes.sh --from [ --to [--branch ] [--no-filter]
+
+Options:
+ --from ][ Source ref on origin: a branch (e.g. release/v0.6.0,
+ hotfix/v0.5.1) or a tag (e.g. v0.6.0). Do not include
+ 'origin/'. Required. Use the tag after the release
+ workflow has deleted the source branch.
+ --to Target branch on origin. Required.
+ --branch Name for the local backport branch (default:
+ `backport/vX.Y.Z` derived from --from when it
+ matches release/vX.Y.Z, hotfix/vX.Y.Z, or vX.Y.Z;
+ otherwise `backport/`).
+ --no-filter Include commits whose subject starts with
+ 'release:' or 'release-prep:' (automated version
+ bumps). Default behavior excludes them to keep the
+ target branch's own version string intact.
+ -h, --help Show this help.
+
+Behavior:
+ 1. Fetches origin (both branch and tag refs).
+ 2. Lists commits on the source ref that aren't already on origin/,
+ and commits that are already there (skipped).
+ 3. Creates the backport branch off origin/.
+ 4. Runs `git cherry-pick -x` with the candidates, oldest-first.
+ 5. On conflict: stops. Resolve, then `git cherry-pick --continue`.
+
+ By default, commits with subjects matching ^(release|release-prep):
+ are excluded from the candidate list — in this repo these are always
+ automated version-file edits that would conflict with the target's
+ own version string. Pass --no-filter to include them.
+
+If the backport branch already exists locally, the script fails — delete
+it (`git branch -D `) or pass `--branch ` to override.
+
+No push. No PR. The branch stays local; you decide what to do next.
+
+Examples:
+ backport-fixes.sh --from release/v0.6.0 --to main
+ backport-fixes.sh --from v0.6.0 --to main # branch deleted post-release
+EOF
+}
+
+FROM=""
+TO=""
+BRANCH_OVERRIDE=""
+NO_FILTER=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --from) FROM="${2-}"; shift 2 ;;
+ --to) TO="${2-}"; shift 2 ;;
+ --branch) BRANCH_OVERRIDE="${2-}"; shift 2 ;;
+ --no-filter) NO_FILTER=1; shift ;;
+ -h|--help) usage; exit 0 ;;
+ *) echo "error: unknown argument: $1" >&2; usage >&2; exit 2 ;;
+ esac
+done
+
+if [[ -z "$FROM" ]]; then
+ echo "error: --from is required" >&2
+ usage >&2
+ exit 2
+fi
+if [[ -z "$TO" ]]; then
+ echo "error: --to is required" >&2
+ usage >&2
+ exit 2
+fi
+if [[ "$FROM" == origin/* || "$TO" == origin/* ]]; then
+ echo "error: refs must not include 'origin/'" >&2
+ exit 2
+fi
+if [[ "$FROM" == "$TO" ]]; then
+ echo "error: --from and --to must differ" >&2
+ exit 2
+fi
+
+# Must be in a git repo
+if ! git rev-parse --git-dir >/dev/null 2>&1; then
+ echo "error: not inside a git repository" >&2
+ exit 1
+fi
+
+# Detect in-progress cherry-pick first — gives a more actionable error
+# than the generic dirty-tree check, which also trips during a conflict.
+if [[ -f "$(git rev-parse --git-dir)/CHERRY_PICK_HEAD" ]]; then
+ echo "error: a cherry-pick is already in progress. Finish or abort it first:" >&2
+ echo " git cherry-pick --continue | --skip | --abort" >&2
+ exit 1
+fi
+
+# Require clean working tree — cherry-picks onto a dirty tree are unsafe.
+if ! git diff --quiet || ! git diff --cached --quiet; then
+ echo "error: working tree has uncommitted changes. Commit or stash first." >&2
+ exit 1
+fi
+
+echo "Fetching origin..."
+# Fetch the target branch and try to fetch FROM as both a branch and a tag.
+# Branches go to refs/remotes/origin/; tags go to refs/tags/.
+# The two explicit refspecs make either accepted; whichever doesn't exist
+# is silently ignored.
+git fetch origin "$TO" --no-tags
+git fetch origin --no-tags \
+ "+refs/heads/$FROM:refs/remotes/origin/$FROM" 2>/dev/null \
+ || true
+git fetch origin --no-tags \
+ "+refs/tags/$FROM:refs/tags/$FROM" 2>/dev/null \
+ || true
+
+# Resolve FROM to a ref that exists locally after fetch.
+if git rev-parse --verify "refs/remotes/origin/$FROM" >/dev/null 2>&1; then
+ from_ref="refs/remotes/origin/$FROM"
+ from_pretty="origin/$FROM"
+elif git rev-parse --verify "refs/tags/$FROM" >/dev/null 2>&1; then
+ from_ref="refs/tags/$FROM"
+ from_pretty="$FROM (tag)"
+else
+ echo "error: $FROM not found on origin (tried both branches and tags)" >&2
+ exit 1
+fi
+
+if ! git rev-parse --verify "refs/remotes/origin/$TO" >/dev/null 2>&1; then
+ echo "error: origin/$TO not found on remote" >&2
+ exit 1
+fi
+
+# `git cherry -v ` prints one line per commit:
+# + -> not on upstream (candidate for backport)
+# - -> already on upstream via patch-ID match
+cherry_out="$(git cherry -v "refs/remotes/origin/$TO" "$from_ref" || true)"
+
+all_candidates="$(echo "$cherry_out" | awk '$1 == "+" { sub(/^\+ /, ""); print }')"
+already_pretty="$(echo "$cherry_out" | awk '$1 == "-" { sub(/^- /, ""); print }')"
+
+if [[ -n "$NO_FILTER" ]]; then
+ candidates_pretty="$all_candidates"
+ filtered_pretty=""
+else
+ filtered_pretty="$(echo "$all_candidates" | grep -E '^[0-9a-f]+ (release|release-prep):' || true)"
+ candidates_pretty="$(echo "$all_candidates" | grep -Ev '^[0-9a-f]+ (release|release-prep):' || true)"
+fi
+
+shas="$(echo "$candidates_pretty" | awk 'NF { print $1 }')"
+
+n_candidates=0
+[[ -n "$candidates_pretty" ]] && n_candidates=$(echo "$candidates_pretty" | wc -l | tr -d ' ')
+n_already=0
+[[ -n "$already_pretty" ]] && n_already=$(echo "$already_pretty" | wc -l | tr -d ' ')
+n_filtered=0
+[[ -n "$filtered_pretty" ]] && n_filtered=$(echo "$filtered_pretty" | wc -l | tr -d ' ')
+
+echo
+echo "=== Commits on $from_pretty not yet on origin/$TO ($n_candidates) ==="
+if [[ "$n_candidates" -gt 0 ]]; then
+ echo "$candidates_pretty"
+else
+ echo "(none)"
+fi
+
+if [[ "$n_already" -gt 0 ]]; then
+ echo
+ echo "=== Already on origin/$TO, excluded ($n_already) ==="
+ echo "$already_pretty"
+fi
+
+if [[ "$n_filtered" -gt 0 ]]; then
+ echo
+ echo "=== Skipped (version-bump commits; pass --no-filter to include) ($n_filtered) ==="
+ echo "$filtered_pretty"
+fi
+
+if [[ "$n_candidates" -eq 0 ]]; then
+ echo
+ echo "Nothing to backport."
+ exit 0
+fi
+
+if [[ -n "$BRANCH_OVERRIDE" ]]; then
+ branch="$BRANCH_OVERRIDE"
+elif [[ "$FROM" =~ ^(release|hotfix)/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
+ branch="backport/v${BASH_REMATCH[2]}"
+elif [[ "$FROM" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
+ branch="backport/v${BASH_REMATCH[1]}"
+else
+ flat="$(echo "$FROM" | sed -E 's|/|-|g; s/[^a-zA-Z0-9._-]+/-/g')"
+ branch="backport/${flat}"
+fi
+
+if git show-ref --verify --quiet "refs/heads/$branch"; then
+ echo "error: local branch '$branch' already exists." >&2
+ echo " Delete it (git branch -D $branch) or pass --branch ." >&2
+ exit 1
+fi
+
+echo
+echo "Creating branch $branch off origin/$TO..."
+git checkout -b "$branch" "origin/$TO"
+
+echo
+echo "Cherry-picking $n_candidates commit(s) with -x, oldest first..."
+echo "If git stops on a conflict:"
+echo " - Resolve, 'git add ', then 'git cherry-pick --continue'."
+echo " - To drop the conflicting commit: 'git cherry-pick --skip'."
+echo " - To bail out entirely: 'git cherry-pick --abort'."
+echo
+
+# shellcheck disable=SC2086 # intentional word-split: $shas is a hex-only list
+exec git cherry-pick -x $shas
diff --git a/src/Authplane.Mcp/Authplane.Mcp.csproj b/src/Authplane.Mcp/Authplane.Mcp.csproj
index f74fc57..488d2ed 100644
--- a/src/Authplane.Mcp/Authplane.Mcp.csproj
+++ b/src/Authplane.Mcp/Authplane.Mcp.csproj
@@ -14,13 +14,13 @@
https://github.com/AuthPlane/cs-sdk.git
git
true
- https://github.com/AuthPlane/cs-sdk/tree/develop/src/Authplane.Mcp
+ https://github.com/AuthPlane/cs-sdk/tree/main/src/Authplane.Mcp
Apache-2.0
README.md
icon.png
Authplane MCP adapter for .NET
Copyright © Authplane Team
- https://github.com/AuthPlane/cs-sdk/blob/develop/CHANGELOG.md
+ https://github.com/AuthPlane/cs-sdk/blob/main/CHANGELOG.md
diff --git a/src/Authplane/Authplane.csproj b/src/Authplane/Authplane.csproj
index d24ad7d..d799759 100644
--- a/src/Authplane/Authplane.csproj
+++ b/src/Authplane/Authplane.csproj
@@ -14,13 +14,13 @@
https://github.com/AuthPlane/cs-sdk.git
git
true
- https://github.com/AuthPlane/cs-sdk/tree/develop/src/Authplane
+ https://github.com/AuthPlane/cs-sdk/tree/main/src/Authplane
Apache-2.0
README.md
icon.png
Authplane SDK for .NET
Copyright © Authplane Team
- https://github.com/AuthPlane/cs-sdk/blob/develop/CHANGELOG.md
+ https://github.com/AuthPlane/cs-sdk/blob/main/CHANGELOG.md
]