Skip to content

chore: serve OpenAPI spec at /openapi.yml and /openapi.json#4

Open
jjmaxwell4 wants to merge 79 commits into
mainfrom
chore/serve-openapi-spec
Open

chore: serve OpenAPI spec at /openapi.yml and /openapi.json#4
jjmaxwell4 wants to merge 79 commits into
mainfrom
chore/serve-openapi-spec

Conversation

@jjmaxwell4

Copy link
Copy Markdown

Summary

  • Serves the OpenAPI spec at two public endpoints: /openapi.yml (YAML) and /openapi.json (JSON)
  • Spec is read once at startup and cached in memory — zero runtime overhead per request
  • Routes are placed before auth middleware so they're publicly accessible

Test plan

  • Hit GET /openapi.yml and verify YAML response with correct Content-Type: text/yaml
  • Hit GET /openapi.json and verify valid JSON response

Made with Cursor

og2701 and others added 30 commits March 19, 2026 10:45
These files are in .gitignore and should not be committed — migrations are generated locally via db:push.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
docs: fix auto top-up dashboard instructions and remove duplicate set…
docs: fix auto top-up dashboard setup instructions
Introduce useQueryKeyFactory hook that appends [env, orgId] to all
React Query keys. Remove queryClient.clear() from env switch and
window.location.reload() from org switch. Redirect to sandbox path
when switching to a sandbox-only org.

Made-with: Cursor
…visibility

Move deployed-org check into EnvDropdown so non-deployed orgs see a
non-interactive StaticEnvPill instead of hiding the env indicator entirely.
Also adjust dark-mode shimmer hover opacity.

Made-with: Cursor
Track the last-switched org ID so the redirect effect skips firing
when the React Query cache has stale data from a previously active
non-deployed org. Also refactors useOrgSwitch to eagerly fetch and
set the new org data instead of relying on invalidateQueries alone.

Made-with: Cursor
fix: env/org switch redirect and stale cache handling
…/env switch

SWR queries were invisible to queryClient.invalidateQueries() and
lacked org/env scoping via useQueryKeyFactory, causing stale data on
the events page and other views when switching orgs or environments.
Removes the swr dependency entirely.

Made-with: Cursor
fix: migrate all SWR queries to React Query to fix stale cache on org/env switch
johnyeocx and others added 30 commits March 23, 2026 12:56
…-threshold-migration-and-test

feature: usage alerts existing threshold migration and test
## Summary
<!-- Provide a short summary of your changes and the motivation behind them. -->

## Related Issues
<!-- List any related issues, e.g. Fixes #123 or Closes #456 -->

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Refactor
- [ ] Other (please describe):

