Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 97 additions & 5 deletions .github/workflows/publish-npm.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
name: Publish to npm

# Tag-triggered publish. The three @authplane/* packages go to npm from
# Tag-triggered publish. The five @authplane/* packages go to npm from
# this workflow; release.yml only pushes the tag and lets this workflow
# take over.
#
# Runs on any v*.*.* tag regardless of which branch it points to. The
# tag itself is the authority — OIDC claim ref = refs/tags/vX.Y.Z, which
# matches the `npm` environment's tag-only deployment policy.
#
# Manual re-dispatch: enabled via `workflow_dispatch` so a failed run can
# be retried for an existing tag without re-publishing from a developer
# machine. The OIDC claim is built from `GITHUB_REF` at run time, not
# from any input — so you MUST dispatch this workflow against the tag
# ref itself (e.g. `gh workflow run publish-npm.yml -r vX.Y.Z` or the
# UI's Run-workflow dropdown set to the tag). Dispatching from a branch
# (main, develop, …) emits an OIDC token with `ref=refs/heads/<branch>`
# which the tag-only Trusted Publisher policy on npmjs.com will reject.

on:
push:
tags:
- "v*.*.*"
workflow_dispatch:

# Shared with `workflow_dispatch` retries against the same tag — see
# RELEASE_GUIDE.md → Troubleshooting → "Re-running the publish workflow
# against a tag" → "Shared concurrency group with the original tag-push
# run" admonition. `cancel-in-progress` stays `false` deliberately:
# cancelling mid-publish would risk a partial multi-package upload, the
# exact failure mode the partial-upload recovery section exists to clean
# up after. Don't "fix" this group key without reading those docs first.
concurrency:
group: publish-npm-${{ github.ref }}
cancel-in-progress: false
Expand Down Expand Up @@ -40,10 +57,20 @@ jobs:
# and version-dependent. Letting npm use its default registry keeps
# .npmrc untouched; the @authplane/* packages publish to npmjs.org
# via npm's defaults.
#
# Node 24 LTS bundles npm 11.x, which is the floor for OIDC Trusted
# Publishing (npm 11.5.1+). Node 22 LTS bundles npm 10.9.x, which
# silently skips the OIDC token exchange and falls back to demanding
# a classic credential — the failure mode that crashed the `0.2.0`
# release tag with `ENEEDAUTH`. Keep this at 24+ so the publish step
# actually performs the OIDC handshake and produces the `--provenance`
# Sigstore attestation without an in-place npm upgrade. Engines
# are satisfied (`>=22` root, `>=20` per-package) and `ci.yml`
# already exercises both 22 and 24 in matrix.
- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
node-version: 24
cache: npm

- name: Derive version from tag
Expand Down Expand Up @@ -71,15 +98,17 @@ jobs:
for pkg in \
packages/sdk/package.json \
packages/mcp/package.json \
packages/fastmcp/package.json
packages/fastmcp/package.json \
packages/hono/package.json \
packages/nestjs/package.json
do
pv="$(node -p "require('./$pkg').version")"
if [[ "$pv" != "$v" ]]; then
echo "::error::$pkg has version $pv but tag is v$v. Refusing to publish."
exit 1
fi
done
echo "All three packages report version $v ✓"
echo "All five packages report version $v ✓"

- name: Install dependencies
run: npm ci
Expand All @@ -92,6 +121,8 @@ jobs:
cd packages/sdk && npm pack
cd ../mcp && npm pack
cd ../fastmcp && npm pack
cd ../hono && npm pack
cd ../nestjs && npm pack

- name: Upload built tarballs (for partial-publish recovery)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand All @@ -101,6 +132,8 @@ jobs:
packages/sdk/*.tgz
packages/mcp/*.tgz
packages/fastmcp/*.tgz
packages/hono/*.tgz
packages/nestjs/*.tgz
if-no-files-found: error
retention-days: 30

Expand All @@ -124,6 +157,65 @@ jobs:
- name: Publish @authplane/fastmcp to npm
run: npm publish --provenance --access public -w @authplane/fastmcp

- name: Publish @authplane/hono to npm
run: npm publish --provenance --access public -w @authplane/hono

- name: Publish @authplane/nestjs to npm
run: npm publish --provenance --access public -w @authplane/nestjs

# The whole point of the OIDC Trusted Publishing flow is the Sigstore
# provenance attestation. `npm publish --provenance` can succeed
# without attaching one (e.g. the publish credential exchange worked
# but the attestation upload silently degraded — exactly the kind of
# invisible regression that masked `0.2.0`'s ENEEDAUTH until release
# time). Read each manifest back and require dist.attestations to
# exist. Versions are already on npm at this point, so a failure
# here is a loud signal for the maintainer, not a recovery path —
# the next release is the first chance to re-attach provenance.
#
# `if: always()` so a partial-upload failure mid-sequence (sdk OK,
# mcp publish fails) still verifies whatever did land on the registry
# — that's exactly the case where the loud signal matters most. The
# per-package skip below guards against false-failing on packages
# this run never managed to publish.
#
# Brief retry absorbs registry CDN propagation delay between publish
# and the read-side manifest reflecting the attestation.
- name: Verify provenance attached for each published package
if: always()
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
fail=0
for p in sdk mcp fastmcp hono nestjs; do
# Skip packages that aren't on the registry at this version
# (publish step never ran or failed before pushing). Only
# verify what actually landed.
published="$(npm view --prefer-online "@authplane/${p}@${VERSION}" version 2>/dev/null || true)"
if [[ -z "${published}" ]]; then
echo "@authplane/${p}@${VERSION} not on registry — skipping provenance check"
continue
fi
pred=""
for attempt in 1 2 3; do
pred="$(npm view --prefer-online "@authplane/${p}@${VERSION}" dist.attestations.provenance.predicateType 2>/dev/null || true)"
if [[ -n "${pred}" ]]; then
break
fi
if [[ "${attempt}" -lt 3 ]]; then
echo "attempt ${attempt}: no provenance yet for @authplane/${p}@${VERSION}; retrying in 5s..."
sleep 5
fi
done
if [[ -z "${pred}" ]]; then
echo "::error::@authplane/${p}@${VERSION} published WITHOUT provenance attestation"
fail=1
else
echo "@authplane/${p}@${VERSION} provenance: ${pred} ✓"
fi
done
exit "${fail}"

- name: Summary
if: always()
run: |
Expand All @@ -132,5 +224,5 @@ jobs:
echo ""
echo "- **Tag**: \`${{ steps.version.outputs.tag }}\`"
echo "- **Version**: \`${{ steps.version.outputs.version }}\`"
echo "- [@authplane/sdk](https://www.npmjs.com/package/@authplane/sdk/v/${{ steps.version.outputs.version }}) · [@authplane/mcp](https://www.npmjs.com/package/@authplane/mcp/v/${{ steps.version.outputs.version }}) · [@authplane/fastmcp](https://www.npmjs.com/package/@authplane/fastmcp/v/${{ steps.version.outputs.version }})"
echo "- [@authplane/sdk](https://www.npmjs.com/package/@authplane/sdk/v/${{ steps.version.outputs.version }}) · [@authplane/mcp](https://www.npmjs.com/package/@authplane/mcp/v/${{ steps.version.outputs.version }}) · [@authplane/fastmcp](https://www.npmjs.com/package/@authplane/fastmcp/v/${{ steps.version.outputs.version }}) · [@authplane/hono](https://www.npmjs.com/package/@authplane/hono/v/${{ steps.version.outputs.version }}) · [@authplane/nestjs](https://www.npmjs.com/package/@authplane/nestjs/v/${{ steps.version.outputs.version }})"
} >> "$GITHUB_STEP_SUMMARY"
6 changes: 5 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ jobs:
fi
for pkg in \
packages/mcp/package.json \
packages/fastmcp/package.json
packages/fastmcp/package.json \
packages/hono/package.json \
packages/nestjs/package.json
do
v="$(node -p "require('./$pkg').version")"
if [[ "$v" != "$pkg_version" ]]; then
Expand Down Expand Up @@ -236,6 +238,8 @@ jobs:
cd packages/sdk && npm pack
cd ../mcp && npm pack
cd ../fastmcp && npm pack
cd ../hono && npm pack
cd ../nestjs && npm pack

- name: Dry run short-circuit notice
if: ${{ inputs.dryRun }}
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/workflows-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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, version-pinned in the URL. This
# is a lint-only path with no secrets / no publish surface, so the
# supply-chain risk of a raw.githubusercontent.com fetch is bounded
# to "the lint job lies about workflow validity" — bump the version
# deliberately rather than tracking a moving tag.
#
# 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_INSTALL_DIR: ${{ runner.temp }}/actionlint
run: |
mkdir -p "${ACTIONLINT_INSTALL_DIR}"
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/v${ACTIONLINT_VERSION}/scripts/download-actionlint.bash") \
"${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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

## [Unreleased]

### Added

- `@authplane/hono` — new Hono adapter: `authplaneHonoAuth()`, `bearerAuth` middleware (Bearer + DPoP, RFC 9449 §7.1), `requireScope` guard, and an RFC 9728 PRM handler.
- `@authplane/nestjs` — new NestJS adapter: `AuthplaneModule.forRoot()` / `forRootAsync()`, `AuthplaneAuthGuard`, `@SkipAuth()` / `@RequireScopes()` decorators, `AuthplaneExceptionFilter` (RFC 6750 §3), and a PRM controller (RFC 9728). Supports Express and Fastify.
- `@authplane/sdk` — `TokenCache` is now bounded by a configurable `maxEntries` cap (default `10_000`) with LRU eviction, via `AuthplaneClient.create({ cacheMaxEntries })`.
- `@authplane/sdk` — new `@authplane/sdk/core` helpers: `extractBearerToken`, `buildRequestUrl`, `pathAndQueryOf`, `extractDpopProof`, `oauthProtectedResourceMetadataPath`.

### Changed

- `@authplane/sdk` — `oauthProtectedResourceMetadataDocumentUrl(resource)` now normalises trailing slashes (RFC 9728 §3.1) and throws a typed `TypeError` on invalid URLs. **Migration**: drop any deliberate trailing slash from `resource` before upgrading — the canonical form is no-trailing-slash.
- `@authplane/mcp` — now accepts a DPoP-bound token presented under the `DPoP` scheme (RFC 9449 §7.1) instead of rejecting it with 401.
- `@authplane/fastmcp` — stricter RFC 6750 §2.1 `Authorization` parsing, consistent with the other adapters.

### Fixed

- `@authplane/mcp` — a non-URL `aud` claim now returns 401 `invalid_token` (RFC 8707) instead of a 500.

## [0.2.0] - 2026-05-22

- `@authplane/fastmcp`: support `fastmcp` 4.x in addition to 3.35+. Consumers using `OAuthProxy` directly must set `allowedRedirectUriPatterns` themselves — fastmcp 4.0 removed the default value (CWE-601 fix).
Expand All @@ -19,3 +36,9 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
## [0.1.0] - 2026-05-11

- Initial release.
- `@authplane/fastmcp` supports `fastmcp` 4.x in addition to 3.35+. The adapter only consumes `ServerOptions["authenticate"]` and `oauth.protectedResource.*`, which are byte-identical between 3.35.1 and 4.0.1. Consumers who use `OAuthProxy` directly must configure `allowedRedirectUriPatterns` themselves — fastmcp 4.0 removed the default value (CWE-601 fix); see the fastmcp v4.0.0 release notes.
- `@authplane/sdk` — `wwwAuthenticate(error, options)` accepts new `resourceMetadataUrl` (RFC 9728 §5.1) and `scope` (RFC 6750) options, sanitises every interpolated value against header injection (RFC 9110 §11.4), and emits the `Bearer` scheme for `DPoPNotSupported` (carve-out — although it extends `DPoPError`, the request was not DPoP-bound). Canonical error → status/challenge tables now live in the SDK user guide under "HTTP status and WWW-Authenticate challenge".
- `@authplane/fastmcp` and `@authplane/mcp` — every authentication failure now emits a spec-compliant per-error `WWW-Authenticate` challenge via `httpStatus` + `wwwAuthenticate` from `@authplane/sdk/core`. DPoP failures (replay, binding mismatch, missing/invalid proof) get the `DPoP` scheme per RFC 9449 §7.1; `resource_metadata` is always included and `scope` is included when `requiredScopes` is configured. Previously every failure collapsed to a generic `Bearer error="invalid_token", error_description="Invalid access token"`.
- **Migration — `@authplane/fastmcp`**: `AuthplaneTokenVerifier.verifyAccessToken` no longer swallows `AuthplaneError` and returns `undefined`; the underlying typed class propagates. Consumers wiring `tokenVerifier` directly should wrap in try/catch if they previously branched on the `undefined` sentinel.
- **Migration — `@authplane/mcp`**: `AuthplaneTokenVerifier.verifyAccessToken` no longer rewraps `AuthplaneError` as the MCP SDK's `InvalidTokenError`; the underlying typed class propagates. Consumers wiring `tokenVerifier` directly should switch `instanceof InvalidTokenError` checks to `instanceof AuthplaneError` and classify with `httpStatus(err)` / `wwwAuthenticate(err, options)` from `@authplane/sdk/core` — or move to the `bearerAuth` middleware the adapter ships.
- **Behaviour change — `@authplane/mcp` upstream-AS failures**: `JWKSFetchError` and `MetadataFetchError` previously fell through the adapter's catch and returned a generic 500 (`{ error: "server_error" }`). They now hit the shared funnel via `httpStatus(error)` and return **503** with `WWW-Authenticate: Bearer error="invalid_token"` (RFC 7235 §4.1 allows `WWW-Authenticate` on any response). The 503 is more accurate, but the `invalid_token` OAuth code in the body is a semantic mismatch since the failure is upstream, not in the client's token — **clients should rely on the HTTP status, not the `error` field, when deciding whether to retry with a fresh token.**
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Authplane TypeScript SDK

OAuth, JWT validation, and MCP-authentication primitives for Node.js. Works with Express, FastMCP, and the official MCP TypeScript SDK.
OAuth, JWT validation, and MCP-authentication primitives for Node.js. Ships framework adapters for Hono, NestJS, FastMCP, and the official MCP TypeScript SDK.

## Packages

Expand All @@ -9,6 +9,8 @@ OAuth, JWT validation, and MCP-authentication primitives for Node.js. Works with
| [`@authplane/sdk`](packages/sdk) | `npm install @authplane/sdk` | JWT validation and OAuth protocol primitives. Ships the stateful `AuthplaneClient` (`@authplane/sdk/core`) plus stateless OAuth protocol helpers (`@authplane/sdk/auth`). |
| [`@authplane/mcp`](packages/mcp) | `npm install @authplane/sdk @authplane/mcp` | JWT validation adapter for the MCP TypeScript SDK |
| [`@authplane/fastmcp`](packages/fastmcp) | `npm install @authplane/sdk @authplane/fastmcp` | JWT validation adapter for FastMCP |
| [`@authplane/hono`](packages/hono) | `npm install @authplane/sdk @authplane/hono` | JWT validation middleware for the Hono web framework |
| [`@authplane/nestjs`](packages/nestjs) | `npm install @authplane/sdk @authplane/nestjs` | NestJS module: guard + decorators + exception filter + PRM controller |

## Requirements

Expand Down Expand Up @@ -69,7 +71,9 @@ For the MCP TypeScript SDK variant, see the [`@authplane/mcp` README](packages/m

- [`@authplane/mcp`](packages/mcp/README.md) — adapter for the official MCP TypeScript SDK.
- [`@authplane/fastmcp`](packages/fastmcp/README.md) — adapter for FastMCP.
- Both adapters integrate with Express / Node.js `http` through the MCP and FastMCP transports.
- [`@authplane/hono`](packages/hono/README.md) — middleware for the Hono web framework (Bearer / DPoP, RFC 6750 `WWW-Authenticate`, PRM handler, scope enforcement).
- [`@authplane/nestjs`](packages/nestjs/README.md) — NestJS module: `AuthplaneAuthGuard`, `@SkipAuth()` / `@RequireScopes(...)` decorators, exception filter mapping to RFC 6750 §3 responses, and an RFC 9728 PRM controller. Works on Express and Fastify platform adapters.
- The MCP / FastMCP adapters integrate with Express / Node.js `http` through their respective transports; the Hono adapter is framework-native middleware; the NestJS adapter integrates with whatever platform NestJS is hosted on.

## Documentation

Expand All @@ -78,6 +82,8 @@ Each package ships its own README (overview) and User Guide (complete reference)
- **`@authplane/sdk`** — [README](packages/sdk/README.md) · [User Guide](packages/sdk/docs/user-guide.md)
- **`@authplane/mcp`** — [README](packages/mcp/README.md) · [User Guide](packages/mcp/docs/user-guide.md)
- **`@authplane/fastmcp`** — [README](packages/fastmcp/README.md) · [User Guide](packages/fastmcp/docs/user-guide.md)
- **`@authplane/hono`** — [README](packages/hono/README.md) · [User Guide](packages/hono/docs/user-guide.md)
- **`@authplane/nestjs`** — [README](packages/nestjs/README.md) · [User Guide](packages/nestjs/docs/user-guide.md)

Other docs:

Expand Down
Loading
Loading