JwtBearer 10.0.7 transitively brings in Microsoft.IdentityModel.JsonWebTokens
9.x, which calls Base64UrlEncoder.Decode(ReadOnlySpan<char>, Span<byte>) —
an overload only present in Microsoft.IdentityModel.Tokens 9.x. The
explicit pin to 8.9.0 forced NuGet to downgrade Tokens to 8.9, producing
a runtime MissingMethodException on every JWT validation. Symptom: every
incoming request gets 401, never visible at default log level.
Drop the explicit pin so JwtBearer's transitive dep wins. The code only
uses SymmetricSecurityKey etc., which are stable across versions.
Same fix as nas-auth: mode=max takes ~4min and times out on the NAS,
which marks the build as failure and skips deploy. mode=min is fast
and ignore-error keeps the build from failing on cache export.
Mirror the cache infra added to nas-auth (commit a1ecba3):
- Dockerfile: `RUN --mount=type=cache,target=/root/.nuget/packages`
for restore + publish, with syntax 1.6 directive.
- workflow: cache-from/cache-to registry cache mode=max, so buildkit
layer cache survives across CI runs (fresh buildkit per run otherwise).
First run after this still pays full cost; each subsequent run should
drop ~50% off dotnet restore.
Claude.ai's MCP OAuth client only sends the RFC 8707 `resource` param
when it has Protected Resource Metadata (PRM) to learn the resource
identifier from. Without it, nas-auth's /authorize rejected the request
with 'resource 必填'.
Adds /.well-known/oauth-protected-resource (RFC 9728) that returns the
resource URL + AS URL + supported scopes. ResourceUrl is configurable
via Mcp__OAuthDiscovery__ResourceUrl, falling back to request authority
when unset (works in local dev).
Deploy job now cd's into /volume1/docker/compose/obsidian-mcp (exposed
to runner via the new gitea-runner mount) instead of cloning nas-infra
to a temp dir. The clone approach couldn't deploy because .env.shared
is gitignored and only exists on NAS.