## Checklist
- [ ] I have read the [CONTRIBUTING.md](https://github.com/useautumn/autumn/blob/staging/.github/CONTRIBUTING.md)
- [ ] My code follows the code style of this project
- [ ] I have added tests where applicable
- [ ] I have tested my changes locally
- [ ] I have linked relevant issues
- [ ] I have added screenshots for UI changes (if applicable)

## Screenshots (if applicable)
<!-- Add before/after screenshots or GIFs here -->

## Additional Context
<!-- Add any other context or information about the PR here --> 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds a per-customer path index in Redis to speed up entitlement lookups and updates FullCustomer cache Lua scripts to build keys internally and replace the index atomically.

- **New Features**
  - Stores a Hash path index at `{orgId}:{env}:fullcustomer:pathidx:{customerId}`.
  - Index built via `buildPathIndex` mapping `ent:{id}` to `{"p": jsonPath, "ef": entityFeatureId}`; TTL matches the cache.
  - Deleting a FullCustomer also deletes its path index.

- **Refactors**
  - Lua scripts now construct cache/guard/test and path-index keys internally via shared builders; `numberOfKeys` reduced to 1 for cluster-slot stability.
  - Injects `FULL_CUSTOMER_CACHE_VERSION` into Lua at load time.
  - Redis command signatures updated:
    - `setFullCustomerCache(cacheKey, orgId, env, customerId, fetchTimeMs, cacheTtl, json, overwrite, pathIndexJson)`
    - `deleteFullCustomerCache(cacheKey, orgId, env, customerId, guardTimestamp, guardTtl, skipGuard)`

<sup>Written for commit 1eea322. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- greptile_comment -->

<details><summary><h3>Greptile Summary</h3></summary>

This PR introduces a per-customer **path index** in Redis (a Hash at `{orgId}:{env}:fullcustomer:pathidx:{customerId}`) that maps entitlement IDs to their JSON paths inside the FullCustomer cache, enabling fast entitlement lookups without scanning the whole document. The Lua scripts for set/delete are refactored to build all keys internally from `orgId`/`env`/`customerId`, reducing `numberOfKeys` to 1 for Redis Cluster slot stability.

**Key changes:**

- **Improvements** — New `fullCustomerKeyBuilders.lua` shared module centralises all cache key construction inside Lua; `FULL_CUSTOMER_CACHE_VERSION` is injected at load time via string replacement, keeping TypeScript and Lua in sync with a single source of truth.
- **Improvements** — `setFullCustomerCache` Lua script now atomically replaces the path index (DEL → HSET → EXPIRE) alongside the main cache write; `deleteFullCustomerCache` unconditionally cleans up the path index alongside the cache key.
- **API changes** — `deleteFullCustomerCache` and `setFullCustomerCache` Redis command signatures updated: keys previously built in TypeScript and passed as `KEYS` args are now derived inside Lua; `pathIndexJson` added as the final ARGV to the set command.
- **Improvements** — New `buildPathIndex.ts` constructs the path index Record from a `FullCustomer` object, covering both `customer_products` entitlements and `extra_customer_entitlements`.
- **Bug fixes** (minor) — `FULL_CUSTOMER_CACHE_VERSION` is now exported so it can be consumed outside `fullCustomerCacheConfig.ts` without duplicating the constant.

Two minor findings:
- `pathIndexConfig.ts` exports `buildPathIndexKey` but is never imported anywhere — appears to be unused dead code.
- `buildPathIndex.ts` uses a flat Record keyed by `ent:{id}`; duplicate entitlement IDs across different `customer_products` (or between `customer_products` and `extra_customer_entitlements`) would silently overwrite earlier entries with incorrect paths.
</details>

<h3>Confidence Score: 4/5</h3>

- Safe to merge; the path index is built and cleaned up atomically and all cluster-slot constraints are respected. Two non-blocking style/logic findings remain.
- The core logic — atomic path index write alongside the cache, single-key cluster routing via shared hash tags, version injection — is correct. The two findings are (1) an unused exported function in a new file (dead code, no runtime impact) and (2) a potential silent overwrite in the index builder if entitlement IDs are not globally unique across a FullCustomer, which depends on a data-model invariant. Neither blocks the happy path.
- server/src/internal/customers/cache/pathIndex/buildPathIndex.ts (duplicate-ID handling) and server/src/internal/customers/cache/pathIndex/pathIndexConfig.ts (unused export).

<h3>Important Files Changed</h3>

| Filename | Overview |
|----------|----------|
| server/src/_luaScriptsV2/fullCustomerKeyBuilders.lua | New shared Lua module defining key-builder functions prepended to both set/delete scripts. All keys use `{orgId}` as the hash tag, ensuring cluster-slot co-location. Version placeholder correctly injected at load time. |
| server/src/_luaScriptsV2/deleteFullCustomerCache/setFullCustomerCache.lua | Refactored to accept orgId/env/customerId as ARGV and build all keys internally. Correctly atomically replaces the path index (DEL + HSET + EXPIRE) after the main cache write. Early returns (STALE_WRITE / CACHE_EXISTS) correctly skip path index writes. |
| server/src/_luaScriptsV2/deleteFullCustomerCache/deleteFullCustomerCache.lua | Refactored similarly; unconditionally deletes the path index key alongside the cache key — correct clean-up even in the NOT_FOUND case. |
| server/src/external/redis/initRedis.ts | numberOfKeys reduced to 1 for both commands; TypeScript signatures updated to match new ARGV layout. Correct given all keys now share the orgId hash tag. |
| server/src/internal/customers/cache/pathIndex/buildPathIndex.ts | Builds the path index Record from a FullCustomer. Potential silent overwrite if duplicate entitlement IDs exist across customer_products or extra_customer_entitlements. |
| server/src/internal/customers/cache/pathIndex/pathIndexConfig.ts | New file exporting buildPathIndexKey, but this function is never imported anywhere — unused dead code. |

</details>

<details><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
    participant TS as TypeScript (setCachedFullCustomer)
    participant Lua as setFullCustomerCache.lua
    participant Redis as Redis

    TS->>TS: buildPathIndex(fullCustomer) → entries Record
    TS->>TS: JSON.stringify(entries) → pathIndexJson
    TS->>Lua: setFullCustomerCache(cacheKey, orgId, env, customerId, fetchTimeMs, cacheTtl, serializedData, overwrite, pathIndexJson)

    Lua->>Lua: build_guard_key / build_full_customer_cache_key / build_path_index_key

    Lua->>Redis: GET guardKey
    alt guard newer than fetchTimeMs
        Lua-->>TS: "STALE_WRITE"
    else cache exists and overwrite=false
        Lua->>Redis: JSON.TYPE cacheKey
        Lua-->>TS: "CACHE_EXISTS"
    else
        Lua->>Redis: JSON.SET cacheKey "$" serializedData
        Lua->>Redis: EXPIRE cacheKey cacheTtl
        Lua->>Redis: DEL pathIdxKey
        Lua->>Redis: HSET pathIdxKey field1 val1 ...
        Lua->>Redis: EXPIRE pathIdxKey cacheTtl
        Lua-->>TS: "OK"
    end
```
</details>

<details><summary>Prompt To Fix All With AI</summary>

`````markdown
This is a comment left during a code review.
Path: server/src/internal/customers/cache/pathIndex/pathIndexConfig.ts
Line: 1-12

Comment:
**`buildPathIndexKey` is unused dead code**

`buildPathIndexKey` is exported from this new file but never imported anywhere in the codebase. Key construction for the path index is now handled entirely inside Lua via `build_path_index_key` in `fullCustomerKeyBuilders.lua`. 

This file can be removed, or if it's intended as a TypeScript-side helper for future reads from the path index, that should be noted with a comment.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: server/src/internal/customers/cache/pathIndex/buildPathIndex.ts
Line: 14-50

Comment:
**Silent overwrite on duplicate entitlement IDs**

Both the `customer_products` loop and the `extra_customer_entitlements` loop write to the same flat `entries` record keyed by `ent:${id}`. If the same entitlement `id` appears more than once — either in two different `customer_products` or also in `extra_customer_entitlements` — the later iteration silently overwrites the earlier one, leaving a stale path in the index and potentially directing lookups to the wrong JSON path.

If the data model guarantees globally unique IDs across all entitlements, this is fine, but it would be safer to guard against this explicitly (or at least add an assertion/log for the duplicate case).

How can I resolve this? If you propose a fix, please make it concise.
`````

</details>

<sub>Reviews (1): Last reviewed commit: ["M"](useautumn/autumn@1eea322) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=26172742)</sub>

> Greptile also left **2 inline comments** on this PR.

<!-- /greptile_comment -->
## Summary
<!-- Provide a short summary of your changes and the motivation behind them. -->

## Related Issues
<!-- List any related issues, e.g. Fixes #123 or Closes #456 -->

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Refactor
- [ ] Other (please describe):

## Checklist
- [ ] I have read the [CONTRIBUTING.md](https://github.com/useautumn/autumn/blob/staging/.github/CONTRIBUTING.md)
- [ ] My code follows the code style of this project
- [ ] I have added tests where applicable
- [ ] I have tested my changes locally
- [ ] I have linked relevant issues
- [ ] I have added screenshots for UI changes (if applicable)

## Screenshots (if applicable)
<!-- Add before/after screenshots or GIFs here -->

## Additional Context
<!-- Add any other context or information about the PR here --> 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Switch deduction to resolve entitlement JSON paths via a Redis entitlement index with a safe fallback, and fix spend-limit fallback so limits work when the index is missing. This reduces JSON.GET calls and speeds up deductions for large customers.

- **Refactors**
  - Deduction uses entitlement index entries (`cus_ent:<id>` → `{ cp, ce }` or `{ ece }` with `ef`) to build base paths and fetch sub-objects with one JSON.GET.
  - Full customer JSON is only decoded when the index is absent; `org_id`, `env`, and `customer_id` are passed so Lua builds and checks the path-index key.
  - Context reads balances, entities, and rollovers from the fetched sub-object and carries `pathidx_key`/`has_pathidx`; lock receipts use the passed `customer_id`.
  - FullCustomer helpers moved to `fullCustomer/fullCustomerUtils.lua` and included across scripts; `buildPathIndex` now stores indices and `entity_feature_id` with `cus_ent:` keys.
  - Added perf tooling: Artillery scenario, Redis seeding script, and a benchmark that compares fast-path vs fallback.

- **Bug Fixes**
  - Spend-limit lookup now gates the fast path on `has_pathidx` and cleanly falls back to the full-customer scan when the index is missing, restoring enforcement for older caches.

<sup>Written for commit 011e08f. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- greptile_comment -->

<details><summary><h3>Greptile Summary</h3></summary>

This PR refactors the Lua deduction script to use a pre-built Redis Hash path index (`cus_ent:<id>` → `{ cp, ce, ef }`) for entitlement lookups, replacing the previous approach of decoding the entire FullCustomer JSON and scanning it in Lua. A safe fallback to the full-scan path is retained when the index is absent.

**Key changes:**

- **Improvements** — `fullCustomerUtils.lua` is introduced as a new module containing `find_entitlement_from_index` and `get_customer_entitlement_via_index`; the deduction script now constructs a `pathidx_key` via `build_path_index_key` and gates the expensive `JSON.GET '.'` behind `has_pathidx = false`, reducing per-deduction Redis I/O for large customers.
- **Improvements** — `contextUtils.lua` reads balance, entities, and rollover data directly from the sub-object fetched in the single `JSON.GET` call, eliminating separate `read_current_balance`, `read_current_entities`, and `read_rollover_data` round-trips.
- **Improvements** — `buildPathIndex.ts` switches from storing full JSON paths (`{ p: "$.customer_products[0]…" }`) to compact index tuples (`{ cp, ce, ef }` / `{ ece, ef }`), reducing Hash entry size and moving path construction to Lua.
- **Improvements** — `luaScriptsV2.ts` moves `FULL_CUSTOMER_KEY_BUILDERS` loading before script assembly, changes `.replace()` to `.replaceAll()` (correct for multiple placeholder occurrences), and adds `FULL_CUSTOMER_UTILS` to all scripts that depend on the moved helpers.
- **Improvements** — `executeRedisDeduction.ts` passes `org_id`, `env`, and `customer_id` to the Lua params so the script can construct the path index key without relying on the decoded full customer object.
- **Improvements** — Artillery load test and `temp.test.ts` benchmark added to validate fast-path vs fallback performance at scale (100 entities × 10 cusEnts).

**Issue found:**

- In `spendLimitUtils.lua`, the `full_customer` fallback is dead code: `pathidx_key` is always a non-nil string, so `not is_nil(pathidx_key)` is always `true`, and the `if is_nil(result) then return nil end` guard fires an early return when the path index is missing — the fallback block is never reached. This silently disables spend-limit enforcement for customers whose caches predate path index support. See inline comment for details.
</details>

<h3>Confidence Score: 3/5</h3>

- The core deduction fast path and fallback in contextUtils.lua are correct, but spend-limit computation is broken for customers without a path index due to an unreachable fallback in spendLimitUtils.lua.
- The main deduction path (contextUtils.lua) correctly gates fast vs fallback on `has_pathidx` and the architecture is sound. However, spendLimitUtils.lua has the same dual-path structure but uses `not is_nil(pathidx_key)` (always true) instead of `has_pathidx`, making its full_customer fallback dead code. Any customer cache written before path indices existed will have spend-limit enforcement silently disabled. This is a correctness bug on a billing-sensitive code path that should be fixed before merging to a production branch.
- server/src/_luaScriptsV2/deductFromCustomerEntitlements/spendLimitUtils.lua

<h3>Important Files Changed</h3>

| Filename | Overview |
|----------|----------|
| server/src/_luaScriptsV2/deductFromCustomerEntitlements/spendLimitUtils.lua | Adds fast-path lookup via path index, but the full-customer fallback is unreachable because the `pathidx_key` nil-check is always true — when the path index is absent the function returns nil early instead of using `full_customer`, breaking spend-limit computation for caches without a path index. |
| server/src/_luaScriptsV2/fullCustomer/fullCustomerUtils.lua | New module extracting FullCustomer navigation helpers from luaUtils.lua. Adds `find_entitlement_from_index` (HGET + cjson decode) and `get_customer_entitlement_via_index` (HGET + JSON.GET + unwrap). JSONPath array-unwrap logic (sub[1]) is correct for the `$`-prefixed paths used. Looks correct. |
| server/src/_luaScriptsV2/deductFromCustomerEntitlements/contextUtils.lua | Correctly uses `has_pathidx` boolean to choose between fast path (HGET + JSON.GET sub-object) and full-customer fallback. Rollover data is now read from the already-fetched sub-object rather than a separate `read_rollover_data` call, reducing Redis round-trips. |
| server/src/_luaScriptsV2/deductFromCustomerEntitlements/deductFromCustomerEntitlements.lua | Correctly adds org_id/env/customer_id params and constructs path index key, gates full-customer decode behind `not has_pathidx`, and replaces `full_customer.id` reference (which would be nil in fast-path mode) with the explicitly passed `customer_id` in the lock receipt. |
| server/src/internal/customers/cache/pathIndex/buildPathIndex.ts | Changed hash entry keys from `ent:<id>` to `cus_ent:<id>` and values from `{ p: fullPath, ef }` to `{ cp, ce, ef }` / `{ ece, ef }` (indices instead of pre-built paths). Consistent with the Lua `find_entitlement_from_index` function and smaller payload size. |
| server/src/_luaScriptsV2/luaScriptsV2.ts | Moved FULL_CUSTOMER_KEY_BUILDERS loading earlier (before it's used in the deduct script), changed `.replace()` to `.replaceAll()` (correct improvement), and added FULL_CUSTOMER_UTILS to all scripts that previously relied on moved helper functions. |
| server/src/internal/balances/utils/deduction/executeRedisDeduction.ts | Adds `org_id`, `env`, and `customer_id` to the Lua params — minimal correct change needed for the Lua script to build the path index key on the Redis side. |
| server/tests/_temp/temp.test.ts | Previous temp test replaced with a large-customer perf benchmark (100 entities × 10 cusEnts). Tests seed Redis with and without a path index, measure SLOWLOG timings and produce a comparison table. Benchmark-quality test file; not part of the regular test suite. |
| server/perf/redis-bench/setup.ts | Standalone seeding script for Artillery benchmarks. Creates a 100-entity FullCustomer in Redis and optionally registers the customer via the Autumn API. Correct and well-structured. |

</details>

<details><summary><h3>Flowchart</h3></summary>

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["deductFromCustomerEntitlements.lua\nKEYS[1] = cache_key\nARGV[1] = { org_id, env, customer_id, … }"] --> B{"EXISTS cache_key?"}
    B -- No --> ERR1["return CUSTOMER_NOT_FOUND"]
    B -- Yes --> C["build_path_index_key(org_id, env, customer_id)"]
    C --> D{"EXISTS pathidx_key?"}
    D -- No (fallback) --> E["JSON.GET cache_key '.'\n→ full_customer"]
    D -- Yes (fast path) --> F["(skip full JSON decode)"]
    E --> G["init_context: find_entitlement(full_customer, id)\nbuild base_path from Lua indices"]
    F --> H["init_context: get_customer_entitlement_via_index\nHGET pathidx_key 'cus_ent:<id>'\n→ { cp, ce, ef } or { ece, ef }\nJSON.GET cache_key base_path → sub-object"]
    G --> I["Deduction passes\napply_pending_writes\nJSON.NUMINCRBY per balance field"]
    H --> I
    I --> J["return { updates, rollover_updates, mutation_logs }"]
```
</details>

<details><summary>Prompt To Fix All With AI</summary>

`````markdown
This is a comment left during a code review.
Path: server/src/_luaScriptsV2/deductFromCustomerEntitlements/spendLimitUtils.lua
Line: 17-38

Comment:
**Fallback path is dead code — spend limit breaks without path index**

`pathidx_key` is always a non-nil Lua string (constructed via `build_path_index_key` and stored in `context.pathidx_key`), so `not is_nil(pathidx_key)` is permanently `true`. When `get_customer_entitlement_via_index` returns `nil` — which happens every time the path index Hash doesn't exist in Redis — the guard `if is_nil(result) then return nil end` fires an early return, and the `full_customer` fallback block below (lines 40–58) is never reached.

**Impact:** When `has_pathidx = false` (caches written before path indices were supported), any call to `build_ent_data_from_full_customer` for a `usage_based_cus_ent_id` that is not already in `context.customer_entitlements` returns `nil` instead of looking the entitlement up in `full_customer`. This causes `total_overage` to be underreported in `get_available_overage_from_spend_limit`, making `available_overage` appear larger than reality and effectively disabling spend-limit enforcement for those customers.

**Root cause:** The function should either (a) only enter the fast-path block when `context.has_pathidx` is `true` (matching the pattern used in `contextUtils.lua`), or (b) only early-return on a nil fast-path result and fall through to the `full_customer` block otherwise.

Compare with how `contextUtils.lua` handles the same two paths correctly:
```lua
if has_pathidx then
  -- fast path
else
  -- full_customer fallback
end
```

The simplest fix for `spendLimitUtils.lua` is to invert the early return so a nil result falls through rather than short-circuits:

```lua
if not is_nil(pathidx_key) then
  local result = get_customer_entitlement_via_index({ ... })
  if not is_nil(result) then  -- only return when we actually got data
    ...
    return { ... }
  end
  -- nil result: fall through to full_customer fallback
end
```

How can I resolve this? If you propose a fix, please make it concise.
`````

</details>

<sub>Reviews (1): Last reviewed commit: ["fix: use entitlement index for json path"](useautumn/autumn@58f6618) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=26172641)</sub>

> Greptile also left **1 inline comment** on this PR.

<!-- /greptile_comment -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants