diff --git a/.busbar-ref b/.busbar-ref index 5b81b3e..7a2df5d 100644 --- a/.busbar-ref +++ b/.busbar-ref @@ -1 +1 @@ -33671357f37a411fd135a2d15cbed3d9947f34f0 1.6.0 +41fd7c5f386fa098890c031721f19bfca903f225 1.6.0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 45f159d..1ba6a40 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/. +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/. * @MattJackson diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 00c127c..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,101 +0,0 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/. -# Dependency updates target dev. GitHub Actions are pinned by the fleet template (busbar-release), -# so only cargo is watched here. Every update of an ecosystem is ONE grouped PR, and -# .github/workflows/dependabot-bundle.yml folds all of them into one branch and one PR (DEPENDABOT-ONE). -# The crates busbar's dependency policy owns (busbar's root [workspace.dependencies] and -# .github/fleet/deps.toml) are ignored here: they move centrally, in busbar, and reach this repo -# through `busbar-release plugin sync` with the pin, so Cargo.lock stays at parity with busbar's. -version: 2 -updates: - - package-ecosystem: cargo - directory: "/" - target-branch: dev - schedule: - interval: weekly - groups: - all: - patterns: - - "*" - ignore: - - dependency-name: "a2a-lf" - - dependency-name: "a2a-pb" - - dependency-name: "arc-swap" - - dependency-name: "async-trait" - - dependency-name: "axum" - - dependency-name: "base64" - - dependency-name: "bumpalo" - - dependency-name: "bytes" - - dependency-name: "core_affinity" - - dependency-name: "crc32fast" - - dependency-name: "criterion" - - dependency-name: "dimpl" - - dependency-name: "ed25519-dalek" - - dependency-name: "flate2" - - dependency-name: "futures" - - dependency-name: "getrandom" - - dependency-name: "h2" - - dependency-name: "hex" - - dependency-name: "hmac" - - dependency-name: "http" - - dependency-name: "http-body" - - dependency-name: "http-body-util" - - dependency-name: "httpdate" - - dependency-name: "hyper" - - dependency-name: "hyper-rustls" - - dependency-name: "hyper-util" - - dependency-name: "indexmap" - - dependency-name: "jsonschema" - - dependency-name: "ldap3" - - dependency-name: "libc" - - dependency-name: "libloading" - - dependency-name: "log" - - dependency-name: "loom" - - dependency-name: "memchr" - - dependency-name: "metrics" - - dependency-name: "metrics-exporter-prometheus" - - dependency-name: "metrics-util" - - dependency-name: "mysql" - - dependency-name: "oauth-as" - - dependency-name: "opentelemetry-proto" - - dependency-name: "postgres" - - dependency-name: "proc-macro2" - - dependency-name: "proptest" - - dependency-name: "prost" - - dependency-name: "rcgen" - - dependency-name: "redis" - - dependency-name: "reqwest" - - dependency-name: "ring" - - dependency-name: "rmcp" - - dependency-name: "rusqlite" - - dependency-name: "rustls" - - dependency-name: "rustls-pki-types" - - dependency-name: "schemars" - - dependency-name: "serde" - - dependency-name: "serde_json" - - dependency-name: "serde_urlencoded" - - dependency-name: "serde_yaml" - - dependency-name: "sha2" - - dependency-name: "smallvec" - - dependency-name: "socket2" - - dependency-name: "sonic-rs" - - dependency-name: "syn" - - dependency-name: "tar" - - dependency-name: "tikv-jemalloc-ctl" - - dependency-name: "tikv-jemallocator" - - dependency-name: "tokio" - - dependency-name: "tokio-rustls" - - dependency-name: "tokio-tungstenite" - - dependency-name: "tokio-util" - - dependency-name: "tonic" - - dependency-name: "tonic-types" - - dependency-name: "tower" - - dependency-name: "tracing" - - dependency-name: "tracing-core" - - dependency-name: "tracing-log" - - dependency-name: "tracing-subscriber" - - dependency-name: "tungstenite" - - dependency-name: "ulid" - - dependency-name: "url" - - dependency-name: "webpki" - - dependency-name: "webpki-roots" - - dependency-name: "zeroize" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e6356d..5c98dbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync busbar-auth-github` from GetBusbar/busbar-release template/; a hand +# RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release template/; a hand # edit is overwritten by the next sync. THIS REPO TESTS ITSELF AGAINST BUSBAR with the fleet's ONE # harness: busbar's reusable plugin-ci.yml, taken at the busbar commit this repo pins (.busbar-ref), so # the CI logic, the gates (busbar scripts/fleet/plugin-gates.py), the dependency policy @@ -6,20 +6,30 @@ name: ci on: + # A tree is tested once: on its pull request into dev and on the push that lands it on dev. qa and + # main take dev's commits by pull request whose head is that tested commit: the checks it carries + # from dev's run satisfy the ruleset, and nothing re-tests it. Any other head: run this by hand + # (workflow_dispatch) on its branch. push: - branches: [dev, qa, main] + branches: [dev] pull_request: - branches: [dev, qa, main] + branches: [dev] workflow_dispatch: {} # release.yml runs this file on the tagged commit before it publishes. workflow_call: {} +# A newer push to a pull request cancels its older run (only the newest commit can merge); a push to +# dev, a dispatch and release.yml's call run to the end. +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read jobs: ci: - uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@33671357f37a411fd135a2d15cbed3d9947f34f0 + uses: GetBusbar/busbar/.github/workflows/plugin-ci.yml@41fd7c5f386fa098890c031721f19bfca903f225 with: service: wiremock busbar_checkout: true diff --git a/.github/workflows/consumer-verify.yml b/.github/workflows/consumer-verify.yml index 080ca5b..40f8f81 100644 --- a/.github/workflows/consumer-verify.yml +++ b/.github/workflows/consumer-verify.yml @@ -1,12 +1,12 @@ -# RENDERED by `busbar-release plugin sync busbar-auth-github` from GetBusbar/busbar-release template/; a hand -# edit is overwritten by the next sync. Daily: does the newest +# RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release template/; a hand +# edit is overwritten by the next sync. Weekly: does the newest # PUBLISHED release still work for a user (busbar's plugin-consumer-verify.yml)? A publish-time check # cannot see an artifact that rots afterwards; release.yml verifies each release as it publishes. name: consumer-verify on: schedule: - - cron: "45 9 * * *" + - cron: "59 9 * * 1" workflow_dispatch: inputs: version: @@ -21,7 +21,7 @@ permissions: jobs: consumer: - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@33671357f37a411fd135a2d15cbed3d9947f34f0 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@41fd7c5f386fa098890c031721f19bfca903f225 with: version: ${{ inputs.version || '' }} asset_prefix: busbar-auth-github diff --git a/.github/workflows/dependabot-bundle.yml b/.github/workflows/dependabot-bundle.yml index 17b71ba..f57ab7d 100644 --- a/.github/workflows/dependabot-bundle.yml +++ b/.github/workflows/dependabot-bundle.yml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync busbar-auth-github` from GetBusbar/busbar-release template/; a hand +# RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release template/; a hand # edit is overwritten by the next sync (DEPENDABOT-ONE). # dependabot-bundle: every open dependabot PR (whatever its base today) is merged into ONE branch (deps/bundle), # which carries ONE PR with auto-merge on. Merged PRs are closed with a link; a PR that conflicts @@ -13,7 +13,7 @@ on: types: [opened, synchronize] branches: [dev] schedule: - - cron: "23 5 * * *" + - cron: "23 5 * * 1" workflow_dispatch: permissions: contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ae9e61..ba868d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync busbar-auth-github` from GetBusbar/busbar-release template/; a hand +# RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release template/; a hand # edit is overwritten by the next sync. On a v* tag: this repo's own ci.yml (the fleet harness) on the # tagged commit, then busbar's plugin-release.yml (build, sign, pack, publish), then the consumer # verification of what was published, each at the busbar commit this repo pins. @@ -25,7 +25,7 @@ jobs: secrets: inherit release: needs: ci - uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@33671357f37a411fd135a2d15cbed3d9947f34f0 + uses: GetBusbar/busbar/.github/workflows/plugin-release.yml@41fd7c5f386fa098890c031721f19bfca903f225 with: plugin_crate: busbar-auth-github-plugin manifest_name: busbar-auth-github @@ -37,11 +37,12 @@ jobs: needs_prompt: "" needs_user: "" declares_file: "busbar-auth-github/declares.json" + former_names: "busbar-auth-github-plugin" secrets: inherit consumer-verify: needs: release if: ${{ !cancelled() }} - uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@33671357f37a411fd135a2d15cbed3d9947f34f0 + uses: GetBusbar/busbar/.github/workflows/plugin-consumer-verify.yml@41fd7c5f386fa098890c031721f19bfca903f225 with: version: ${{ github.ref_name }} asset_prefix: busbar-auth-github diff --git a/.github/workflows/repin.yml b/.github/workflows/repin.yml index 5df7054..19d84b3 100644 --- a/.github/workflows/repin.yml +++ b/.github/workflows/repin.yml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync busbar-auth-github` from GetBusbar/busbar-release template/; a hand +# RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release template/; a hand # edit is overwritten by the next sync. Moves this repo's # busbar pin (busbar's plugin-repin.yml) when busbar dispatches `busbar-repin` or on demand; the # cut/skip decision is by commit, and the commit goes to dev only. @@ -28,7 +28,7 @@ permissions: jobs: repin: - uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@33671357f37a411fd135a2d15cbed3d9947f34f0 + uses: GetBusbar/busbar/.github/workflows/plugin-repin.yml@41fd7c5f386fa098890c031721f19bfca903f225 with: busbar_sha: ${{ github.event.client_payload.sha || inputs.busbar_sha }} busbar_version: ${{ github.event.client_payload.version || inputs.busbar_version }} diff --git a/.gitignore b/.gitignore index bfd8c32..c144adc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/ (the plugins.yaml +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/ (the plugins.yaml # entry's `gitignore:` lines follow the fleet's own). /target /mutants.out* diff --git a/.mailmap b/.mailmap index 5f52e81..33013c2 100644 --- a/.mailmap +++ b/.mailmap @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/. +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/. # Canonical authorship. Commits go out under Matthew Jackson's GitHub noreply only. Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Matthew diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67299f4..e129494 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,14 +7,14 @@ Thanks for your interest in improving `busbar-auth-github`. - Be respectful and constructive in all project spaces (see [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)). - By contributing, you agree your contributions are licensed under the project's - [MIT](LICENSE) license. + [Apache-2.0](LICENSE) license. - Security issues go through [SECURITY.md](SECURITY.md), **not** public issues. ## Layout Every busbar plugin repo has the same skeleton. This one is a two-crate Cargo workspace: `auth-github/` holds the plugin's logic and `auth-github-plugin/` is the thin `cdylib` that packages it as a droppable `kind: auth` plugin. busbar itself is a git dependency pinned to the commit in `.busbar-ref`. The CI, release, dependency and lint configuration -are rendered by `busbar-release plugin sync` from the fleet template (GetBusbar/busbar-release +are rendered by `busbar-release plugin heal` from the fleet template (GetBusbar/busbar-release `template/`), [busbar's plugin registry](https://github.com/GetBusbar/busbar/blob/main/plugins.yaml) and busbar's dependency policy (`.github/fleet/deps.toml` and the root `[workspace.dependencies]` at the pin); change them there, not here. diff --git a/Cargo.lock b/Cargo.lock index 5c1188b..5714027 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,7 +82,7 @@ dependencies = [ [[package]] name = "busbar-contract" version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=33671357f37a411fd135a2d15cbed3d9947f34f0#33671357f37a411fd135a2d15cbed3d9947f34f0" +source = "git+https://www.github.com/GetBusbar/busbar?rev=41fd7c5f386fa098890c031721f19bfca903f225#41fd7c5f386fa098890c031721f19bfca903f225" dependencies = [ "async-trait", "base64", @@ -102,33 +102,12 @@ dependencies = [ "zeroize", ] -[[package]] -name = "busbar-kernel-ledger" -version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=33671357f37a411fd135a2d15cbed3d9947f34f0#33671357f37a411fd135a2d15cbed3d9947f34f0" -dependencies = [ - "busbar-contract", - "ring", - "serde", -] - -[[package]] -name = "busbar-kernel-wal" -version = "1.6.0" -source = "git+https://github.com/GetBusbar/busbar?rev=33671357f37a411fd135a2d15cbed3d9947f34f0#33671357f37a411fd135a2d15cbed3d9947f34f0" -dependencies = [ - "busbar-contract", - "ring", -] - [[package]] name = "busbar-plugin-loader" version = "1.5.0" -source = "git+https://github.com/GetBusbar/busbar?rev=33671357f37a411fd135a2d15cbed3d9947f34f0#33671357f37a411fd135a2d15cbed3d9947f34f0" +source = "git+https://github.com/GetBusbar/busbar?rev=41fd7c5f386fa098890c031721f19bfca903f225#41fd7c5f386fa098890c031721f19bfca903f225" dependencies = [ "busbar-contract", - "busbar-kernel-ledger", - "busbar-kernel-wal", "ed25519-dalek", "flate2", "futures", diff --git a/Cargo.toml b/Cargo.toml index f2b8a8a..d20d548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ -# SPDX-License-Identifier: MIT +# SPDX-License-Identifier: Apache-2.0 # -# RENDERED by `busbar-release plugin sync busbar-auth-github` from GetBusbar/busbar-release template/ and +# RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release template/ and # GetBusbar/busbar's dependency policy; a hand edit is overwritten by the next sync. # # One repo per plugin, every repo a twin: this workspace is exactly two crates, the logic (the rlib a @@ -10,8 +10,8 @@ # `.busbar-ref`; no sibling-checkout path dependency ships in any manifest. # # THIRD-PARTY VERSIONS ARE THE FLEET'S, NOT THIS REPO'S. The table below is busbar's own root -# `[workspace.dependencies]` (third-party rows) at busbar 33671357f37a411fd135a2d15cbed3d9947f34f0, followed by the plugin-only rows -# of busbar's `.github/fleet/deps.toml` at 33671357f37a411fd135a2d15cbed3d9947f34f0. A member takes a crate with +# `[workspace.dependencies]` (third-party rows) at busbar 41fd7c5f386fa098890c031721f19bfca903f225, followed by the plugin-only rows +# of busbar's `.github/fleet/deps.toml` at 41fd7c5f386fa098890c031721f19bfca903f225. A member takes a crate with # `{ workspace = true }` (adding features if it needs them); a crate in neither table is a new # `deps.toml` row in busbar first. CI holds this repo's Cargo.lock to busbar's lock at the pin. [workspace] @@ -24,16 +24,23 @@ members = [ [workspace.package] edition = "2021" rust-version = "1.98" -license = "MIT" +license = "Apache-2.0" publish = false repository = "https://github.com/GetBusbar/busbar-auth-github" [workspace.dependencies] -busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "33671357f37a411fd135a2d15cbed3d9947f34f0" } -busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "33671357f37a411fd135a2d15cbed3d9947f34f0" } -# busbar's root [workspace.dependencies], third-party rows, at 33671357f37a411fd135a2d15cbed3d9947f34f0 +busbar-contract = { git = "https://github.com/GetBusbar/busbar", rev = "41fd7c5f386fa098890c031721f19bfca903f225" } +busbar-plugin-loader = { git = "https://github.com/GetBusbar/busbar", rev = "41fd7c5f386fa098890c031721f19bfca903f225" } +# THE SUITE'S HOST CONNECTOR (dev-only; auth-github-plugin/tests/support/conformance_host.rs, +# rendered because the declares file states network needs): busbar's connector and its TLS +# test kit at the pin, and the transport framers those needs name, at the revs busbar links. +busbar-core-connector = { git = "https://github.com/GetBusbar/busbar", rev = "41fd7c5f386fa098890c031721f19bfca903f225", features = ["test-support"] } +busbar-transport-http = { git = "https://github.com/GetBusbar/busbar-transport-http", rev = "a715d1ab57dcec469930563da0c0509a4fb75289" } +busbar-transport-tcp = { git = "https://github.com/GetBusbar/busbar-transport-tcp", rev = "eb334dd6e9d3dcd62ae1616f77a5ed56311b255a" } +# busbar's root [workspace.dependencies], third-party rows, at 41fd7c5f386fa098890c031721f19bfca903f225 a2a-lf = "0.3.0" a2a-pb = "0.2.0" +aes = { version = "0.8", default-features = false } arc-swap = "1" async-trait = "0.1" axum = "0.8" @@ -75,7 +82,6 @@ prost = { version = "0.14", default-features = false, features = ["std"] } rcgen = { version = "0.14", default-features = false, features = ["crypto", "ring"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } ring = "0.17" -rmcp = { version = "3.1.2", default-features = false } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } rustls-pki-types = { version = "1", default-features = false, features = ["std"] } schemars = "1" @@ -87,6 +93,7 @@ sha2 = "0.10" smallvec = "1" socket2 = { version = "0.6", features = ["all"] } sonic-rs = "0.5" +str0m = { version = "=0.24.1", default-features = false } syn = { version = "2", features = ["full", "visit", "parsing", "printing"] } tar = { version = "0.4", default-features = false } tikv-jemalloc-ctl = "0.6" @@ -107,10 +114,22 @@ url = "2" webpki = { package = "rustls-webpki", version = "0.103", default-features = false, features = ["std", "ring"] } webpki-roots = "1" zeroize = "1" -# busbar .github/fleet/deps.toml [plugin-deps], at 33671357f37a411fd135a2d15cbed3d9947f34f0 +# busbar .github/fleet/deps.toml [plugin-deps], at 41fd7c5f386fa098890c031721f19bfca903f225 +fallible-iterator = "0.2" ldap3 = { version = "0.12", default-features = false, features = ["sync"] } -mysql = { version = "26", default-features = false, features = ["minimal"] } +mysql = { version = "28", default-features = false, features = ["minimal-rust"] } +mysql_common = { version = "0.35", default-features = false } +percent-encoding = "2" postgres = "0.19" +postgres-protocol = "0.6.11" +postgres-types = "0.2" redis = { version = "1", default-features = false, features = ["script"] } rusqlite = { version = "0.40", features = ["bundled"] } ulid = "1" + +# ONE busbar-contract (the suite's host connector): the transport framers above name +# busbar-contract at their own revs of busbar; this repo's pin serves them all, as busbar +# patches them to its own tree. The source is busbar spelled through www.github.com: Cargo +# refuses a patch that names the source it patches. +[patch."https://github.com/GetBusbar/busbar"] +busbar-contract = { git = "https://www.github.com/GetBusbar/busbar", rev = "41fd7c5f386fa098890c031721f19bfca903f225" } diff --git a/LICENSE b/LICENSE index 26e9150..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -MIT License - -Copyright (c) 2026 Busbar Inc and contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index da051d6..993fcda 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ - + # busbar-auth-github The GitHub-OAuth login module as a droppable busbar plugin: a cdylib exporting the auth C ABI (login-capable). Drop it in the plugins folder and add github to auth.chain with a browser_login block (the core injects client_secret). | kind | alias | crate | busbar | license | |---|---|---|---|---| -| `auth` | `github` | `busbar-auth-github-plugin` | 1.6.0 (pinned in `.busbar-ref`) | MIT | +| `auth` | `github` | `busbar-auth-github-plugin` | 1.6.0 (pinned in `.busbar-ref`) | Apache-2.0 | [![ci](https://github.com/GetBusbar/busbar-auth-github/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/GetBusbar/busbar-auth-github/actions/workflows/ci.yml) diff --git a/auth-github-plugin/tests/support/conformance_host.rs b/auth-github-plugin/tests/support/conformance_host.rs new file mode 100644 index 0000000..6791c09 --- /dev/null +++ b/auth-github-plugin/tests/support/conformance_host.rs @@ -0,0 +1,660 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors +// +// RENDERED by `busbar-release plugin heal busbar-auth-github` from GetBusbar/busbar-release +// template/conformance-host/, because this repo's declares file states network needs (`needs`: +// `http`); a hand edit is overwritten by the next sync. + +//! **THE SUITE'S HOST CONNECTOR, IN THIS PLUGIN'S OWN TEST** (ARCHITECT Q-P4-9 (d)). busbar keeps +//! only the neutral seam (`busbar_plugin_loader::conformance::Host`, the suite's `host:` and `tls:` +//! arguments); this adapter implements it the way busbar's composition root composes the process's +//! one connector — carrier -> [TLS] -> framer (`BUSBAR-1.6.0.md` THE DESIGN §5): +//! +//! * busbar's own `Connector` (`busbar-core-connector`, a dev-dependency at the pin), its framer +//! entries the transport doors this plugin's needs name (`tcp`, `http`), each loaded +//! through the loader's one load and opened as the root opens a transport door; +//! * the connector's own TLS (connsec), trusting the platform roots plus the suite's TEST trust +//! anchors (`tls:`), never handed to the plugin: no plugin holds a key, a certificate or a TLS +//! type (TRANSPORT-STACK (1)); +//! * a dial judge that admits loopback only: the suite dials the REAL local endpoints its settings +//! name, never the network; +//! * a parked read wakes the plugin's ticket through the leg dispatcher's conn waker. +//! +//! [`far_end`] is a TLS far end at [`FAR_END`] whose certificate chains to [`anchors`] (busbar's +//! test kit, `busbar_core_connector::test_support`): the plugin's `conformance.json` settings name +//! it, and its `answer` speaks the plugin's protocol. Its own tests, below, run with the suite: +//! a TLS upgrade verifies against the anchors, and is REFUSED with the anchors withheld (RED). +//! +//! The plugin's `tests/conformance.rs` mounts this file and names it: +//! +//! ```ignore +//! #[path = "support/conformance_host.rs"] +//! mod conformance_host; +//! +//! busbar_plugin_loader::conformance_suite! { +//! door: …, cdylib: …, inputs: include_str!("conformance.json"), +//! host: conformance_host::host, +//! tls: conformance_host::anchors(), +//! } +//! ``` +//! +//! with the dev-dependencies the rendered root `Cargo.toml` states (`{ workspace = true }`): +//! `busbar-core-connector`, `busbar-transport-tcp`, `busbar-transport-http`, `tokio`. + +#![allow(dead_code)] + +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs}; +use std::sync::{Arc, OnceLock}; +use std::time::{Duration, Instant}; + +use busbar_contract::abi::host::conn::connector::{ + DIRECTION_OUTBOUND, EGRESS_OPERATOR_INFRASTRUCTURE, +}; +use busbar_contract::abi::mechanism::call::{Blob, Outcome, BLOB_ABSENT}; +use busbar_contract::abi::mechanism::door::DoorFn; +use busbar_contract::abi::mechanism::lifecycle::{slot as life, OpenIn, OpenOut}; +use busbar_contract::abi::mechanism::rendering::{ReadBlob, ReadNeed}; +use busbar_contract::abi::sdk::door::{blank_in, blank_out}; +use busbar_contract::abi::transport::slot; +use busbar_contract::conn::{ + ConnError, ConnId, DeclaredConns, InstanceId, NeedId, OpenDesc, PieceKind, NO_TICKET, +}; +use busbar_contract::transport::EgressTrust; +use busbar_core_connector::framer::{Call, Crossed, DoorFacts, FramerDoor}; +use busbar_core_connector::registry::{Entry, Transports}; +use busbar_core_connector::test_support::{private_ca, ServerTls, StdServerSession, TestCa}; +use busbar_core_connector::{Connector, Judged, Verdict}; +use busbar_plugin_loader::dispatch::kinds::transport::{Transport, TransportFacts}; +use busbar_plugin_loader::dispatch::{ + load_linked, Bind, ConnTable, DispatchConfig, Dispatcher, Frame, InFrame, LinkedRow, NoSink, + OutFrame, Plugin, +}; + +/// Where [`far_end`] listens: loopback, a port of this repo's own. +pub const FAR_END: &str = "localhost:42841"; + +/// The framer doors the host serves, as the root links them: (claim, door). One row a line, +/// whatever the claims' lengths (a `vec!` would be reflowed by rustfmt). +#[allow(clippy::vec_init_then_push)] +fn doors() -> Vec<(&'static str, DoorFn)> { + use busbar_transport_http::linked::door as http_door; + use busbar_transport_tcp::linked::door as tcp_door; + // Each row is compiled against this repo's own pin of busbar-contract (the rendered `[patch]`), + // as busbar compiles the rows it links. + let mut doors: Vec<(&'static str, DoorFn)> = Vec::new(); + doors.push(("tcp", tcp_door)); + doors.push(("http", http_door)); + doors +} + +/// The suite's test CA: [`anchors`] is its certificate, [`far_end`] serves a leaf it signed. +fn ca() -> &'static TestCa { + static CA: OnceLock = OnceLock::new(); + CA.get_or_init(private_ca) +} + +/// THE SUITE'S TEST TRUST ANCHORS (PEM), for `conformance_suite!`'s `tls:`. +#[must_use] +pub fn anchors() -> &'static str { + &ca().ca_pem +} + +/// THE HOST CONNECTOR (`busbar_plugin_loader::conformance::Host`): one per leg. +/// +/// # Panics +/// The anchors do not parse, a transport door does not load or open, or the view does not compose. +#[must_use] +pub fn host(wake: Arc, anchors: Option<&str>) -> Arc { + // The reactor a socket a plugin opens from a dispatcher worker registers on, as the root + // installs it (`root::connector::io_reactor`): the connector's own I/O thread. + busbar_core_connector::io::install_process_reactor(io_reactor()); + let trust = EgressTrust { + extra_anchors: anchors.map(pem_certs).unwrap_or_default(), + ..EgressTrust::default() + }; + let tls = busbar_core_connector::tls::client::build_client_config(&trust) + .unwrap_or_else(|e| panic!("the host's connection security does not build: {e}")); + let view = Transports::new(entries()) + .unwrap_or_else(|e| panic!("the host's transport doors do not compose: {e}")); + Arc::new(Connector::serving( + view, + Arc::new(loopback), + Some(Arc::new(tls)), + wake, + )) +} + +/// The connector's I/O thread: a single-threaded runtime of its own whose reactor drives every +/// socket the plugin opens from a dispatcher worker. Built once. +fn io_reactor() -> tokio::runtime::Handle { + static IO: OnceLock = OnceLock::new(); + IO.get_or_init(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("the host's I/O runtime builds"); + let handle = rt.handle().clone(); + std::thread::Builder::new() + .name("conformance-host-io".into()) + .spawn(move || rt.block_on(std::future::pending::<()>())) + .expect("the host's I/O thread starts"); + handle + }) + .clone() +} + +/// THE DIAL JUDGE: a loopback address (a literal, or a name that resolves to one) and nothing else. +fn loopback(dest: &str, _class: u32, _done: Judged) -> Option> { + let refused = busbar_contract::abi::host::service::DEST_UNRESOLVABLE; + let at = dest + .to_socket_addrs() + .ok() + .and_then(|mut a| a.find(|a| a.ip().is_loopback())); + Some(at.ok_or(refused)) +} + +/// Every certificate in `pem`, DER. +fn pem_certs(pem: &str) -> Vec> { + let certs = busbar_core_connector::test_support::certs_from_pem(pem); + assert!( + !certs.is_empty(), + "the suite's trust anchors hold no certificate" + ); + certs +} + +/// The dispatcher the host's transport doors are adopted by. As the root's: its workers run inside +/// the connector's I/O runtime, and a CARRIER door moves its bytes over the process's one host I/O +/// (`io.*`, `busbar_core_connector::hostio::process`), the connector's. +fn transport_dispatcher() -> &'static Dispatcher { + static ONE: OnceLock = OnceLock::new(); + ONE.get_or_init(|| { + let d = Dispatcher::new(DispatchConfig::default()); + d.install_runtime(io_reactor()); + d.install_io(busbar_core_connector::hostio::process()); + d + }) +} + +/// The framer entries: each door loaded through the one loader and opened with no settings, as +/// the root opens a transport door that declares none it reads. +fn entries() -> Vec { + doors() + .into_iter() + .map(|(claim, door)| { + let bind = Bind { + instance: Arc::from(claim), + max_inflight_cap: 1024, + sink: Arc::new(NoSink), + dispatcher: transport_dispatcher().adopter(), + // A transport door is a framer the connector drives: it declares no need. + conns: ConnTable::NoNeeds, + }; + let plugin = LinkedRow::of(door) + .and_then(|row| load_linked::(&row, bind)) + .unwrap_or_else(|e| panic!("the host's transport `{claim}` does not load: {e}")); + Entry { + door: Arc::new(Opened::open(plugin, claim)), + alpn: Vec::new(), + } + }) + .collect() +} + +/// One transport door, opened, as the connector reaches it (the root's `doors::Dispatched`). +struct Opened { + plugin: Plugin, + facts: DoorFacts, +} + +impl Opened { + fn open(plugin: Plugin, claim: &str) -> Self { + let stated = plugin + .context::() + .cloned() + .unwrap_or_else(|| panic!("the host's transport `{claim}` states no transport tail")); + let mut i: OpenIn = blank_in(); + i.settings = Blob { + ptr: std::ptr::null(), + len: 0, + fmt: BLOB_ABSENT, + flags: 0, + }; + let mut f = Frame::new(i, blank_out::()); + let opened = plugin.call(life::OPEN, &mut f); + assert_eq!( + opened.outcome, + Outcome::Ready, + "the host's transport `{claim}` does not open" + ); + plugin + .ready(transport_dispatcher(), Duration::from_secs(10)) + .unwrap_or_else(|e| panic!("the host's transport `{claim}` is not ready: {e}")); + let facts = DoorFacts { + name: plugin.name().to_owned(), + claims: stated.claims, + role: stated.role, + composes_over: stated.composes_over, + ported: stated.ported, + status_rows: stated.status_rows, + duplex: stated + .upgrades + .iter() + .chain(&stated.sessions) + .copied() + .collect(), + }; + Self { plugin, facts } + } +} + +/// One crossing through the dispatcher: the host's `in`/`out` copied in, the answer copied back. +fn go(p: &Plugin, s: u32, i: &mut I, o: &mut O) -> Crossed { + let mut f = Frame::new(*i, *o); + let c = p.call(s, &mut f); + *i = f.input; + *o = f.out; + Crossed { + outcome: c.outcome, + error: c.error, + } +} + +/// One side of a carried connection: an inline ticket of the transport dispatcher, the one every +/// transport door is adopted by (its wakes route there), as the root's. +struct InlineSide(busbar_plugin_loader::dispatch::InlineTicket); + +impl busbar_core_connector::carrier::Side for InlineSide { + fn ticket(&self) -> busbar_contract::abi::mechanism::ticket::Ticket { + self.0.ticket() + } + + fn register(&self, waker: &std::task::Waker) { + self.0.register(waker); + } +} + +/// One INLINE crossing on `ticket` (a RESUME when `resume`): the host's `in`/`out` copied in, the +/// answer copied back. +fn go_inline( + p: &Plugin, + ticket: busbar_contract::abi::mechanism::ticket::Ticket, + resume: bool, + s: u32, + i: &mut I, + o: &mut O, +) -> Crossed { + let mut f = Frame::new(*i, *o); + let c = p.call_inline(ticket, resume, s, &mut f); + *i = f.input; + *o = f.out; + Crossed { + outcome: c.outcome, + error: c.error, + } +} + +impl FramerDoor for Opened { + fn facts(&self) -> &DoorFacts { + &self.facts + } + + fn side(&self) -> Option> { + if self.facts.role != busbar_contract::abi::transport::ROLE_CARRIER { + return None; + } + transport_dispatcher() + .inline_ticket() + .map(|t| Box::new(InlineSide(t)) as Box) + } + + fn carry( + &self, + side: &dyn busbar_core_connector::carrier::Side, + resume: bool, + call: busbar_core_connector::carrier::Carry<'_>, + ) -> Crossed { + use busbar_core_connector::carrier::Carry; + let (p, t) = (&self.plugin, side.ticket()); + match call { + Carry::Listen(i, o) => go_inline(p, t, resume, slot::LISTEN, i, o), + Carry::Accept(i, o) => go_inline(p, t, resume, slot::ACCEPT, i, o), + Carry::Dial(i, o) => go_inline(p, t, resume, slot::DIAL, i, o), + Carry::Read(i, o) => go_inline(p, t, resume, slot::READ, i, o), + Carry::Write(i, o) => go_inline(p, t, resume, slot::WRITE, i, o), + Carry::Flush(i, o) => go_inline(p, t, resume, slot::FLUSH, i, o), + Carry::Shut(i, o) => go_inline(p, t, resume, slot::SHUT, i, o), + Carry::Arrival(i, o) => go_inline(p, t, resume, slot::ARRIVAL, i, o), + // `cancel` is made on no ticket: the ticket it cancels rides its `in`. + Carry::Cancel(i, o) => go(p, life::CANCEL, i, o), + } + } + + fn cross(&self, call: Call<'_>) -> Crossed { + let p = &self.plugin; + match call { + Call::Locate(i, o) => go(p, slot::LOCATE, i, o), + Call::Begin(i, o) => go(p, slot::BEGIN, i, o), + Call::Ingest(i, o) => go(p, slot::INGEST, i, o), + Call::Emit(i, o) => go(p, slot::EMIT, i, o), + Call::Encode(i, o) => go(p, slot::ENCODE, i, o), + Call::Refuse(i, o) => go(p, slot::REFUSE, i, o), + Call::Finish(i, o) => go(p, slot::FINISH, i, o), + Call::Detach(i, o) => go(p, slot::DETACH, i, o), + Call::Adopt(i, o) => go(p, slot::ADOPT, i, o), + Call::Timer(i, o) => go(p, slot::TIMER, i, o), + } + } +} + +// ---- the far end ---- + +/// What a far end answers a connection: given every byte it has read so far, the bytes to send +/// back and close (`Some`), or `None` to read on. +pub type Answer = fn(&[u8]) -> Option>; + +/// THE SUITE'S TLS FAR END at [`FAR_END`] (started once; later calls are no-ops): every connection's +/// handshake is busbar's test kit's, with a `localhost` certificate [`anchors`] trusts, and its +/// bytes are answered by `answer`. +/// +/// # Panics +/// [`FAR_END`] cannot be bound. +pub fn far_end(answer: Answer) { + static STARTED: OnceLock<()> = OnceLock::new(); + STARTED.get_or_init(|| { + let listener = TcpListener::bind(FAR_END) + .unwrap_or_else(|e| panic!("the suite's far end cannot bind {FAR_END}: {e}")); + serve(listener, answer); + }); +} + +/// Serve `listener` over TLS with a leaf the test CA signed, answering each connection by `answer`. +fn serve(listener: TcpListener, answer: Answer) { + let tls = ServerTls::new(&[ca().leaf_der.clone()], &ca().key_der, None, &[]) + .unwrap_or_else(|e| panic!("the suite's far end has no TLS identity: {e}")); + std::thread::spawn(move || { + for tcp in listener.incoming() { + let Ok(tcp) = tcp else { return }; + let tls = tls.clone(); + std::thread::spawn(move || { + if let Ok(mut s) = tls.accept_std(tcp) { + if s.handshake_ok() { + reply(&mut s, answer); + } + } + }); + } + }); +} + +fn reply(s: &mut StdServerSession, answer: Answer) { + let mut seen = Vec::new(); + let mut buf = [0_u8; 4096]; + loop { + if let Some(out) = answer(&seen) { + let _ = s.write_all(&out); + let _ = s.flush(); + s.close(); + return; + } + match s.read(&mut buf) { + Ok(0) | Err(_) => return, + Ok(n) => seen.extend_from_slice(&buf[..n]), + } + } +} + +/// A far end's cleartext negotiation before TLS (a protocol's own StartTLS: Postgres's +/// `SSLRequest` answered `S`, LDAP's StartTLS extended operation, ...): `true` = go on to TLS. +pub type Preamble = fn(&mut TcpStream) -> bool; + +/// THE SUITE'S TLS FRONT at [`FAR_END`] (started once; later calls are no-ops): each connection's +/// cleartext `preamble` runs, then the TLS handshake (busbar's test kit, a `localhost` certificate +/// [`anchors`] trusts), then its bytes are carried both ways to the REAL backend at `upstream` +/// (the plugin's live service, in the clear on loopback). What it proves: the plugin's own +/// connection is secured by the HOST's TLS against the suite's anchors. +/// +/// # Panics +/// [`FAR_END`] cannot be bound. +pub fn tls_front(preamble: Preamble, upstream: &'static str) { + static STARTED: OnceLock<()> = OnceLock::new(); + STARTED.get_or_init(|| { + let listener = TcpListener::bind(FAR_END) + .unwrap_or_else(|e| panic!("the suite's TLS front cannot bind {FAR_END}: {e}")); + let tls = ServerTls::new(&[ca().leaf_der.clone()], &ca().key_der, None, &[]) + .unwrap_or_else(|e| panic!("the suite's TLS front has no TLS identity: {e}")); + std::thread::spawn(move || { + for tcp in listener.incoming() { + let Ok(tcp) = tcp else { return }; + let tls = tls.clone(); + std::thread::spawn(move || front(tcp, &tls, preamble, upstream)); + } + }); + }); +} + +/// One connection through the TLS front: preamble, handshake, then both directions carried until +/// either side ends. +fn front(mut tcp: TcpStream, tls: &ServerTls, preamble: Preamble, upstream: &str) { + if !preamble(&mut tcp) { + return; + } + let Ok(ctl) = tcp.try_clone() else { return }; + let Ok(mut s) = tls.accept_std(tcp) else { + return; + }; + if !s.handshake_ok() { + return; + } + let Ok(mut up) = TcpStream::connect(upstream) else { + return; + }; + let tick = Some(Duration::from_millis(2)); + if ctl.set_read_timeout(tick).is_err() || up.set_read_timeout(tick).is_err() { + return; + } + let idle = |e: &std::io::Error| { + matches!( + e.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) + }; + let mut buf = [0_u8; 16 * 1024]; + loop { + match s.read(&mut buf) { + Ok(0) => return, + Ok(n) => { + if up.write_all(&buf[..n]).is_err() { + return; + } + } + Err(e) if idle(&e) => {} + Err(_) => return, + } + match up.read(&mut buf) { + Ok(0) => { + s.close(); + return; + } + Ok(n) => { + if s.write_all(&buf[..n]).and_then(|()| s.flush()).is_err() { + return; + } + } + Err(e) if idle(&e) => {} + Err(_) => return, + } + } +} + +// ---- the host's own proof, run with the suite ---- + +const OWNER: InstanceId = InstanceId(7); + +fn need(transport: &str, class: u32) -> ReadNeed { + ReadNeed { + direction: DIRECTION_OUTBOUND, + egress_class: class, + transport: transport.to_owned(), + auth: String::new(), + target_from: String::new(), + trust_from: String::new(), + details: ReadBlob { + fmt: 0, + flags: 0, + bytes: Vec::new(), + }, + timeout_ms: 0, + } +} + +/// `f` until it answers other than PENDING, as a plugin's re-entries on its ticket would. +fn settle(mut f: impl FnMut() -> Result) -> Result { + let until = Instant::now() + Duration::from_secs(10); + loop { + match f() { + Err(ConnError::Pending) if Instant::now() < until => { + std::thread::sleep(Duration::from_millis(2)); + } + answered => return answered, + } + } +} + +/// Read `id` to its completion (or `want` body bytes): its fields pieces' count and its body. +fn drain(c: &dyn DeclaredConns, id: ConnId, want: usize) -> (usize, Vec) { + let (mut fields, mut body) = (0, Vec::new()); + let mut buf = [0_u8; 256]; + while body.len() < want { + let Ok(p) = settle(|| c.read(OWNER, id, NO_TICKET, &mut buf)) else { + break; + }; + match p.kind { + PieceKind::Fields => fields += 1, + PieceKind::Body => body.extend_from_slice(&buf[..p.len]), + PieceKind::Completion => break, + PieceKind::HookReply => {} + } + } + (fields, body) +} + +/// Five bytes back for five bytes in: the TLS echo the host's own proof dials. +fn echo(seen: &[u8]) -> Option> { + (seen.len() >= 5).then(|| seen[..5].to_vec()) +} + +/// A raw `tcp` stream to a TLS echo, upgraded through the host's TLS: what came back. +fn upgraded_echo(c: &dyn DeclaredConns, far: &str) -> Result, ConnError> { + c.declare( + OWNER, + NeedId(0), + &need("tcp", EGRESS_OPERATOR_INFRASTRUCTURE), + None, + None, + )?; + let id = settle(|| { + c.open( + OWNER, + NeedId(0), + &OpenDesc { + target: far, + timeout_ms: 5_000, + ..OpenDesc::default() + }, + ) + })?; + settle(|| c.upgrade_secure(OWNER, id, Some("localhost"), None, false, NO_TICKET))?; + let mut at = 0; + while at < 5 { + at += settle(|| c.write(OWNER, id, &b"hello"[at..], false, false))?; + } + let (_, body) = drain(c, id, 5); + let _ = c.close(OWNER, id); + Ok(body) +} + +/// A TLS echo on a port of its own; its address. +fn tls_echo() -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("a local listener"); + let at = listener.local_addr().expect("its address").to_string(); + serve(listener, echo); + at +} + +/// THE HOST'S TLS TRUSTS THE SUITE'S TEST ANCHORS: a stream to a far end whose certificate chains +/// to the test CA is upgraded and round-trips (the anchors went to the host, never to the plugin). +#[test] +fn the_hosts_tls_upgrade_verifies_against_the_suites_test_anchors() { + let c = host(Arc::new(|_| {}), Some(anchors())); + assert_eq!( + upgraded_echo(c.as_ref(), &tls_echo()), + Ok(b"hello".to_vec()) + ); +} + +/// RED: the same upgrade with the anchors withheld is refused: the platform roots alone do not +/// trust the test CA. +#[test] +fn red_with_the_anchors_withheld_the_hosts_tls_upgrade_is_refused() { + let c = host(Arc::new(|_| {}), None); + let answer = upgraded_echo(c.as_ref(), &tls_echo()); + assert!(answer.is_err(), "upgraded over an untrusted CA: {answer:?}"); +} + +/// AN `http` NEED THROUGH THE HOST'S FRAMER: declared on the host, opened at a local HTTP +/// listener's URL, the request goes out framed by the linked http door over the tcp carrier and the +/// answer comes back as one head and its body. +#[test] +fn an_http_need_reaches_a_local_http_listener_through_the_hosts_framer() { + use busbar_contract::abi::host::conn::connector::EGRESS_LOOPBACK_ALLOWED; + + let listener = TcpListener::bind("127.0.0.1:0").expect("a local listener"); + let at = listener.local_addr().expect("its address"); + let far = std::thread::spawn(move || { + let (mut s, _) = listener.accept().expect("the host dials it"); + let mut seen = Vec::new(); + let mut buf = [0_u8; 512]; + while !seen.windows(4).any(|w| w == b"\r\n\r\n") { + let n = s.read(&mut buf).expect("the request"); + assert!(n > 0, "the request ended early"); + seen.extend_from_slice(&buf[..n]); + } + s.write_all(b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello") + .expect("the answer"); + String::from_utf8_lossy(&seen).into_owned() + }); + + let c = host(Arc::new(|_| {}), None); + assert!(c.serves_scheme("http"), "the host serves http"); + c.declare( + OWNER, + NeedId(0), + &need("http", EGRESS_LOOPBACK_ALLOWED), + None, + None, + ) + .expect("the http need is declared"); + let target = format!("http://{at}/v1/probe"); + let id = settle(|| { + c.open( + OWNER, + NeedId(0), + &OpenDesc { + target: &target, + method: b"GET", + head_target: b"/v1/probe", + timeout_ms: 5_000, + ..OpenDesc::default() + }, + ) + }) + .expect("the need opens through the framer"); + let (fields, body) = drain(c.as_ref(), id, 5); + let seen = far.join().expect("the listener answered"); + assert!(seen.starts_with("GET /v1/probe HTTP/1.1\r\n"), "{seen}"); + assert_eq!(fields, 1, "one head"); + assert_eq!(body, b"hello"); + c.close(OWNER, id).expect("closed"); +} diff --git a/busbar-auth-github-plugin/tests/conformance.rs b/busbar-auth-github-plugin/tests/conformance.rs index 8ae50e7..d2d88c3 100644 --- a/busbar-auth-github-plugin/tests/conformance.rs +++ b/busbar-auth-github-plugin/tests/conformance.rs @@ -97,7 +97,10 @@ fn bind(d: &Dispatcher) -> Bind { max_inflight_cap: 64, sink: Arc::new(NoSink), dispatcher: d.adopter(), - conns: None, + // The door declares its `http` need (the login's token exchange); this transcript binds it + // as a PROBE, with no connection table (nothing opened to the network), as it bound before + // the loader named the table. + conns: busbar_plugin_loader::dispatch::ConnTable::Probe, } } diff --git a/busbar-auth-github-plugin/tests/e2e.rs b/busbar-auth-github-plugin/tests/e2e.rs index 1296329..529a6f1 100644 --- a/busbar-auth-github-plugin/tests/e2e.rs +++ b/busbar-auth-github-plugin/tests/e2e.rs @@ -124,7 +124,9 @@ fn pack_github(pack_bin: &std::path::Path, so: &std::path::Path, out: &std::path "--kind", "auth", "--version", - "0.0.0-e2e", + // The release version is the plugin's own, the one its Statement states (the pack tool + // refuses any other). + env!("CARGO_PKG_VERSION"), "--publisher", "busbar", "--description", diff --git a/busbar-auth-github/declares.json b/busbar-auth-github/declares.json index 344abf0..0aaccdd 100644 --- a/busbar-auth-github/declares.json +++ b/busbar-auth-github/declares.json @@ -2,5 +2,8 @@ "contract_abi": { "min": 3, "max": 3 - } + }, + "needs": [ + "http" + ] } diff --git a/clippy.toml b/clippy.toml index fd71967..521f225 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/; a hand edit is +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/; a hand edit is # overwritten by the next sync. CI (busbar's plugin-ci.yml at the pin) runs clippy with -D warnings on # the 1.98.0 toolchain; the MSRV clippy judges against is each crate's own `rust-version`. too-many-arguments-threshold = 7 diff --git a/codecov.yml b/codecov.yml index f1b9b9a..3b2cc9f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/. +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/. # Codecov is INFORMATIONAL, never a gate: the correctness gate is the `ci` workflow (busbar's # plugin-ci.yml at the pin). Every status is informational, there are no PR comments, and test-only paths # plus the busbar checkout the end-to-end tests build are excluded from the percentage. diff --git a/deny.toml b/deny.toml index a76e4f0..1f57efe 100644 --- a/deny.toml +++ b/deny.toml @@ -1,5 +1,5 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/ and busbar's -# `.github/fleet/deps.toml` at 8fc3b1b2a6d123575a77ce505e501b595fb24cef; a hand edit is overwritten by the next sync. CI runs +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/ and busbar's +# `.github/fleet/deps.toml` at 41fd7c5f386fa098890c031721f19bfca903f225; a hand edit is overwritten by the next sync. CI runs # `cargo deny check` on it (busbar's plugin-ci.yml at the pin). The license allowlist is busbar's own; # the one reviewed git source is busbar itself, at the pin. [graph] @@ -7,6 +7,15 @@ all-features = false [advisories] version = 2 +ignore = [ + # RUSTSEC-2024-0449: `unsafe-libyaml` 0.2.11 is unmaintained. It reaches this plugin only through + # busbar-contract at the pin (busbar-contract -> serde_yaml_ng 0.10 -> unsafe-libyaml), the same + # line busbar's own Cargo.lock carries, and busbar's own deny.toml accepts it with this reason: + # "unmaintained", not a security hole, and no safe swap exists now (a different YAML parser + # risks 1.5.5 config-parse parity). Re-evaluate with busbar's row when serde_yaml_ng moves off + # unsafe-libyaml. Tracking: https://rustsec.org/advisories/RUSTSEC-2024-0449.html + "RUSTSEC-2024-0449", +] [licenses] version = 2 @@ -56,6 +65,8 @@ deny = [ { crate = "a2a-lf:>=0.4.0", reason = "busbar resolves a2a-lf to 0.3.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "a2a-pb:<0.2.0", reason = "busbar resolves a2a-pb to 0.2.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "a2a-pb:>=0.3.0", reason = "busbar resolves a2a-pb to 0.2.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "aes:<0.8.0", reason = "busbar resolves aes to 0.8.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "aes:>=0.9.0", reason = "busbar resolves aes to 0.8.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "arc-swap:<1.0.0", reason = "busbar resolves arc-swap to 1.9.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "arc-swap:>=2.0.0", reason = "busbar resolves arc-swap to 1.9.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "async-trait:<0.1.0", reason = "busbar resolves async-trait to 0.1.92; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -88,8 +99,6 @@ deny = [ { crate = "h2:>=0.5.0", reason = "busbar resolves h2 to 0.4.18; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hex:<0.4.0", reason = "busbar resolves hex to 0.4.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hex:>=0.5.0", reason = "busbar resolves hex to 0.4.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hmac:<0.13.0", reason = "busbar resolves hmac to 0.13.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hmac:>=0.14.0", reason = "busbar resolves hmac to 0.13.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "http:<1.0.0", reason = "busbar resolves http to 1.4.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "http:>=2.0.0", reason = "busbar resolves http to 1.4.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "http-body:<1.0.0", reason = "busbar resolves http-body to 1.1.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -100,10 +109,10 @@ deny = [ { crate = "httpdate:>=2.0.0", reason = "busbar resolves httpdate to 1.0.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper:<1.0.0", reason = "busbar resolves hyper to 1.11.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper:>=2.0.0", reason = "busbar resolves hyper to 1.11.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hyper-rustls:<0.27.0", reason = "busbar resolves hyper-rustls to 0.27.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "hyper-rustls:>=0.28.0", reason = "busbar resolves hyper-rustls to 0.27.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper-util:<0.1.0", reason = "busbar resolves hyper-util to 0.1.20; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "hyper-util:>=0.2.0", reason = "busbar resolves hyper-util to 0.1.20; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "idna_adapter:<1.0.0", reason = "busbar resolves idna_adapter to 1.1.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "idna_adapter:>=2.0.0", reason = "busbar resolves idna_adapter to 1.1.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "indexmap:<2.0.0", reason = "busbar resolves indexmap to 2.14.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "indexmap:>=3.0.0", reason = "busbar resolves indexmap to 2.14.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "jsonschema:<0.49.0", reason = "busbar resolves jsonschema to 0.49.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -140,8 +149,6 @@ deny = [ { crate = "reqwest:>=0.13.0", reason = "busbar resolves reqwest to 0.12.28; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "ring:<0.17.0", reason = "busbar resolves ring to 0.17.14; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "ring:>=0.18.0", reason = "busbar resolves ring to 0.17.14; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "rmcp:<3.0.0", reason = "busbar resolves rmcp to 3.1.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "rmcp:>=4.0.0", reason = "busbar resolves rmcp to 3.1.2; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "rustls:<0.23.0", reason = "busbar resolves rustls to 0.23.45; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "rustls:>=0.24.0", reason = "busbar resolves rustls to 0.23.45; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "rustls-pki-types:<1.0.0", reason = "busbar resolves rustls-pki-types to 1.15.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -156,14 +163,16 @@ deny = [ { crate = "serde_urlencoded:>=0.8.0", reason = "busbar resolves serde_urlencoded to 0.7.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "serde_yaml_ng:<0.10.0", reason = "busbar resolves serde_yaml_ng to 0.10.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "serde_yaml_ng:>=0.11.0", reason = "busbar resolves serde_yaml_ng to 0.10.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "sha2:<0.10.0", reason = "busbar resolves sha2 to 0.10.9, 0.11.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, - { crate = "sha2:>=0.12.0", reason = "busbar resolves sha2 to 0.10.9, 0.11.0; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "sha2:<0.10.0", reason = "busbar resolves sha2 to 0.10.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "sha2:>=0.11.0", reason = "busbar resolves sha2 to 0.10.9; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "smallvec:<1.0.0", reason = "busbar resolves smallvec to 1.16.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "smallvec:>=2.0.0", reason = "busbar resolves smallvec to 1.16.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "socket2:<0.6.0", reason = "busbar resolves socket2 to 0.6.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "socket2:>=0.7.0", reason = "busbar resolves socket2 to 0.6.4; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "sonic-rs:<0.5.0", reason = "busbar resolves sonic-rs to 0.5.10; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "sonic-rs:>=0.6.0", reason = "busbar resolves sonic-rs to 0.5.10; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "str0m:<0.24.0", reason = "busbar resolves str0m to 0.24.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, + { crate = "str0m:>=0.25.0", reason = "busbar resolves str0m to 0.24.1; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "syn:<2.0.0", reason = "busbar resolves syn to 2.0.117, 3.0.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "syn:>=4.0.0", reason = "busbar resolves syn to 2.0.117, 3.0.3; a plugin may not pull a second version (OWNER 2026-10-02)" }, { crate = "tar:<0.4.0", reason = "busbar resolves tar to 0.4.46; a plugin may not pull a second version (OWNER 2026-10-02)" }, @@ -212,16 +221,8 @@ skip = [ { crate = "base64@0.23.1", reason = "busbar's own lock holds this duplicate" }, { crate = "bit-vec@0.8.0", reason = "busbar's own lock holds this duplicate" }, { crate = "bit-vec@0.9.1", reason = "busbar's own lock holds this duplicate" }, - { crate = "block-buffer@0.10.4", reason = "busbar's own lock holds this duplicate" }, - { crate = "block-buffer@0.12.0", reason = "busbar's own lock holds this duplicate" }, - { crate = "const-oid@0.10.2", reason = "busbar's own lock holds this duplicate" }, - { crate = "const-oid@0.9.6", reason = "busbar's own lock holds this duplicate" }, { crate = "cpufeatures@0.2.17", reason = "busbar's own lock holds this duplicate" }, { crate = "cpufeatures@0.3.0", reason = "busbar's own lock holds this duplicate" }, - { crate = "crypto-common@0.1.7", reason = "busbar's own lock holds this duplicate" }, - { crate = "crypto-common@0.2.2", reason = "busbar's own lock holds this duplicate" }, - { crate = "digest@0.10.7", reason = "busbar's own lock holds this duplicate" }, - { crate = "digest@0.11.3", reason = "busbar's own lock holds this duplicate" }, { crate = "foldhash@0.1.5", reason = "busbar's own lock holds this duplicate" }, { crate = "foldhash@0.2.0", reason = "busbar's own lock holds this duplicate" }, { crate = "getrandom@0.2.17", reason = "busbar's own lock holds this duplicate" }, @@ -241,8 +242,6 @@ skip = [ { crate = "rand_core@0.10.1", reason = "busbar's own lock holds this duplicate" }, { crate = "rand_core@0.6.4", reason = "busbar's own lock holds this duplicate" }, { crate = "rand_core@0.9.5", reason = "busbar's own lock holds this duplicate" }, - { crate = "sha2@0.10.9", reason = "busbar's own lock holds this duplicate" }, - { crate = "sha2@0.11.0", reason = "busbar's own lock holds this duplicate" }, { crate = "syn@2.0.117", reason = "busbar's own lock holds this duplicate" }, { crate = "syn@3.0.3", reason = "busbar's own lock holds this duplicate" }, { crate = "windows-sys@0.52.0", reason = "busbar's own lock holds this duplicate" }, @@ -253,4 +252,4 @@ skip = [ unknown-registry = "deny" unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = ["https://github.com/GetBusbar/busbar"] +allow-git = ["https://github.com/GetBusbar/busbar", "https://www.github.com/GetBusbar/busbar", "https://github.com/GetBusbar/busbar-transport-http", "https://github.com/GetBusbar/busbar-transport-tcp"] diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 42c3341..1a37c3e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ -# RENDERED by `busbar-release plugin sync` from GetBusbar/busbar-release template/. The toolchain is +# RENDERED by `busbar-release plugin heal` from GetBusbar/busbar-release template/. The toolchain is # busbar's own pin (its rust-toolchain.toml at the pin), so a plugin and the core that links it compile # with one compiler. [toolchain]