diff --git a/.clang-tidy b/.clang-tidy
index e319898..28173cf 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -3,6 +3,11 @@
# - performance-enum-size: narrowing public enums would change the ABI for little gain.
# - optin.performance.Padding: public parameter structs are ordered for readability and
# aggregate initialization, not layout.
+# - modernize: the clang-tidy 18 checks are listed by name, so a newer clang-tidy cannot add
+# errors CI never sees. Left out: use-trailing-return-type and avoid-c-arrays (style this
+# code base does not follow; C arrays appear only at C API boundaries), use-nodiscard
+# ([[nodiscard]] is placed deliberately), and use-auto (local types are spelled out; see
+# tools/cpp_auto_audit.py).
Checks: >
-*,
bugprone-*,
@@ -12,19 +17,58 @@ Checks: >
performance-*,
-performance-enum-size,
portability-*,
- modernize-use-nullptr,
- modernize-use-override,
+ modernize-avoid-bind,
+ modernize-concat-nested-namespaces,
+ modernize-deprecated-headers,
+ modernize-deprecated-ios-base-aliases,
+ modernize-loop-convert,
+ modernize-macro-to-enum,
+ modernize-make-shared,
+ modernize-make-unique,
+ modernize-pass-by-value,
+ modernize-raw-string-literal,
+ modernize-redundant-void-arg,
+ modernize-replace-auto-ptr,
+ modernize-replace-disallow-copy-and-assign-macro,
+ modernize-replace-random-shuffle,
+ modernize-return-braced-init-list,
+ modernize-shrink-to-fit,
+ modernize-type-traits,
+ modernize-unary-static-assert,
+ modernize-use-bool-literals,
+ modernize-use-constraints,
+ modernize-use-default-member-init,
+ modernize-use-emplace,
modernize-use-equals-default,
modernize-use-equals-delete,
- modernize-redundant-void-arg,
+ modernize-use-noexcept,
+ modernize-use-nullptr,
+ modernize-use-override,
+ modernize-use-starts-ends-with,
+ modernize-use-std-numbers,
+ modernize-use-std-print,
+ modernize-use-transparent-functors,
+ modernize-use-uncaught-exceptions,
modernize-use-using,
+ readability-braces-around-statements,
+ readability-const-return-type,
readability-container-size-empty,
- readability-redundant-string-init,
+ readability-else-after-return,
+ readability-inconsistent-declaration-parameter-name,
readability-make-member-function-const,
- misc-unused-using-decls,
+ readability-misleading-indentation,
+ readability-redundant-control-flow,
+ readability-redundant-member-init,
+ readability-redundant-smartptr-get,
+ readability-redundant-string-init,
+ readability-simplify-boolean-expr,
misc-definitions-in-headers,
- cppcoreguidelines-virtual-class-destructor,
- cppcoreguidelines-slicing
+ misc-unused-using-decls,
+ cppcoreguidelines-init-variables,
+ cppcoreguidelines-missing-std-forward,
+ cppcoreguidelines-prefer-member-initializer,
+ cppcoreguidelines-slicing,
+ cppcoreguidelines-virtual-class-destructor
WarningsAsErrors: '*'
HeaderFilterRegex: '.*/(include/kalshi(/detail)?|src(/api|/auth|/http|/models|/ws)?)/[^/]+$'
FormatStyle: file
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..e70390c
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,57 @@
+name: Docs
+
+# Builds the Doxygen API reference on pull requests that touch it. Deploys it
+# to GitHub Pages when dispatched, which release.yml does after it publishes a
+# release, so only released headers are documented.
+on:
+ pull_request:
+ paths:
+ - "include/**"
+ - "docs/**"
+ - "*.md"
+ - "Doxyfile"
+ - ".github/workflows/docs.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: docs-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - name: Install Doxygen
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends doxygen
+ - name: Build the API reference
+ run: make docs
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
+ if: github.event_name == 'workflow_dispatch'
+ with:
+ path: build-docs/html
+
+ deploy:
+ if: github.event_name == 'workflow_dispatch'
+ needs: build
+ # One deploy at a time, in order, so an older release never lands last.
+ concurrency:
+ group: pages-deploy
+ cancel-in-progress: false
+ runs-on: ubuntu-24.04
+ timeout-minutes: 10
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 05bc40e..c3320ec 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -73,3 +73,10 @@ jobs:
run: >-
gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME"
--notes-file "$RUNNER_TEMP/notes.md" --verify-tag
+
+ # Events from this workflow's token start no other workflows, except a
+ # dispatch, so the docs deploy is requested explicitly.
+ - name: Publish the API reference
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: gh workflow run docs.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME"
diff --git a/.gitignore b/.gitignore
index acbe64d..ea9757f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,3 +49,6 @@ __pycache__/
*~
.DS_Store
Thumbs.db
+
+# Local Python environment for tools/
+.venv/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e68b9d5..4a7254c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Added
+- An API reference built from the headers with `make docs` and published to
+ for each release.
- `WebSocketClient` covers every channel and command in Kalshi's AsyncAPI
document: all 13 channels, `update_subscription` for markets, snapshots, CF
Benchmarks indices, and Pyth underlyings, and `list_subscriptions`. Message
@@ -61,6 +63,10 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Changed
+- libwebsockets is found through its own CMake package when one is installed
+ (vcpkg, Homebrew, and Ubuntu ship one), with pkg-config as the fallback on
+ every platform. The installed `kalshiConfig.cmake` finds it the same way and
+ reports kalshi-cpp as not found, instead of stopping, when it is missing.
- CMake 3.31 or newer is required when Glaze is fetched (Glaze's own
minimum), or 3.21 with `KALSHI_USE_SYSTEM_GLAZE=ON`. Glaze 8.3 or newer is
enforced at compile time.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b5cb86a..135f0da 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -95,14 +95,8 @@ configure_file(
find_package(OpenSSL 3.0 REQUIRED)
find_package(CURL REQUIRED)
-if(WIN32)
- find_package(libwebsockets CONFIG REQUIRED)
- set(KALSHI_WEBSOCKETS_TARGET websockets_shared)
-else()
- find_package(PkgConfig REQUIRED)
- pkg_check_modules(WEBSOCKETS REQUIRED IMPORTED_TARGET libwebsockets)
- set(KALSHI_WEBSOCKETS_TARGET PkgConfig::WEBSOCKETS)
-endif()
+include(cmake/KalshiWebsockets.cmake)
+kalshi_find_websockets(TRUE)
# Glaze is a private, header-only build dependency; installed headers never
# include it. src/json.hpp rejects versions older than 8.3 at compile time,
@@ -178,6 +172,7 @@ install(EXPORT kalshiTargets
NAMESPACE kalshi::
DESTINATION ${KALSHI_INSTALL_CMAKEDIR})
install(FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/cmake/KalshiWebsockets.cmake
${CMAKE_CURRENT_BINARY_DIR}/kalshiConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/kalshiConfigVersion.cmake
DESTINATION ${KALSHI_INSTALL_CMAKEDIR})
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a825cd9..381a2f5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,113 +1,104 @@
-# Contributing to kalshi-cpp
+# Contributing
-Thanks for your interest in contributing. This guide covers the local
-build flow, code style expectations, and PR conventions used in this
-repo. For security-vulnerability reports, see [SECURITY.md](SECURITY.md)
-and do not open a public issue for those.
+Report security problems privately as described in [SECURITY.md](SECURITY.md),
+not in a public issue.
-## Getting started
+## Set up
```bash
git clone https://github.com/Reddimus/kalshi-cpp.git
cd kalshi-cpp
-
-# One-time: install build deps (Ubuntu 24.04 example)
-sudo apt install -y build-essential cmake clang-format \
- libssl-dev libcurl4-openssl-dev libwebsockets-dev
-
-make build # CMake configure + Release build
-make test # Run unit tests (ctest)
```
-macOS uses Homebrew (`brew install openssl curl libwebsockets`),
-Windows uses vcpkg (the CI workflow has the exact invocations).
-
-## Development workflow
+On Ubuntu 24.04, whose apt CMake is older than the 3.31 this build needs:
```bash
-make debug # Debug build in build-debug/
-make test # Build and run the tests
-make sanitize # ASan + UBSan
-make tsan # ThreadSanitizer
-make tidy # clang-tidy build
-make lint # clang-format 18, cpp_auto_audit, generated-code check
-make codegen # Regenerate the REST client from spec/openapi.yaml
-make format # Apply clang-format in place
-make bench # Google Benchmark suite
-make coverage # lcov report (needs lcov)
-make clean # Remove build directories
+sudo apt install build-essential ninja-build pkg-config clang-format-18 python3-yaml \
+ pipx libssl-dev libcurl4-openssl-dev libwebsockets-dev
+pipx install cmake && pipx ensurepath # then open a new shell
+make test
```
-Run `make lint` before pushing; CI runs the same checks. It needs
-clang-format 18 and PyYAML (`python3 -m pip install pyyaml`).
+On macOS, Homebrew's `llvm@18` provides clang-format 18 without putting it on
+`PATH`, and PyYAML goes in a virtual environment:
-## Generated code
-
-`tools/codegen/generate.py` writes the REST client from `spec/openapi.yaml`
-and the WebSocket messages from `spec/asyncapi.yaml`: `include/kalshi/api.hpp`,
-`models.hpp`, and `ws_models.hpp`, `src/api/operations/`, `src/api/validate.hpp`,
-`src/models/json_meta.hpp`, `src/ws/wire.hpp`, `tests/test_operation_routes.cpp`,
-`tests/test_ws_messages.cpp`, `docs/operations.md`, and `docs/channels.md`.
-Change the generator or a spec, run `make codegen`, and commit both. CI rejects stale
-output. `docs/research.md` explains how to refresh the spec.
+```bash
+brew install cmake ninja pkg-config openssl libwebsockets llvm@18
+python3 -m venv .venv && .venv/bin/pip install pyyaml
+export CLANG_FORMAT="$(brew --prefix llvm@18)/bin/clang-format" PYTHON=.venv/bin/python
+make test lint
+```
-## Code style
+Windows builds use vcpkg; the `build-windows` job in `.github/workflows/ci.yml`
+has the steps.
-- **C++23** features encouraged: `std::expected` for all
- error-returning operations, no exceptions in the public API.
-- **No `auto`** for local variable declarations. Spell out the type so
- reviewers can verify intent without IDE help. Carve-outs:
- - Structured bindings: `auto& [k, v] = ...`
- - Lambda closures: `auto callback = ...`
- - Iterator-like results: `auto it = container.find(...)`
-- **Formatting**: `.clang-format` (LLVM base, tabs, 100-col limit).
- `make format` applies it.
-- **Includes**: project headers first, then system headers
- (enforced by clang-format `SortIncludes`).
-- **JSON**: use Glaze for structured payloads. Keep hand-rolled scanners limited
- to measured hot paths with focused parser and benchmark tests.
-
-## PR conventions
-
-- Branch names: `feat/...`, `fix/...`, `docs/...`, `chore/...`,
- `test/...`, `build/...`, `ci/...`, `refactor/...`.
-- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/):
- `(): `, for example:
- `fix(ws): null-guard moved-from accessors`.
-- Squash + delete branch on merge. PR titles become the squash commit
- subject, so write them clearly.
-- Update `CHANGELOG.md` under `## [Unreleased]` for any user-visible
- change (new API, fix that consumers will notice, dep bump). Use the
- Keep-a-Changelog sub-headers: Added / Changed / Fixed / Removed.
-- CI must pass on all platforms (Ubuntu 24.04 + macOS + Windows) before
- merge.
-
-## Release process
-
-Releases are cut from `main` via tag push:
+## Everyday commands
```bash
-# 1. Update CMakeLists.txt VERSION and move CHANGELOG.md entries into [X.Y.Z].
-# release.yml refuses tags without a matching CHANGELOG section or passing CI.
-# 2. Commit the version bump
-git commit -am "chore(release): cut vX.Y.Z"
-git push origin main
-
-# 3. Tag and push the tag; release.yml creates the GitHub Release
-git tag vX.Y.Z
-git push origin vX.Y.Z
+make test # Release build and tests
+make debug # Debug build in build-debug/
+make sanitize tsan # ASan + UBSan, ThreadSanitizer
+make tidy # clang-tidy build
+make lint # clang-format 18, explicit-type audit, generated-code check
+make format # Apply clang-format
+make codegen # Regenerate from spec/
+make docs # Doxygen API reference in build-docs/html (needs Doxygen)
+make bench # Google Benchmark suite
+make consumers # Build installed and FetchContent consumers
+make coverage # lcov report (needs lcov)
```
-Semver: bump MINOR for new public API, PATCH for fixes/docs/CI.
+Run `make format lint test` before pushing. `make install-hooks` runs format
+and lint on every commit. CI adds the sanitizers, clang-tidy, and the consumer
+check, on Linux, macOS, and Windows.
+
+## Generated code
+
+`tools/codegen/generate.py` writes the REST client from `spec/openapi.yaml` and
+the WebSocket types from `spec/asyncapi.yaml`: `include/kalshi/api.hpp`,
+`models.hpp`, and `ws_models.hpp`, `src/api/operations/`,
+`src/api/validate.hpp`, `src/models/json_meta.hpp`, `src/ws/wire.hpp`,
+`tests/test_operation_routes.cpp`, `tests/test_ws_messages.cpp`,
+`docs/operations.md`, and `docs/channels.md`. Change the generator, a template
+in `tools/codegen/`, or a spec, run `make codegen`, and commit the result.
+`make lint` fails on stale output. [docs/research.md](docs/research.md)
+explains how to refresh the specs.
-## Reporting issues
+## Code style
-- **Bugs / feature requests**: open a GitHub issue with reproduction
- steps + the kalshi-cpp version (`kalshi::VERSION`).
-- **Security vulnerabilities**: see [SECURITY.md](SECURITY.md) for the
- private reporting channel.
+- Public functions return `kalshi::Result` (`std::expected`) and
+ don't throw.
+- Spell out local variable types. `auto` is fine for structured bindings,
+ lambdas, and iterators. Anything else needs an `// auto-ok: reason` comment
+ or an entry in `tools/cpp_auto_allowlist.txt`; `make lint` checks this.
+- `.clang-format` sets the layout: tabs, 100 columns, project includes before
+ system includes.
+- Glaze reads and writes JSON. `strip_null_members` is the only hand-written
+ scanner, and it runs only after a parse fails.
+- Keep tests offline. Inject an `HttpTransport`, or use the local HTTP and
+ WebSocket servers in `tests/`.
+
+## Pull requests
+
+- Name branches `feat/`, `fix/`, `docs/`, `ci/`, `refactor/`, `test/`, or
+ `chore/`.
+- Title PRs as [Conventional Commits](https://www.conventionalcommits.org/),
+ such as `fix(ws): keep subscriptions across reconnects`. PRs are
+ squash-merged, so the title becomes the commit subject.
+- Note user-visible changes in `CHANGELOG.md` under `[Unreleased]`.
+
+## Releases
+
+1. Set `VERSION` in `CMakeLists.txt`, move the `[Unreleased]` notes into a new
+ `[X.Y.Z]` section, and update the `GIT_TAG` in `README.md`.
+2. Merge that change to `main`.
+3. Run `git tag vX.Y.Z && git push origin vX.Y.Z`. `release.yml` publishes the
+ GitHub release after CI passes on the tagged commit, and `docs.yml`
+ publishes the API reference.
+
+While the version is 0.x, a minor release may break the API; patch releases
+only fix things.
## License
-By contributing, you agree your changes are licensed under the MIT
-license that covers this repository.
+Contributions are licensed under the repository's MIT license.
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..d4c3e6b
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,36 @@
+# API reference for GitHub Pages. `make docs` writes it to build-docs/html.
+# Only settings that differ from Doxygen's defaults are listed.
+
+PROJECT_NAME = kalshi-cpp
+PROJECT_NUMBER = $(KALSHI_VERSION)
+PROJECT_BRIEF = "C++23 client for Kalshi's Predictions API"
+OUTPUT_DIRECTORY = build-docs
+
+INPUT = README.md \
+ docs/operations.md \
+ docs/channels.md \
+ docs/api-coverage.md \
+ docs/research.md \
+ CHANGELOG.md \
+ CONTRIBUTING.md \
+ SECURITY.md \
+ include/kalshi
+FILE_PATTERNS = *.hpp *.md
+EXCLUDE = include/kalshi/detail
+USE_MDFILE_AS_MAINPAGE = README.md
+STRIP_FROM_PATH = include
+STRIP_FROM_INC_PATH = include
+
+# Generated models document their fields from the spec where it does, so list
+# every member rather than hiding undocumented ones.
+EXTRACT_ALL = YES
+JAVADOC_AUTOBRIEF = YES
+BUILTIN_STL_SUPPORT = YES
+SORT_MEMBER_DOCS = NO
+WARN_IF_UNDOCUMENTED = NO
+WARN_AS_ERROR = FAIL_ON_WARNINGS
+QUIET = YES
+
+GENERATE_LATEX = NO
+GENERATE_TREEVIEW = YES
+HAVE_DOT = NO
diff --git a/Makefile b/Makefile
index 12abafd..ec4f842 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ CLANG_FORMAT_MAJOR := 18
CPP_SOURCES = git ls-files -z --cached --others --exclude-standard '*.cpp' '*.hpp' | \
xargs -0 sh -c 'for f; do [ -e "$$f" ] && printf "%s\0" "$$f"; done' _
-.PHONY: all configure build debug test sanitize tsan tidy bench consumers codegen lint lint-docs \
+.PHONY: all configure build debug test sanitize tsan tidy bench consumers codegen docs lint lint-docs \
format pre-commit install-hooks coverage clean help
all: build
@@ -48,6 +48,10 @@ bench:
consumers:
./tools/test_consumers.sh
+# Needs Doxygen. Opens at build-docs/html/index.html.
+docs:
+ KALSHI_VERSION=$$(./tools/project_version.sh) doxygen Doxyfile
+
# Needs PyYAML and clang-format 18.
codegen:
CLANG_FORMAT="$(CLANG_FORMAT)" $(PYTHON) tools/codegen/generate.py
@@ -103,7 +107,8 @@ help:
@echo "make tidy clang-tidy build in build-tidy/"
@echo "make bench Google Benchmark suite in build-bench/ (BENCH_ARGS=...)"
@echo "make consumers Check install and FetchContent consumers"
- @echo "make codegen Regenerate the REST client from spec/openapi.yaml"
+ @echo "make codegen Regenerate the REST client and WebSocket types from spec/"
+ @echo "make docs Doxygen API reference in build-docs/html"
@echo "make lint clang-format, explicit-type audit, generated-code check"
@echo "make lint-docs markdownlint"
@echo "make format Format C++ sources in place"
diff --git a/README.md b/README.md
index c2e9fac..9bf9a7d 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,17 @@ Kalshi's OpenAPI document and streams market data over WebSockets. Requests are
signed with Ed25519 or RSA-PSS keys, prices stay exact fixed-point strings, and
every call returns `std::expected` instead of throwing.
-The REST client is generated from [`spec/openapi.yaml`](spec/openapi.yaml), so
-model and field names match [Kalshi's API reference](https://docs.kalshi.com).
-Kalshi's separate Margin and Perpetuals API is out of scope.
+The client is generated from Kalshi's OpenAPI and AsyncAPI documents in
+[`spec/`](https://github.com/Reddimus/kalshi-cpp/tree/main/spec), so type and field names match
+[Kalshi's API reference](https://docs.kalshi.com). The
+[API reference for this library](https://reddimus.github.io/kalshi-cpp/) is
+built from its headers. Kalshi's separate Margin and Perpetuals API is out of
+scope.
## Build
You need a C++23 compiler, CMake 3.31+, OpenSSL 3, libcurl, and libwebsockets.
+Ubuntu 24.04's apt CMake is older; `pipx install cmake` gets a current one.
```bash
brew install cmake openssl curl libwebsockets pkg-config # macOS
@@ -154,10 +158,10 @@ lists each channel's message types.
| Program | What it does |
| --- | --- |
-| [`market_data`](examples/market_data.cpp) | Markets, an order book, and candlesticks, without a key |
-| [`portfolio`](examples/portfolio.cpp) | Balance, positions, and resting orders |
-| [`place_and_cancel_order`](examples/place_and_cancel_order.cpp) | A resting order and its cancel, on the demo exchange only |
-| [`stream_orderbook`](examples/stream_orderbook.cpp) | A live local order book that recovers from gaps and reconnects |
+| [`market_data`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/market_data.cpp) | Markets, an order book, and candlesticks, without a key |
+| [`portfolio`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/portfolio.cpp) | Balance, positions, and resting orders |
+| [`place_and_cancel_order`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/place_and_cancel_order.cpp) | A resting order and its cancel, on the demo exchange only |
+| [`stream_orderbook`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/stream_orderbook.cpp) | A live local order book that recovers from gaps and reconnects |
Put `KALSHI_API_KEY_ID`, `KALSHI_API_KEY_FILE`, and optionally `KALSHI_ENV=demo`
in `.env`, then run `make run-portfolio`.
@@ -168,7 +172,8 @@ in `.env`, then run `make run-portfolio`.
make format lint test # before every commit
make sanitize tsan tidy # ASan/UBSan, ThreadSanitizer, clang-tidy
make consumers bench # packaging check, benchmarks
-make codegen # after updating spec/openapi.yaml
+make codegen # after updating a spec in spec/
+make docs # API reference in build-docs/html
```
[CONTRIBUTING.md](CONTRIBUTING.md) covers the workflow and release steps.
diff --git a/SECURITY.md b/SECURITY.md
index e569d9e..779787a 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,43 +1,30 @@
# Security policy
-`kalshi-cpp` is a third-party C++ client for the Kalshi exchange API.
-It signs every request with an account-bound RSA private key, so a
-vulnerability that mishandles credentials or leaks request material
-could put live trading capital at risk. This file is the canonical
-contact path for reporting one.
+`kalshi-cpp` is a third-party C++ client for the Kalshi exchange API. It signs
+requests with an account's Ed25519 or RSA private key, so a bug that leaks a
+key or lets someone forge requests could put trading capital at risk.
## Supported versions
-Security fixes are made on the latest published `vX.Y.Z` tag. Older
-tags are not back-patched. Bump your `FetchContent_Declare(... GIT_TAG ...)`
-pin or your `find_package(kalshi X.Y.Z CONFIG REQUIRED)` constraint to
-the latest minor on the same major as part of the upgrade.
-
-| Version | Supported |
-| ---------- | ------------------ |
-| latest tag | :white_check_mark: |
-| older | :x: |
+Fixes go into the next release after the latest tag; older releases are not
+patched. Upgrade by moving your `GIT_TAG` or `find_package(kalshi X.Y CONFIG)`
+version to the new release.
## Reporting a vulnerability
-**Do not open a public issue.** Use GitHub's [private vulnerability
-reporting](https://github.com/Reddimus/kalshi-cpp/security/advisories/new)
-flow, which delivers the report to the maintainer privately and
-tracks coordinated disclosure.
+Don't open a public issue. Use GitHub's [private vulnerability
+reporting](https://github.com/Reddimus/kalshi-cpp/security/advisories/new),
+which reaches the maintainer privately and tracks disclosure.
-When reporting, please include:
+Include:
- Affected version (tag or commit SHA)
- A minimal reproduction or test case
- Impact (credential leak / request forgery / DoS / something else)
- Whether you've notified anyone else (e.g. Kalshi directly)
-You can expect:
-
-- Acknowledgement within **3 business days**
-- An initial assessment + severity rating within **7 business days**
-- A fix on a new `vX.Y.Z+1` tag, or a clear timeline if the fix is
- larger
+You should hear back within 3 business days, and get an assessment within 7.
+The fix ships in a new release, or you get a timeline if it takes longer.
## Out of scope
diff --git a/cmake/KalshiWebsockets.cmake b/cmake/KalshiWebsockets.cmake
new file mode 100644
index 0000000..2a7449e
--- /dev/null
+++ b/cmake/KalshiWebsockets.cmake
@@ -0,0 +1,42 @@
+# kalshi_find_websockets()
+#
+# Finds libwebsockets and sets KALSHI_WEBSOCKETS_TARGET to an imported target
+# and KALSHI_WEBSOCKETS_VIA to "cmake" or "pkg-config" in the caller's scope.
+#
+# libwebsockets' own CMake package (vcpkg, Homebrew, and most distributions ship
+# one) calls include_directories() and link_directories() and appends to
+# CMAKE_MODULE_PATH and CMAKE_REQUIRED_INCLUDES. Running it inside a function
+# drops the variable changes, and the directory properties are restored
+# exactly, so nothing leaks into the caller. The headers go on the imported
+# target instead, where they count as system headers.
+function(kalshi_find_websockets required)
+ get_property(include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES)
+ get_property(link_dirs DIRECTORY PROPERTY LINK_DIRECTORIES)
+ find_package(libwebsockets CONFIG QUIET)
+ set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "${include_dirs}")
+ set_property(DIRECTORY PROPERTY LINK_DIRECTORIES "${link_dirs}")
+
+ foreach(candidate websockets_shared websockets)
+ if(TARGET ${candidate})
+ if(DEFINED LIBWEBSOCKETS_INCLUDE_DIRS)
+ set_property(TARGET ${candidate} APPEND PROPERTY
+ INTERFACE_INCLUDE_DIRECTORIES ${LIBWEBSOCKETS_INCLUDE_DIRS})
+ endif()
+ set(KALSHI_WEBSOCKETS_TARGET ${candidate} PARENT_SCOPE)
+ set(KALSHI_WEBSOCKETS_VIA cmake PARENT_SCOPE)
+ return()
+ endif()
+ endforeach()
+
+ find_package(PkgConfig QUIET)
+ if(PKG_CONFIG_FOUND)
+ pkg_check_modules(WEBSOCKETS QUIET IMPORTED_TARGET GLOBAL libwebsockets)
+ endif()
+ if(TARGET PkgConfig::WEBSOCKETS)
+ set(KALSHI_WEBSOCKETS_TARGET PkgConfig::WEBSOCKETS PARENT_SCOPE)
+ set(KALSHI_WEBSOCKETS_VIA pkg-config PARENT_SCOPE)
+ elseif(required)
+ message(FATAL_ERROR "libwebsockets not found: install it with its CMake package or a "
+ "pkg-config file")
+ endif()
+endfunction()
diff --git a/cmake/kalshiConfig.cmake.in b/cmake/kalshiConfig.cmake.in
index 2e98379..5ab7dba 100644
--- a/cmake/kalshiConfig.cmake.in
+++ b/cmake/kalshiConfig.cmake.in
@@ -4,11 +4,19 @@ include(CMakeFindDependencyMacro)
find_dependency(OpenSSL 3.0)
find_dependency(CURL)
-if(NOT WIN32)
- find_dependency(PkgConfig)
- pkg_check_modules(WEBSOCKETS REQUIRED IMPORTED_TARGET libwebsockets)
-else()
- find_dependency(libwebsockets CONFIG)
+
+include("${CMAKE_CURRENT_LIST_DIR}/KalshiWebsockets.cmake")
+kalshi_find_websockets(FALSE)
+if(NOT DEFINED KALSHI_WEBSOCKETS_TARGET)
+ set(kalshi_FOUND FALSE)
+ set(kalshi_NOT_FOUND_MESSAGE "kalshi-cpp needs libwebsockets, which was not found")
+ return()
+endif()
+# The exported targets link the libwebsockets target this package was built
+# against; map it to whichever one was found here.
+if(NOT TARGET @KALSHI_WEBSOCKETS_TARGET@)
+ add_library(@KALSHI_WEBSOCKETS_TARGET@ INTERFACE IMPORTED)
+ target_link_libraries(@KALSHI_WEBSOCKETS_TARGET@ INTERFACE ${KALSHI_WEBSOCKETS_TARGET})
endif()
include("${CMAKE_CURRENT_LIST_DIR}/kalshiTargets.cmake")
diff --git a/docs/research.md b/docs/research.md
index 0b0898e..bdecb28 100644
--- a/docs/research.md
+++ b/docs/research.md
@@ -26,8 +26,7 @@ RSA-PSS and SHA-256; Ed25519 keys sign the message directly. See Kalshi's
curl -fsS https://docs.kalshi.com/openapi.yaml -o spec/openapi.yaml
curl -fsS https://docs.kalshi.com/asyncapi.yaml -o spec/asyncapi.yaml
shasum -a 256 spec/*.yaml
-python3 tools/codegen/generate.py
-make test
+make codegen test
```
Review the diff of the generated files, update the table above, and note
diff --git a/include/kalshi/ws_models.hpp b/include/kalshi/ws_models.hpp
index 89b0ed2..46d6507 100644
--- a/include/kalshi/ws_models.hpp
+++ b/include/kalshi/ws_models.hpp
@@ -174,10 +174,7 @@ enum class LifecyclePriceLevelStructure : std::uint8_t {
return "";
}
-/// Field to annotate which of the event type this event is for: - `created` - Market created -
-/// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated` -
-/// Market close date updated - `determined` - Market determined - `settled` - Market settled -
-/// `price_level_structu...
+/// Field to annotate which of the event type this event is for
enum class MarketLifecycleV2EventType : std::uint8_t {
Unknown, ///< A value this SDK version does not know.
Created, ///< `created`
@@ -211,9 +208,7 @@ enum class MarketLifecycleV2EventType : std::uint8_t {
return "";
}
-/// Field to annotate which of the event type this event is for: - `created` - Market created -
-/// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated` -
-/// Market close date updated - `determined` - Market determined - `settled` - Market settled
+/// Field to annotate which of the event type this event is for
enum class MultivariateMarketLifecycleEventType : std::uint8_t {
Unknown, ///< A value this SDK version does not know.
Created, ///< `created`
@@ -555,10 +550,7 @@ struct MarketLifecycleV2 {
std::optional is_deactivated;
/// Optional - This key will be emitted when the market is created
std::optional additional_metadata;
- /// Field to annotate which of the event type this event is for: - `created` - Market created -
- /// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated`
- /// - Market close date updated - `determined` - Market determined - `settled` - Market settled
- /// - `price_level_structu...
+ /// Field to annotate which of the event type this event is for
MarketLifecycleV2EventType event_type{};
/// Optional - The market price level structure on creation or price_level_structure_updated
/// events
@@ -631,9 +623,7 @@ struct MultivariateMarketLifecycle {
std::optional is_deactivated;
/// Optional - This key will be emitted when the market is created
std::optional additional_metadata;
- /// Field to annotate which of the event type this event is for: - `created` - Market created -
- /// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated`
- /// - Market close date updated - `determined` - Market determined - `settled` - Market settled
+ /// Field to annotate which of the event type this event is for
MultivariateMarketLifecycleEventType event_type{};
/// Optional - The market price level structure on creation
std::optional price_level_structure;
diff --git a/tools/codegen/common.py b/tools/codegen/common.py
index 2554a0b..c4bfea1 100644
--- a/tools/codegen/common.py
+++ b/tools/codegen/common.py
@@ -56,10 +56,19 @@ def first_sentence(text: str | None) -> str:
return ""
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # markdown links -> text
text = re.sub(r"<[^>]+>", "", text)
+ # A summary that introduces a list ends where the list starts.
+ text = re.split(r":\s*\n\s*[-*] ", text, maxsplit=1)[0]
text = " ".join(text.split())
match = re.match(r"(.+?[.!?])(\s|$)", text)
sentence = match.group(1) if match else text
- return sentence if len(sentence) <= 300 else sentence[:297].rstrip() + "..."
+ if len(sentence) <= 300:
+ return sentence
+ cut = sentence[:297]
+ if sentence[297] != " ": # mid-word: back up to the last whole word
+ cut = cut.rsplit(" ", 1)[0]
+ if cut.count("`") % 2: # never end inside a code span
+ cut = cut[:cut.rindex("`")].rstrip() or cut.replace("`", "")
+ return cut.rstrip() + "..."
def doc_lines(text: str, indent: str, width: int = 96) -> list[str]: