Remove API key requirement

This commit is contained in:
NeilClack
2026-08-17 20:36:59 -05:00
parent 359ae905d7
commit 74245ee07d
16 changed files with 74 additions and 194 deletions
+1 -12
View File
@@ -4,7 +4,6 @@ on:
# Manual only for now. Select the branch to build when dispatching: # Manual only for now. Select the branch to build when dispatching:
# main -> APP_VERSION is the current git tag # main -> APP_VERSION is the current git tag
# dev -> APP_VERSION is the current commit hash # dev -> APP_VERSION is the current commit hash
# Both branches bake the portal API key from secrets.
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -44,28 +43,18 @@ jobs:
else else
APP_VERSION="$(git rev-parse --short HEAD)" APP_VERSION="$(git rev-parse --short HEAD)"
fi fi
echo "APP_VERSION=$APP_VERSION" >> "$GITHUB_ENV"
echo "Branch: ${{ gitea.ref_name }} -> APP_VERSION=$APP_VERSION" echo "Branch: ${{ gitea.ref_name }} -> APP_VERSION=$APP_VERSION"
if [ -n "${{ secrets.RWT_API_KEY }}" ]; then
echo "API key: provided via secret RWT_API_KEY"
else
echo "API key: NOT set (build proceeds without an Authorization header)"
fi
# checks is keyed per-system (checks.<system>.default); nix build's # checks is keyed per-system (checks.<system>.default); nix build's
# #attr shorthand can't resolve it, so resolve the current system inline. # #attr shorthand can't resolve it, so resolve the current system inline.
# --impure lets the flake read APP_VERSION/API_KEY via builtins.getEnv # --impure lets the flake read APP_VERSION via builtins.getEnv
# (pure evals always get the package defaults). The offline test suite # (pure evals always get the package defaults). The offline test suite
# runs in the package's checkPhase. # runs in the package's checkPhase.
- name: Build + offline test suite - name: Build + offline test suite
env:
API_KEY: ${{ secrets.RWT_API_KEY }}
run: nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" --impure run: nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" --impure
- name: Live portal tests - name: Live portal tests
if: ${{ secrets.RWT_LIVE_API_KEY != '' }}
env: env:
RWT_LIVE_API_KEY: ${{ secrets.RWT_LIVE_API_KEY }}
RWT_LIVE_ACCOUNT_EMAIL: ${{ secrets.RWT_LIVE_ACCOUNT_EMAIL }} RWT_LIVE_ACCOUNT_EMAIL: ${{ secrets.RWT_LIVE_ACCOUNT_EMAIL }}
RWT_LIVE_LICENSE_EMAIL: ${{ secrets.RWT_LIVE_LICENSE_EMAIL }} RWT_LIVE_LICENSE_EMAIL: ${{ secrets.RWT_LIVE_LICENSE_EMAIL }}
RWT_LIVE_LICENSE_KEY: ${{ secrets.RWT_LIVE_LICENSE_KEY }} RWT_LIVE_LICENSE_KEY: ${{ secrets.RWT_LIVE_LICENSE_KEY }}
+6 -24
View File
@@ -33,23 +33,8 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION="1.2.3"
The Nix package bakes `-DAPP_VERSION=<pkg version>` via `cmakeFlags` (`version` The Nix package bakes `-DAPP_VERSION=<pkg version>` via `cmakeFlags` (`version`
defaults to `0.1.0` in `nix/package.nix`; the flake overrides it from the defaults to `0.1.0` in `nix/package.nix`; the flake overrides it from the
`APP_VERSION` env var on impure evals). `nix develop` `APP_VERSION` env var on impure evals). `nix develop`
sets `APP_VERSION=dev` and exports `API_KEY` from the ambient environment sets `APP_VERSION=dev`. CMake seeds a *fresh* configure from the
(falling back to `RWT_LIVE_API_KEY`). CMake seeds a *fresh* configure from the `APP_VERSION` env var; `-D` wins on reconfigure.
`APP_VERSION` / `API_KEY` env vars; `-D` wins on reconfigure.
The portal API key is also a build-time argument: `-DAPI_KEY="..."` (CMake) or
`apiKey` on `callPackage` (Nix). It is compiled into the binary as the
`Authorization: Bearer` token and defaults to empty (no auth header). Keep the
real value in CI secrets, never in source — rotate by rebuilding with a new
key.
Version and API key can be baked in together with a single configure:
```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DAPP_VERSION="1.2.3" \
-DAPI_KEY="$API_KEY"
```
- The app resolves `assets/...` relative to its working directory — launch - The app resolves `assets/...` relative to its working directory — launch
`./build/radiantwave` from the repo root. (The Nix wrapper chdirs to `./build/radiantwave` from the repo root. (The Nix wrapper chdirs to
@@ -238,14 +223,11 @@ cached.
`checks` is keyed per-system, so the workflow resolves the current system `checks` is keyed per-system, so the workflow resolves the current system
via `nix eval --impure --expr 'builtins.currentSystem'`). The build bakes via `nix eval --impure --expr 'builtins.currentSystem'`). The build bakes
in a per-branch `APP_VERSION` — the current git tag on `main`, the current in a per-branch `APP_VERSION` — the current git tag on `main`, the current
commit hash on `dev` — plus the portal API key from the `RWT_API_KEY` commit hash on `dev`. The flake reads `APP_VERSION` from the environment via
secret. The flake reads `APP_VERSION`/`API_KEY` from the environment via
`builtins.getEnv`, so the build must pass `--impure` (pure evals get the `builtins.getEnv`, so the build must pass `--impure` (pure evals get the
package defaults). When the `RWT_LIVE_API_KEY` secret is set it also runs package defaults). The live portal tests run from `nix develop` when the
`test_api_client` / `test_license_service` against the portal via `RWT_LIVE_*` secrets are set and need `RWT_LIVE_ACCOUNT_EMAIL`,
`nix develop` (outside the sandbox, like a dev machine). Those live tests `RWT_LIVE_LICENSE_EMAIL`, and `RWT_LIVE_LICENSE_KEY`.
also need `RWT_LIVE_ACCOUNT_EMAIL`, `RWT_LIVE_LICENSE_EMAIL`, and
`RWT_LIVE_LICENSE_KEY`.
- `test_assets` must pass before committing — it catches missing fonts/images - `test_assets` must pass before committing — it catches missing fonts/images
that break the app at runtime. that break the app at runtime.
- `cmake --build build` builds everything; pass `--target <name>` to build one - `cmake --build build` builds everything; pass `--target <name>` to build one
-15
View File
@@ -18,20 +18,6 @@ else()
"Version string shown in the Home footer (override at configure time)") "Version string shown in the Home footer (override at configure time)")
endif() endif()
# API bearer key baked into the binary and sent as `Authorization: Bearer <key>`
# on every portal request. Leave empty to send no auth header. Never commit the
# real value — inject it at CI build time from a secret, e.g.:
# cmake ... -DAPI_KEY="${{ secrets.PORTAL_API_KEY }}"
# The `nix develop` shell exports API_KEY from the ambient environment, which
# seeds a fresh configure; -D wins on reconfigure.
if(DEFINED ENV{API_KEY})
set(API_KEY "$ENV{API_KEY}" CACHE STRING
"Portal API key baked into the binary (leave empty to skip auth)")
else()
set(API_KEY "" CACHE STRING
"Portal API key baked into the binary (leave empty to skip auth)")
endif()
find_package(SDL3 REQUIRED) find_package(SDL3 REQUIRED)
find_package(SDL3_ttf REQUIRED) find_package(SDL3_ttf REQUIRED)
find_package(SDL3_image REQUIRED) find_package(SDL3_image REQUIRED)
@@ -90,7 +76,6 @@ add_executable(radiantwave
target_compile_definitions(radiantwave PRIVATE target_compile_definitions(radiantwave PRIVATE
APP_VERSION="${APP_VERSION}" APP_VERSION="${APP_VERSION}"
API_KEY="${API_KEY}"
) )
target_include_directories(radiantwave PRIVATE src) target_include_directories(radiantwave PRIVATE src)
+11 -43
View File
@@ -136,45 +136,13 @@ If `APP_VERSION` is left unset the footer falls back to the project version
from `CMakeLists.txt` (`project(... VERSION ...)`); a build that defines no from `CMakeLists.txt` (`project(... VERSION ...)`); a build that defines no
version at all shows `unknown`. The Nix build sets `APP_VERSION` automatically version at all shows `unknown`. The Nix build sets `APP_VERSION` automatically
to the flake package version, so a plain `nix build` needs no extra flags. to the flake package version, so a plain `nix build` needs no extra flags.
With `--impure` the flake instead reads the `APP_VERSION` / `API_KEY` With `--impure` the flake instead reads the `APP_VERSION`
environment variables — this is how CI injects the version and portal key environment variable — this is how CI injects the version
(see the CI note under *Common Pitfalls*). Pure evals always get the package (see the CI note under *Common Pitfalls*). Pure evals always get the package
defaults, because `builtins.getEnv` cannot see the environment in pure defaults, because `builtins.getEnv` cannot see the environment in pure
evaluation mode. evaluation mode.
#### Portal API key #### Version in one configure
Every portal request sends `Authorization: Bearer <key>`. The key is baked
into the binary at build time via the `API_KEY` CMake cache variable. It is
**optional** — when unset, requests go out without an auth header. Keep the
real value out of the source tree and inject it at build time from your CI
secret store so it never lands in `git`:
```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DAPI_KEY="${{ secrets.PORTAL_API_KEY }}"
cmake --build build
```
Rotate the key by building with a new value and shipping the new binary. The
Nix package accepts the same value via `callPackage ./nix/package.nix {
apiKey = "…"; }`, or via the `API_KEY` environment variable with
`nix build --impure`; the flake itself never embeds one.
#### Version + API key in one configure
`APP_VERSION` and `API_KEY` are independent CMake cache variables, so pass
both — plus the build type — in a single configure:
```sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug \
-DAPP_VERSION="1.2.3" \
-DAPI_KEY="${{ secrets.PORTAL_API_KEY }}"
cmake --build build
```
`-D` values always win over the `APP_VERSION` / `API_KEY` environment
variables, which `nix develop` seeds from the ambient environment.
### Building with Nix ### Building with Nix
@@ -187,14 +155,14 @@ nix build .#packages.x86_64-linux.default # needs a Linux machine/builders
nix build .#packages.aarch64-linux.default nix build .#packages.aarch64-linux.default
``` ```
To bake in a custom version and/or portal key, build impurely — the flake To bake in a custom version, build impurely — the flake
reads `APP_VERSION` and `API_KEY` from the environment on impure evals: reads `APP_VERSION` from the environment on impure evals:
```sh ```sh
APP_VERSION="1.2.3" API_KEY="$PORTAL_API_KEY" nix build --impure .#default APP_VERSION="1.2.3" nix build --impure .#default
``` ```
A pure `nix build` always uses the package defaults (`0.1.0`, no auth header) A pure `nix build` always uses the package defaults (`0.1.0`)
even if those variables are set in your shell, because pure evaluation cannot even if those variables are set in your shell, because pure evaluation cannot
see the environment. CI uses `--impure` to inject them. see the environment. CI uses `--impure` to inject them.
@@ -633,9 +601,9 @@ status to decide which page to show.
workflow is `.gitea/workflows/build.yml`. It is **manual-only** workflow is `.gitea/workflows/build.yml`. It is **manual-only**
(`workflow_dispatch`) — pick the branch when dispatching. `main` builds bake (`workflow_dispatch`) — pick the branch when dispatching. `main` builds bake
the current git tag as `APP_VERSION`; `dev` builds bake the current commit the current git tag as `APP_VERSION`; `dev` builds bake the current commit
hash. Both pass the portal key from the `RWT_API_KEY` secret and build with hash. Both build with `nix build --impure` (which lets the flake's env
`nix build --impure` (which lets the flake's env injection work). The injection work). The offline test suite runs in the package's `checkPhase`;
offline test suite runs in the package's `checkPhase`; the live portal tests the live portal tests run from `nix develop` when the `RWT_LIVE_*` secrets
run from `nix develop` when the `RWT_LIVE_*` secrets are set. are set.
- Asset tests (`test_assets`) must pass before committing -- they catch missing - Asset tests (`test_assets`) must pass before committing -- they catch missing
fonts and images that would break the app at runtime. fonts and images that would break the app at runtime.
+13 -20
View File
@@ -34,18 +34,16 @@
}; };
in in
{ {
# Version and API key are injected from the ambient environment on # Version is injected from the ambient environment on
# impure evals (CI sets APP_VERSION/API_KEY from the git ref and a # impure evals (CI sets APP_VERSION from the git ref). Pure evals
# secret). Pure evals (`nix flake check`, local `nix build`) can't # (`nix flake check`, local `nix build`) can't see the
# see the environment, so builtins.getEnv returns "" and the # environment, so builtins.getEnv returns "" and the package
# package falls back to its 0.1.0 default / no auth header. # falls back to its 0.1.0 default.
packages.default = packages.default =
let let
envVersion = builtins.getEnv "APP_VERSION"; envVersion = builtins.getEnv "APP_VERSION";
envApiKey = builtins.getEnv "API_KEY";
packageArgs = packageArgs =
{ apiKey = envApiKey; } nixpkgs.lib.optionalAttrs (envVersion != "") { version = envVersion; };
// (nixpkgs.lib.optionalAttrs (envVersion != "") { version = envVersion; });
in in
pkgs.callPackage ./nix/package.nix packageArgs; pkgs.callPackage ./nix/package.nix packageArgs;
@@ -53,22 +51,17 @@
inputsFrom = [ self.packages.${system}.default ]; inputsFrom = [ self.packages.${system}.default ];
packages = [ ]; packages = [ ];
env = { env = {
# APP_VERSION and API_KEY are CMake cache variables with no # APP_VERSION is a CMake cache variable with no default; on a
# default; on a fresh configure cmake seeds their cache from # fresh configure cmake seeds its cache from the environment
# environment variables of the same name, so a plain # variable of the same name, so a plain `cmake -S . -B build`
# `cmake -S . -B build` bakes them into the binary. # bakes it into the binary.
APP_VERSION = "dev"; APP_VERSION = "dev";
}; };
shellHook = '' shellHook = ''
# Never commit the real key. Export API_KEY (or the CI name echo "RadiantWave dev shell (APP_VERSION=dev)"
# RWT_LIVE_API_KEY) in your ambient environment and this shell
# picks it up; empty by default (no auth header).
export API_KEY="''${API_KEY:-''${RWT_LIVE_API_KEY:-}}"
echo "RadiantWave dev shell (APP_VERSION=dev, API_KEY=$([[ -n "$API_KEY" ]] && echo set || echo unset))"
echo " Configure: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug" echo " Configure: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug"
echo " Bake version+key: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION=\"1.2.3\" -DAPI_KEY=\"<key>\"" echo " Bake version: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DAPP_VERSION=\"1.2.3\""
echo " Nix build (env version+key, impure): APP_VERSION=... API_KEY=... nix build --impure .#default" echo " Nix build (env version, impure): APP_VERSION=... nix build --impure .#default"
echo " Build: cmake --build build" echo " Build: cmake --build build"
echo " Run: ./build/radiantwave" echo " Run: ./build/radiantwave"
echo " Tests: ./build/test_assets && ./build/test_<name>" echo " Tests: ./build/test_assets && ./build/test_<name>"
+1 -7
View File
@@ -15,9 +15,6 @@
curl, curl,
openssl, openssl,
systemdLibs, systemdLibs,
# Portal API key baked into the binary. Keep it out of the flake; pass it
# when calling callPackage from CI/your own config (see README "App version").
apiKey ? "",
# App version baked into the Home footer's Version row. CI overrides this # App version baked into the Home footer's Version row. CI overrides this
# per branch (git tag on main, commit hash on dev); local/pure builds keep # per branch (git tag on main, commit hash on dev); local/pure builds keep
# the 0.1.0 default. # the 0.1.0 default.
@@ -54,10 +51,7 @@ stdenv.mkDerivation (finalAttrs: {
# Bake the package version into the binary so the Home footer's Version row # Bake the package version into the binary so the Home footer's Version row
# shows it (CMake passes APP_VERSION through as a compile definition). # shows it (CMake passes APP_VERSION through as a compile definition).
# API_KEY is only added when a non-empty key is supplied to callPackage. cmakeFlags = [ "-DAPP_VERSION=${finalAttrs.version}" ];
cmakeFlags =
[ "-DAPP_VERSION=${finalAttrs.version}" ]
++ lib.optionals (apiKey != "") [ "-DAPI_KEY=${apiKey}" ];
# gtest is a buildInput, not native: nixpkgs builds it shared, so the # gtest is a buildInput, not native: nixpkgs builds it shared, so the
# check-phase test binaries need its lib dir baked into their RPATH. # check-phase test binaries need its lib dir baked into their RPATH.
+1 -1
View File
@@ -97,7 +97,7 @@ void LicenseKeyPage::submit()
// When the email is missing the key is saved but the check is skipped. // When the email is missing the key is saved but the check is skipped.
std::string errorMessage; std::string errorMessage;
const radiantwave::license::Result result = radiantwave::license::validateStoredLicense( const radiantwave::license::Result result = radiantwave::license::validateStoredLicense(
state_->db, "", &errorMessage); state_->db, &errorMessage);
switch (result) { switch (result) {
case radiantwave::license::Result::Ok: case radiantwave::license::Result::Ok:
+13 -26
View File
@@ -4,10 +4,6 @@
#include <curl/curl.h> #include <curl/curl.h>
#include <string> #include <string>
#ifndef API_KEY
#define API_KEY ""
#endif
namespace radiantwave::api { namespace radiantwave::api {
namespace { namespace {
@@ -31,7 +27,7 @@ size_t writeCallback(char* ptr, size_t size, size_t nmemb, void* userdata) {
} }
// Shared curl setup + perform. Returns a populated HttpResponse. // Shared curl setup + perform. Returns a populated HttpResponse.
HttpResponse performRequest(const std::string& url, const std::string& apiKey, HttpResponse performRequest(const std::string& url,
bool post, const std::string& jsonBody) { bool post, const std::string& jsonBody) {
HttpResponse response; HttpResponse response;
@@ -47,12 +43,6 @@ HttpResponse performRequest(const std::string& url, const std::string& apiKey,
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response.body);
struct curl_slist* headers = nullptr; struct curl_slist* headers = nullptr;
// An explicit apiKey wins; otherwise fall back to the key baked in at
// build time via -DAPI_KEY (empty string when unset).
const std::string effectiveKey = apiKey.empty() ? API_KEY : apiKey;
if (!effectiveKey.empty()) {
headers = curl_slist_append(headers, ("Authorization: Bearer " + effectiveKey).c_str());
}
if (post) { if (post) {
headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "Content-Type: application/json");
curl_easy_setopt(curl, CURLOPT_POST, 1L); curl_easy_setopt(curl, CURLOPT_POST, 1L);
@@ -79,13 +69,12 @@ HttpResponse performRequest(const std::string& url, const std::string& apiKey,
// ─── HTTP helpers ────────────────────────────────────────────────────── // ─── HTTP helpers ──────────────────────────────────────────────────────
HttpResponse httpGet(const std::string& path, const std::string& apiKey) { HttpResponse httpGet(const std::string& path) {
return performRequest(urlFor(path), apiKey, false, ""); return performRequest(urlFor(path), false, "");
} }
HttpResponse httpPost(const std::string& path, const std::string& jsonBody, HttpResponse httpPost(const std::string& path, const std::string& jsonBody) {
const std::string& apiKey) { return performRequest(urlFor(path), true, jsonBody);
return performRequest(urlFor(path), apiKey, true, jsonBody);
} }
// ─── GET /frequencies ────────────────────────────────────────────────── // ─── GET /frequencies ──────────────────────────────────────────────────
@@ -112,8 +101,8 @@ bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& out)
return true; return true;
} }
bool fetchFrequencies(std::vector<Frequency>& out, const std::string& apiKey) { bool fetchFrequencies(std::vector<Frequency>& out) {
HttpResponse response = httpGet(PATH_FREQUENCIES, apiKey); HttpResponse response = httpGet(PATH_FREQUENCIES);
if (!response.transportOk || response.status < 200 || response.status >= 300) { if (!response.transportOk || response.status < 200 || response.status >= 300) {
return false; return false;
} }
@@ -169,8 +158,8 @@ bool parseColorPalettesJson(const std::string& body, std::vector<ColorPalette>&
return true; return true;
} }
bool fetchColorPalettes(std::vector<ColorPalette>& out, const std::string& apiKey) { bool fetchColorPalettes(std::vector<ColorPalette>& out) {
HttpResponse response = httpGet(PATH_COLOR_PALETTES, apiKey); HttpResponse response = httpGet(PATH_COLOR_PALETTES);
if (!response.transportOk || response.status < 200 || response.status >= 300) { if (!response.transportOk || response.status < 200 || response.status >= 300) {
return false; return false;
} }
@@ -196,13 +185,12 @@ bool parseLicenseJson(const std::string& body, LicenseInfo& out) {
return !out.key.empty(); return !out.key.empty();
} }
LicenseResult validateLicense(const std::string& key, const std::string& email, LicenseResult validateLicense(const std::string& key, const std::string& email) {
const std::string& apiKey) {
LicenseResult result; LicenseResult result;
const std::string jsonBody = const std::string jsonBody =
"{\"license_key\":\"" + key + "\",\"email_address\":\"" + email + "\"}"; "{\"license_key\":\"" + key + "\",\"email_address\":\"" + email + "\"}";
HttpResponse response = httpPost(PATH_LICENSE, jsonBody, apiKey); HttpResponse response = httpPost(PATH_LICENSE, jsonBody);
result.status = response.status; result.status = response.status;
if (!response.transportOk) { if (!response.transportOk) {
@@ -233,8 +221,7 @@ bool parseCreateAccountJson(const std::string& body, AccountBody& out) {
return true; return true;
} }
AccountResult createAccount(const std::string& email, const std::string& name, AccountResult createAccount(const std::string& email, const std::string& name) {
const std::string& apiKey) {
AccountResult result; AccountResult result;
std::string jsonBody = std::string jsonBody =
@@ -244,7 +231,7 @@ AccountResult createAccount(const std::string& email, const std::string& name,
} }
jsonBody += "}"; jsonBody += "}";
HttpResponse response = httpPost(PATH_CREATE_ACCOUNT, jsonBody, apiKey); HttpResponse response = httpPost(PATH_CREATE_ACCOUNT, jsonBody);
result.status = response.status; result.status = response.status;
if (!response.transportOk) { if (!response.transportOk) {
+7 -14
View File
@@ -24,14 +24,9 @@ struct HttpResponse {
bool transportOk = false; // true when the request completed without a transport error bool transportOk = false; // true when the request completed without a transport error
}; };
// Performs a GET/POST against `path` (relative to API_BASE_URL). When `apiKey` is // Performs a GET/POST against `path` (relative to API_BASE_URL).
// non-empty it is sent as `Authorization: Bearer <apiKey>`. When it is empty (or HttpResponse httpGet(const std::string& path);
// omitted) the request falls back to the key baked in at build time via the HttpResponse httpPost(const std::string& path, const std::string& jsonBody);
// `API_KEY` compile definition (see CMakeLists.txt); with neither set, no auth
// header is sent.
HttpResponse httpGet(const std::string& path, const std::string& apiKey = "");
HttpResponse httpPost(const std::string& path, const std::string& jsonBody,
const std::string& apiKey = "");
// ─── GET /frequencies ────────────────────────────────────────────────── // ─── GET /frequencies ──────────────────────────────────────────────────
@@ -45,7 +40,7 @@ struct Frequency {
bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& out); bool parseFrequenciesJson(const std::string& body, std::vector<Frequency>& out);
// Fetch the solfeggio frequencies. Returns true only on HTTP 2xx + successful parse. // Fetch the solfeggio frequencies. Returns true only on HTTP 2xx + successful parse.
bool fetchFrequencies(std::vector<Frequency>& out, const std::string& apiKey = ""); bool fetchFrequencies(std::vector<Frequency>& out);
// ─── GET /color-palettes ─────────────────────────────────────────────── // ─── GET /color-palettes ───────────────────────────────────────────────
@@ -75,7 +70,7 @@ bool parseColorPalettesJson(const std::string& body, std::vector<ColorPalette>&
// NOTE: the portal does not expose this route yet. This is readiness code — it // NOTE: the portal does not expose this route yet. This is readiness code — it
// is deliberately not called anywhere in the app until the route ships, so // is deliberately not called anywhere in the app until the route ships, so
// palettes can be updated from the portal rather than code releases. // palettes can be updated from the portal rather than code releases.
bool fetchColorPalettes(std::vector<ColorPalette>& out, const std::string& apiKey = ""); bool fetchColorPalettes(std::vector<ColorPalette>& out);
// ─── POST /license ───────────────────────────────────────────────────── // ─── POST /license ─────────────────────────────────────────────────────
@@ -98,8 +93,7 @@ struct LicenseResult {
// Validate a license key / email pair against the portal. Never throws; failures // Validate a license key / email pair against the portal. Never throws; failures
// (network, HTTP error status) are reported through the returned LicenseResult. // (network, HTTP error status) are reported through the returned LicenseResult.
// A 2xx response means the license is valid and the subscription is active. // A 2xx response means the license is valid and the subscription is active.
LicenseResult validateLicense(const std::string& key, const std::string& email, LicenseResult validateLicense(const std::string& key, const std::string& email);
const std::string& apiKey = "");
// ─── POST /automations/create/account ────────────────────────────────── // ─── POST /automations/create/account ──────────────────────────────────
@@ -122,7 +116,6 @@ struct AccountResult {
// Creates (or reuses) a user account and issues a license key. The key is emailed // Creates (or reuses) a user account and issues a license key. The key is emailed
// to the user and is NOT part of the response. `name` may be empty. // to the user and is NOT part of the response. `name` may be empty.
AccountResult createAccount(const std::string& email, const std::string& name = "", AccountResult createAccount(const std::string& email, const std::string& name = "");
const std::string& apiKey = "");
} // namespace radiantwave::api } // namespace radiantwave::api
+1 -2
View File
@@ -17,7 +17,6 @@ long long nowEpochSeconds() {
} // namespace } // namespace
Result validateStoredLicense(radiantwave::db::DatabaseManager& db, Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
const std::string& apiKey,
std::string* outErrorMessage) { std::string* outErrorMessage) {
if (outErrorMessage) outErrorMessage->clear(); if (outErrorMessage) outErrorMessage->clear();
@@ -28,7 +27,7 @@ Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
} }
const radiantwave::api::LicenseResult result = const radiantwave::api::LicenseResult result =
radiantwave::api::validateLicense(key, email, apiKey); radiantwave::api::validateLicense(key, email);
if (result.status == 0) { if (result.status == 0) {
if (outErrorMessage) *outErrorMessage = "network error"; if (outErrorMessage) *outErrorMessage = "network error";
-1
View File
@@ -26,7 +26,6 @@ constexpr long long GRACE_PERIOD_SECONDS = 7LL * 24 * 60 * 60;
// //
// `outErrorMessage` is an optional out-param. // `outErrorMessage` is an optional out-param.
Result validateStoredLicense(radiantwave::db::DatabaseManager& db, Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
const std::string& apiKey = "",
std::string* outErrorMessage = nullptr); std::string* outErrorMessage = nullptr);
// Records the current time as the "last verified" stamp of the license. Called // Records the current time as the "last verified" stamp of the license. Called
+4 -6
View File
@@ -190,10 +190,9 @@ bool parseProclamationPalettesJson(const std::string& body,
return true; return true;
} }
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db, bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db) {
const std::string& apiKey) {
std::vector<api::ColorPalette> palettes; std::vector<api::ColorPalette> palettes;
if (!api::fetchColorPalettes(palettes, apiKey)) { if (!api::fetchColorPalettes(palettes)) {
return false; return false;
} }
@@ -211,9 +210,8 @@ bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db,
return db.replaceColorPalettes(records); return db.replaceColorPalettes(records);
} }
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db, bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db) {
const std::string& apiKey) { api::HttpResponse response = api::httpGet(api::PATH_PROCLAMATIONS);
api::HttpResponse response = api::httpGet(api::PATH_PROCLAMATIONS, apiKey);
if (!response.transportOk || response.status < 200 || response.status >= 300) { if (!response.transportOk || response.status < 200 || response.status >= 300) {
return false; return false;
} }
+2 -4
View File
@@ -37,13 +37,11 @@ bool parseProclamationPalettesJson(const std::string& body,
// NOTE: not yet wired into the app flow — the portal does not expose the route // NOTE: not yet wired into the app flow — the portal does not expose the route
// yet. Readiness code: palettes should be updated from the portal, not code // yet. Readiness code: palettes should be updated from the portal, not code
// releases, so callers should treat this as a future sync step. // releases, so callers should treat this as a future sync step.
bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db, bool syncColorPalettesFromApi(radiantwave::db::DatabaseManager& db);
const std::string& apiKey = "");
// Alternative to syncColorPalettesFromApi: fetch proclamations and replace the // Alternative to syncColorPalettesFromApi: fetch proclamations and replace the
// local palette table using the palettes embedded in each proclamation. // local palette table using the palettes embedded in each proclamation.
// NOTE: not yet wired into the app flow; see syncColorPalettesFromApi. // NOTE: not yet wired into the app flow; see syncColorPalettesFromApi.
bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db, bool syncProclamationPalettesFromApi(radiantwave::db::DatabaseManager& db);
const std::string& apiKey = "");
} // namespace radiantwave::sync } // namespace radiantwave::sync
+9 -14
View File
@@ -19,11 +19,6 @@ std::string env(const char* name) {
return value ? std::string(value) : std::string(); return value ? std::string(value) : std::string();
} }
// Returns the optional live API key, or "" when none was provided.
std::string liveApiKey() {
return env("RW_LIVE_API_KEY");
}
} // namespace } // namespace
// ─── Offline: parse functions ───────────────────────────────────────── // ─── Offline: parse functions ─────────────────────────────────────────
@@ -163,7 +158,7 @@ TEST(ApiClientTest, ParseCategoriesHandlesNullParentId) {
TEST(ApiClientLiveTest, FetchFrequenciesLive) { TEST(ApiClientLiveTest, FetchFrequenciesLive) {
std::vector<Frequency> out; std::vector<Frequency> out;
EXPECT_TRUE(fetchFrequencies(out, liveApiKey())); EXPECT_TRUE(fetchFrequencies(out));
ASSERT_GE(out.size(), 7u); ASSERT_GE(out.size(), 7u);
bool foundSolarPlexus = false; bool foundSolarPlexus = false;
@@ -180,7 +175,7 @@ TEST(ApiClientLiveTest, FetchFrequenciesLive) {
// ─── Live: proclamation + category parsers against the real API ──────── // ─── Live: proclamation + category parsers against the real API ────────
TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) { TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
HttpResponse procsResponse = httpGet(PATH_PROCLAMATIONS, liveApiKey()); HttpResponse procsResponse = httpGet(PATH_PROCLAMATIONS);
ASSERT_TRUE(procsResponse.transportOk); ASSERT_TRUE(procsResponse.transportOk);
ASSERT_EQ(procsResponse.status, 200); ASSERT_EQ(procsResponse.status, 200);
@@ -193,7 +188,7 @@ TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
EXPECT_FALSE(proclamations[0].id.empty()); EXPECT_FALSE(proclamations[0].id.empty());
} }
HttpResponse catsResponse = httpGet(PATH_CATEGORIES, liveApiKey()); HttpResponse catsResponse = httpGet(PATH_CATEGORIES);
ASSERT_TRUE(catsResponse.transportOk); ASSERT_TRUE(catsResponse.transportOk);
ASSERT_EQ(catsResponse.status, 200); ASSERT_EQ(catsResponse.status, 200);
@@ -205,14 +200,14 @@ TEST(ApiClientLiveTest, ProclamationsAndCategoriesLiveSmoke) {
// ─── Live: POST /license error paths (no credentials required) ───────── // ─── Live: POST /license error paths (no credentials required) ─────────
TEST(ApiClientLiveTest, ValidateLicenseMissingKeyLive) { TEST(ApiClientLiveTest, ValidateLicenseMissingKeyLive) {
LicenseResult result = validateLicense("", "test@example.com", liveApiKey()); LicenseResult result = validateLicense("", "test@example.com");
ASSERT_FALSE(result.ok()); ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400); EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "license_key is required"); EXPECT_EQ(result.error_message, "license_key is required");
} }
TEST(ApiClientLiveTest, ValidateLicenseMissingEmailLive) { TEST(ApiClientLiveTest, ValidateLicenseMissingEmailLive) {
LicenseResult result = validateLicense("AAAA-AAAA-AAAA-AAAA", "", liveApiKey()); LicenseResult result = validateLicense("AAAA-AAAA-AAAA-AAAA", "");
ASSERT_FALSE(result.ok()); ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400); EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "email_address is required"); EXPECT_EQ(result.error_message, "email_address is required");
@@ -220,7 +215,7 @@ TEST(ApiClientLiveTest, ValidateLicenseMissingEmailLive) {
TEST(ApiClientLiveTest, ValidateLicenseUnknownKeyLive) { TEST(ApiClientLiveTest, ValidateLicenseUnknownKeyLive) {
LicenseResult result = LicenseResult result =
validateLicense("AAAA-AAAA-AAAA-AAAA", "nobody@example.com", liveApiKey()); validateLicense("AAAA-AAAA-AAAA-AAAA", "nobody@example.com");
ASSERT_FALSE(result.ok()); ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400); EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "license key not found"); EXPECT_EQ(result.error_message, "license key not found");
@@ -235,7 +230,7 @@ TEST(ApiClientLiveTest, ValidateLicenseSuccessLive) {
GTEST_SKIP() << "RW_LIVE_LICENSE_KEY / RW_LIVE_LICENSE_EMAIL not set"; GTEST_SKIP() << "RW_LIVE_LICENSE_KEY / RW_LIVE_LICENSE_EMAIL not set";
} }
LicenseResult result = validateLicense(key, email, liveApiKey()); LicenseResult result = validateLicense(key, email);
ASSERT_TRUE(result.ok()) << "status=" << result.status ASSERT_TRUE(result.ok()) << "status=" << result.status
<< " error=" << result.error_message; << " error=" << result.error_message;
EXPECT_EQ(result.info.key, key); EXPECT_EQ(result.info.key, key);
@@ -245,7 +240,7 @@ TEST(ApiClientLiveTest, ValidateLicenseSuccessLive) {
// ─── Live: POST /automations/create/account error paths (harmless) ───── // ─── Live: POST /automations/create/account error paths (harmless) ─────
TEST(ApiClientLiveTest, CreateAccountMissingEmailLive) { TEST(ApiClientLiveTest, CreateAccountMissingEmailLive) {
AccountResult result = createAccount("", "", liveApiKey()); AccountResult result = createAccount("", "");
ASSERT_FALSE(result.ok()); ASSERT_FALSE(result.ok());
EXPECT_EQ(result.status, 400); EXPECT_EQ(result.status, 400);
EXPECT_EQ(result.error_message, "email is required"); EXPECT_EQ(result.error_message, "email is required");
@@ -260,7 +255,7 @@ TEST(ApiClientLiveTest, CreateAccountSuccessLive) {
GTEST_SKIP() << "RW_LIVE_ACCOUNT_EMAIL not set"; GTEST_SKIP() << "RW_LIVE_ACCOUNT_EMAIL not set";
} }
AccountResult result = createAccount(email, "RadiantWave Test", liveApiKey()); AccountResult result = createAccount(email, "RadiantWave Test");
ASSERT_TRUE(result.ok()) << "status=" << result.status ASSERT_TRUE(result.ok()) << "status=" << result.status
<< " error=" << result.error_message; << " error=" << result.error_message;
EXPECT_TRUE(result.body.success); EXPECT_TRUE(result.body.success);
+1 -1
View File
@@ -175,7 +175,7 @@ TEST(ColorPalettesLiveTest, FetchColorPalettesLive) {
} }
std::vector<ColorPalette> palettes; std::vector<ColorPalette> palettes;
ASSERT_TRUE(fetchColorPalettes(palettes, env("RW_LIVE_API_KEY"))); ASSERT_TRUE(fetchColorPalettes(palettes));
EXPECT_FALSE(palettes.empty()); EXPECT_FALSE(palettes.empty());
for (const auto& p : palettes) { for (const auto& p : palettes) {
EXPECT_GE(p.colors.size(), 1u); EXPECT_GE(p.colors.size(), 1u);
+4 -4
View File
@@ -48,7 +48,7 @@ TEST(LicenseServiceTest, NotConfiguredWhenNoEmail) {
tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP"); tmp.db.setKV(KV_LICENSE_KEY, "ABCD-EFGH-IJKL-MNOP");
std::string errorMessage = "stale"; std::string errorMessage = "stale";
EXPECT_EQ(validateStoredLicense(tmp.db, "", &errorMessage), EXPECT_EQ(validateStoredLicense(tmp.db, &errorMessage),
Result::NotConfigured); Result::NotConfigured);
EXPECT_EQ(errorMessage, ""); EXPECT_EQ(errorMessage, "");
} }
@@ -57,7 +57,7 @@ TEST(LicenseServiceTest, NotConfiguredWhenNoLicense) {
TempDb tmp; TempDb tmp;
tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com"); tmp.db.setKV(KV_EMAIL_ADDRESS, "user@example.com");
EXPECT_EQ(validateStoredLicense(tmp.db, "", nullptr), EXPECT_EQ(validateStoredLicense(tmp.db, nullptr),
Result::NotConfigured); Result::NotConfigured);
} }
@@ -117,7 +117,7 @@ TEST(LicenseServiceLiveTest, ValidatesAndGrantsSubscribedState) {
tmp.db.setKV(KV_EMAIL_ADDRESS, email); tmp.db.setKV(KV_EMAIL_ADDRESS, email);
std::string errorMessage; std::string errorMessage;
const Result result = validateStoredLicense(tmp.db, "", &errorMessage); const Result result = validateStoredLicense(tmp.db, &errorMessage);
EXPECT_EQ(result, Result::Ok) << "error=" << errorMessage; EXPECT_EQ(result, Result::Ok) << "error=" << errorMessage;
// A successful validation is the "subscribed" flag; no subscription-level // A successful validation is the "subscribed" flag; no subscription-level
@@ -134,7 +134,7 @@ TEST(LicenseServiceLiveTest, InvalidPairReportsInvalid) {
tmp.db.setKV(KV_EMAIL_ADDRESS, "nobody@example.com"); tmp.db.setKV(KV_EMAIL_ADDRESS, "nobody@example.com");
std::string errorMessage; std::string errorMessage;
EXPECT_EQ(validateStoredLicense(tmp.db, "", &errorMessage), EXPECT_EQ(validateStoredLicense(tmp.db, &errorMessage),
Result::Invalid); Result::Invalid);
EXPECT_FALSE(errorMessage.empty()); EXPECT_FALSE(errorMessage.empty());
} }