mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-07 15:32:10 +03:00
Compare commits
12 Commits
release/v3
...
fix/check-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5b3f94e1 | ||
|
|
dea6bb8b6b | ||
|
|
8e2fb04329 | ||
|
|
b7c07edad8 | ||
|
|
0ce21232db | ||
|
|
b65ef333da | ||
|
|
925feb27b8 | ||
|
|
aa52351113 | ||
|
|
8778ea7d18 | ||
|
|
c44c0a29e8 | ||
|
|
5458026c21 | ||
|
|
b4ec7807ab |
@@ -1023,6 +1023,13 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# to disable the check. Used by: src/lib/db/migrationRunner.ts. Default: 50.
|
||||
#OMNIROUTE_MAX_PENDING_MIGRATIONS=50
|
||||
|
||||
# Working directory for the check:install-upgrade release gate. It builds two ~3 GB
|
||||
# install trees plus a ~275 MB tarball, so it needs roughly 12 GB — more than the
|
||||
# 12 GB RAM-backed tmpfs that /tmp is on the self-hosted runner, where it exhausted
|
||||
# the tmpfs and npm silently truncated the package. Defaults to <repo>/.install-upgrade
|
||||
# on real disk. Used by: scripts/check/check-install-upgrade.mjs. Default: <repo>/.install-upgrade.
|
||||
#OMNIROUTE_INSTALL_UPGRADE_WORKDIR=/var/tmp/omniroute-install-upgrade
|
||||
|
||||
# Trust user-managed RTK project filter rules without strict signature checks.
|
||||
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
|
||||
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
|
||||
|
||||
63
.github/workflows/docker-publish.yml
vendored
63
.github/workflows/docker-publish.yml
vendored
@@ -185,6 +185,13 @@ jobs:
|
||||
|
||||
- name: Build and push BUN base platform image by digest
|
||||
id: build-bun-base
|
||||
# Bun is a best-effort compatibility target, not a supported runtime
|
||||
# (AGENTS.md -> Environment). Its `bun run build` has been OOM-killing on
|
||||
# both arches; letting that sink the whole publish means the SUPPORTED
|
||||
# runner-base / runner-web images never reach the registry either. The
|
||||
# image is still built and pushed whenever it succeeds — only its power to
|
||||
# block the release is removed.
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .
|
||||
@@ -203,6 +210,13 @@ jobs:
|
||||
|
||||
- name: Build and push BUN web platform image by digest
|
||||
id: build-bun-web
|
||||
# Bun is a best-effort compatibility target, not a supported runtime
|
||||
# (AGENTS.md -> Environment). Its `bun run build` has been OOM-killing on
|
||||
# both arches; letting that sink the whole publish means the SUPPORTED
|
||||
# runner-base / runner-web images never reach the registry either. The
|
||||
# image is still built and pushed whenever it succeeds — only its power to
|
||||
# block the release is removed.
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .
|
||||
@@ -230,8 +244,15 @@ jobs:
|
||||
mkdir -p /tmp/digests/base /tmp/digests/web /tmp/digests/bun-base /tmp/digests/bun-web
|
||||
touch "/tmp/digests/base/${DIGEST_BASE#sha256:}"
|
||||
touch "/tmp/digests/web/${DIGEST_WEB#sha256:}"
|
||||
touch "/tmp/digests/bun-base/${DIGEST_BUN_BASE#sha256:}"
|
||||
touch "/tmp/digests/bun-web/${DIGEST_BUN_WEB#sha256:}"
|
||||
# Empty when the (non-blocking) bun build produced no image. `if` blocks,
|
||||
# not `[ -n ] && touch`: under `set -e` a failing AND-list aborts the step,
|
||||
# which is precisely the case being handled here.
|
||||
if [ -n "$DIGEST_BUN_BASE" ]; then
|
||||
touch "/tmp/digests/bun-base/${DIGEST_BUN_BASE#sha256:}"
|
||||
fi
|
||||
if [ -n "$DIGEST_BUN_WEB" ]; then
|
||||
touch "/tmp/digests/bun-web/${DIGEST_BUN_WEB#sha256:}"
|
||||
fi
|
||||
|
||||
- name: Upload base digests
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -254,7 +275,11 @@ jobs:
|
||||
with:
|
||||
name: digests-bun-base-${{ matrix.arch }}
|
||||
path: /tmp/digests/bun-base/*
|
||||
if-no-files-found: error
|
||||
# `ignore`, not `error`: the bun build is non-blocking, so an absent
|
||||
# digest is the expected outcome of a failed/skipped bun image — the
|
||||
# manifest step already treats these tags as optional. Leaving `error`
|
||||
# here just relocates the blocker from the manifest to the upload.
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload bun-web digests
|
||||
@@ -262,7 +287,11 @@ jobs:
|
||||
with:
|
||||
name: digests-bun-web-${{ matrix.arch }}
|
||||
path: /tmp/digests/bun-web/*
|
||||
if-no-files-found: error
|
||||
# `ignore`, not `error`: the bun build is non-blocking, so an absent
|
||||
# digest is the expected outcome of a failed/skipped bun image — the
|
||||
# manifest step already treats these tags as optional. Leaving `error`
|
||||
# here just relocates the blocker from the manifest to the upload.
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
@@ -320,6 +349,9 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download bun-base digests
|
||||
# Non-blocking: the bun image is best-effort, so its artifact may not
|
||||
# exist at all. The manifest step treats these tags as optional.
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: digests-bun-base-*
|
||||
@@ -327,6 +359,9 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download bun-web digests
|
||||
# Non-blocking: the bun image is best-effort, so its artifact may not
|
||||
# exist at all. The manifest step treats these tags as optional.
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: digests-bun-web-*
|
||||
@@ -338,7 +373,7 @@ jobs:
|
||||
set -euo pipefail
|
||||
|
||||
create_manifest() {
|
||||
local image="$1" suffix="$2" dir="$3"
|
||||
local image="$1" suffix="$2" dir="$3" optional="${4:-}"
|
||||
local tags=(-t "${image}:${VERSION}${suffix}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${image}:latest${suffix}")
|
||||
@@ -348,6 +383,10 @@ jobs:
|
||||
refs+=("${image}@sha256:$(basename "$digest_file")")
|
||||
done < <(find "$dir" -type f | sort)
|
||||
if [ "${#refs[@]}" -eq 0 ]; then
|
||||
if [ -n "$optional" ]; then
|
||||
echo "::warning::No image digests in $dir — skipping optional tag ${image}:${VERSION}${suffix}" >&2
|
||||
return 0
|
||||
fi
|
||||
echo "No image digests in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -356,15 +395,15 @@ jobs:
|
||||
|
||||
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
|
||||
create_manifest "${IMAGE_NAME}" "-web" /tmp/digests/web
|
||||
create_manifest "${IMAGE_NAME}" "-bun" /tmp/digests/bun-base
|
||||
create_manifest "${IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web
|
||||
create_manifest "${IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional
|
||||
create_manifest "${IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional
|
||||
|
||||
- name: Create GHCR manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
create_manifest() {
|
||||
local image="$1" suffix="$2" dir="$3"
|
||||
local image="$1" suffix="$2" dir="$3" optional="${4:-}"
|
||||
local tags=(-t "${image}:${VERSION}${suffix}")
|
||||
if [ "$PROMOTE_LATEST" = "true" ]; then
|
||||
tags+=(-t "${image}:latest${suffix}")
|
||||
@@ -374,6 +413,10 @@ jobs:
|
||||
refs+=("${image}@sha256:$(basename "$digest_file")")
|
||||
done < <(find "$dir" -type f | sort)
|
||||
if [ "${#refs[@]}" -eq 0 ]; then
|
||||
if [ -n "$optional" ]; then
|
||||
echo "::warning::No image digests in $dir — skipping optional tag ${image}:${VERSION}${suffix}" >&2
|
||||
return 0
|
||||
fi
|
||||
echo "No image digests in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -382,8 +425,8 @@ jobs:
|
||||
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web" /tmp/digests/web
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-bun" /tmp/digests/bun-base
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional
|
||||
create_manifest "${GHCR_IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional
|
||||
|
||||
- name: Inspect image
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
|
||||
172
.github/workflows/npm-publish.yml
vendored
172
.github/workflows/npm-publish.yml
vendored
@@ -63,10 +63,14 @@ jobs:
|
||||
# This job never runs on `pull_request`, so the fork-safety clause is always true here;
|
||||
# it is kept verbatim so the expression stays greppable against ci.yml.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
tag: ${{ steps.resolve.outputs.tag }}
|
||||
skip: ${{ steps.resolve.outputs.skip }}
|
||||
permissions:
|
||||
actions: read # find + download the CI run's next-build artifact for this SHA
|
||||
contents: write # gh release upload (attach SBOM to the GitHub Release)
|
||||
id-token: write # npm provenance
|
||||
id-token: write # npm provenance (GitHub Packages step)
|
||||
packages: write # publish to npm.pkg.github.com
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -226,6 +230,28 @@ jobs:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
run: npm run build:cli
|
||||
|
||||
# `build:cli` assembles dist/ but does NOT write dist/BUILD_SHA — only
|
||||
# `build:release` does, by calling write-build-sha.mjs. The #10427 provenance
|
||||
# guard inside check:pack-artifact rejects an artifact with no SHA (and rejects
|
||||
# it even under OMNIROUTE_ALLOW_CANARY_BUILD=1: what cannot be identified cannot
|
||||
# be vouched for). Without this step the build+validate pair in this job is
|
||||
# structurally incompatible and fails 100% of the time — the same gap that was
|
||||
# fixed in ci.yml's Package Artifact job.
|
||||
- name: Stamp dist/BUILD_SHA for the provenance guard (#10427)
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
env:
|
||||
OMNIROUTE_BUILD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
export OMNIROUTE_BUILD_SHA="${OMNIROUTE_BUILD_SHA:0:7}"
|
||||
node scripts/build/write-build-sha.mjs
|
||||
|
||||
# The guard checks ancestry against origin/main by default, which is correct
|
||||
# here (a release tag is cut from main), but the ref has to exist locally for
|
||||
# `git merge-base` to resolve it.
|
||||
- name: Fetch main for the provenance probe
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
run: git fetch --no-tags --depth=50 origin +refs/heads/main:refs/remotes/origin/main
|
||||
|
||||
- name: Validate npm package artifact
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
run: npm run check:pack-artifact
|
||||
@@ -265,7 +291,12 @@ jobs:
|
||||
# a staged package that is never approved simply expires, with no `npm deprecate` needed.
|
||||
- name: Prove clean-install AND upgrade-over-previous both boot
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
timeout-minutes: 30
|
||||
# 60, not 30. This gate was added in #8953 and the 2026-08-27 v3.8.50 publish
|
||||
# was the FIRST run to ever reach it — every earlier attempt died upstream, so
|
||||
# its budget had never been measured against a real run. It then blew the limit
|
||||
# on its debut: `npm pack` alone took 24m37s, leaving 5 minutes for two installs
|
||||
# and two boots. 30 was a guess; 60 is sized to the one measurement we have.
|
||||
timeout-minutes: 60
|
||||
run: npm run check:install-upgrade
|
||||
|
||||
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
|
||||
@@ -287,41 +318,33 @@ jobs:
|
||||
fi
|
||||
npm --version
|
||||
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
# The registry upload itself moved to the `stage-npm` job below: npm REFUSES
|
||||
# `--provenance` from a self-hosted runner (422 "Unsupported GitHub Actions
|
||||
# runner environment"), and the heavy verification above cannot move to a
|
||||
# hosted one (16 GB is not enough for build:cli's next-build fallback — see
|
||||
# this job's runs-on comment). So this job proves the bytes and hands them
|
||||
# over; a tiny hosted job does the upload.
|
||||
- name: Pack the verified tarball for the upload job
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Always pass --tag explicitly. Defense in depth: even if VERSION is
|
||||
# accidentally an older release, the historic tag will NOT claim `@latest`.
|
||||
npm stage publish --provenance --access public --tag "$TAG"
|
||||
{
|
||||
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
|
||||
echo ""
|
||||
echo "The exact bytes are parked on the registry. To release them:"
|
||||
echo '```'
|
||||
echo "npm stage list omniroute # find the stage id"
|
||||
echo "npm stage approve <id> # owner 2FA — THE publish"
|
||||
echo '```'
|
||||
echo "To verify the staged bytes first: npm stage download <id> → run"
|
||||
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
|
||||
echo "To discard: npm stage reject <id>."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
|
||||
# --ignore-scripts: prepublishOnly would re-run build:cli-api && build:cli,
|
||||
# rebuilding bytes this job has already built, validated and boot-smoked.
|
||||
npm pack --ignore-scripts
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "expected $TARBALL to exist after npm pack" >&2; ls -la ./*.tgz || true; exit 1; }
|
||||
echo "packed $TARBALL ($(du -h "$TARBALL" | cut -f1))"
|
||||
|
||||
- name: Publish to npm (DIRECT — emergency fallback)
|
||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
|
||||
- name: Hand the tarball to the hosted publish job
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: npm-tarball
|
||||
path: omniroute-${{ steps.resolve.outputs.version }}.tgz
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
@@ -338,6 +361,91 @@ jobs:
|
||||
|| echo "⚠️ omniroute@${VERSION} might already be published on GitHub Packages."
|
||||
echo "✅ Action finished for GitHub Packages"
|
||||
|
||||
# npm REFUSES `--provenance` from a self-hosted runner:
|
||||
# 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
||||
# Unsupported GitHub Actions runner environment: "self-hosted".
|
||||
# Only "github-hosted" runners are supported when publishing with provenance.
|
||||
# v3.8.49 published fine because it predates USE_VPS_RUNNER being turned on
|
||||
# (2026-08-02); v3.8.50 was the first release after it, so this had been latent
|
||||
# for four weeks. Dropping --provenance was not an option: 3.8.49 carries a
|
||||
# SLSA attestation and 3.8.50 must not regress that.
|
||||
# The `publish` job cannot simply move to a hosted runner either — 16 GB is not
|
||||
# enough for build:cli's next-build fallback. So it keeps proving the bytes and
|
||||
# this job, which needs no memory at all, performs the upload.
|
||||
stage-npm:
|
||||
needs: publish
|
||||
if: needs.publish.outputs.skip != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # npm provenance — the whole reason this job is separate
|
||||
steps:
|
||||
- name: Download the tarball the publish job proved
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: npm-tarball
|
||||
path: .
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.NPM_PUBLISH_NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Ensure npm supports staged publishing
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CUR=$(npm --version)
|
||||
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
|
||||
# Pinned exact version (supply-chain: never float @latest in a publish
|
||||
# job); bump deliberately when a newer npm is required.
|
||||
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
|
||||
npm install -g --ignore-scripts npm@11.15.0
|
||||
fi
|
||||
npm --version
|
||||
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct'
|
||||
env:
|
||||
VERSION: ${{ needs.publish.outputs.version }}
|
||||
TAG: ${{ needs.publish.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "tarball $TARBALL did not arrive from the publish job" >&2; ls -la; exit 1; }
|
||||
# Always pass --tag explicitly. Defense in depth: even if VERSION is
|
||||
# accidentally an older release, the historic tag will NOT claim `@latest`.
|
||||
# --ignore-scripts: publishing a built tarball must never re-run
|
||||
# prepublishOnly (build:cli-api && build:cli) on this small runner.
|
||||
npm stage publish "$TARBALL" --provenance --access public --tag "$TAG" --ignore-scripts
|
||||
{
|
||||
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
|
||||
echo ""
|
||||
echo "The exact bytes are parked on the registry. To release them:"
|
||||
echo '```'
|
||||
echo "npm stage list omniroute # find the stage id"
|
||||
echo "npm stage approve <id> # owner 2FA — THE publish"
|
||||
echo '```'
|
||||
echo "To verify the staged bytes first: npm stage download <id> → run"
|
||||
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
|
||||
echo "To discard: npm stage reject <id>."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
|
||||
|
||||
- name: Publish to npm (DIRECT — emergency fallback)
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
env:
|
||||
VERSION: ${{ needs.publish.outputs.version }}
|
||||
TAG: ${{ needs.publish.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL="omniroute-${VERSION}.tgz"
|
||||
test -f "$TARBALL" || { echo "tarball $TARBALL did not arrive from the publish job" >&2; ls -la; exit 1; }
|
||||
npm publish "$TARBALL" --provenance --access public --tag "$TAG" --ignore-scripts
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
|
||||
|
||||
publish-opencode-plugin:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -293,3 +293,6 @@ docker-compose.yml.bak
|
||||
# Ad-hoc test sandboxes (never tracked — may contain local DBs)
|
||||
/.sandbox/
|
||||
.aider*
|
||||
|
||||
# check:install-upgrade work trees (~12 GB, disposable)
|
||||
/.install-upgrade/
|
||||
|
||||
40
.mailmap
Normal file
40
.mailmap
Normal file
@@ -0,0 +1,40 @@
|
||||
# .mailmap — canonical author identities for git log/shortlog/blame.
|
||||
#
|
||||
# Why this file exists: between 2026-08-13 and 2026-08-26 this checkout carried a
|
||||
# `git config --local` whose user.name was one contributor's ("Xiangzhe" / @xz-dev)
|
||||
# and whose user.email was ANOTHER contributor's (@backryun). Every commit produced
|
||||
# on this machine in that window was therefore signed with @backryun's address —
|
||||
# 237 commits, all in the -0300 timezone, while @backryun's own work commits from
|
||||
# +0900 and continued normally throughout. The local override was removed on
|
||||
# 2026-08-26; this file repairs the RECORD without rewriting published history
|
||||
# (those commits live on release/v3.8.50 and release/v3.8.51, which other sessions
|
||||
# and open PRs build on — a rewrite would force-push both and orphan the v3.8.50 tag).
|
||||
#
|
||||
# Format: Canonical Name <canonical@email> Commit Name <commit@email>
|
||||
|
||||
# --- Maintainer: several addresses used over the project's life ---
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <diegosouza.pw@gmail.com>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <diegosouza.pw@outlook.com>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <diegosouzapw@users.noreply.github.com>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Diego Souza <8016841+diegosouzapw@users.noreply.github.com>
|
||||
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <diego.souza.pw@gmail.com>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <souzamiriamrodrigues790@gmail.com>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <diego.souza@cdwasolutions.com.br>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> <diegosouzapw@devbox.local>
|
||||
|
||||
# --- The misattribution window: name Xiangzhe + @backryun's email, from -0300.
|
||||
# These are maintainer/session commits, NOT @backryun's contributions.
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Xiangzhe <bakryun0718@proton.me>
|
||||
diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Xiangzhe <diegosouza.pw@gmail.com>
|
||||
|
||||
# --- Xiangzhe (@xz-dev) — a distinct contributor; keep their own work intact ---
|
||||
Xiangzhe <32761048+xz-dev@users.noreply.github.com> <xz-dev@users.noreply.github.com>
|
||||
Xiangzhe <32761048+xz-dev@users.noreply.github.com> <xiangzhedev@gmail.com>
|
||||
|
||||
# --- @backryun's own alternate addresses (their real work, kept intact) ---
|
||||
backryun <24198422+backryun@users.noreply.github.com> <bakryun0718@proton.me>
|
||||
backryun <24198422+backryun@users.noreply.github.com> <backryun@daonlab.local>
|
||||
backryun <24198422+backryun@users.noreply.github.com> <busan011@ormbiz.co.kr>
|
||||
backryun <24198422+backryun@users.noreply.github.com> <backryun@users.noreply.github.com>
|
||||
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (159 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (160 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
|
||||
71
CHANGELOG.md
71
CHANGELOG.md
@@ -93,6 +93,67 @@
|
||||
|
||||
_Living section — regenerated 2026-08-12 from all cycle commits (cycle open `ed2db6cb19` → tip). Bullets carry the merged PR and its author; direct pushes listed separately._
|
||||
|
||||
### 📊 Release by the numbers
|
||||
|
||||
| | |
|
||||
| --- | ---: |
|
||||
| 👥 People who contributed | **248** |
|
||||
| 📝 Commits in the cycle | **1,714** |
|
||||
| 🔀 Pull requests referenced | **1,666** |
|
||||
| 📋 Changelog entries | **1,182** |
|
||||
| 🙌 Contributors credited in entries | **256** |
|
||||
| 🤖 Automated dependency commits | 22 |
|
||||
|
||||
**Entries by type**
|
||||
|
||||
| Type | Count |
|
||||
| --- | ---: |
|
||||
| 🐛 Fixes | 779 |
|
||||
| ✨ Features | 169 |
|
||||
| 📚 Docs | 29 |
|
||||
| 🧹 Chore | 27 |
|
||||
| 🧪 Tests | 15 |
|
||||
| ♻️ Refactor | 5 |
|
||||
| ⚡ Performance | 3 |
|
||||
| providers | 2 |
|
||||
| 🔒 Security | 2 |
|
||||
| ⚙️ CI | 2 |
|
||||
| deps | 2 |
|
||||
| maint | 2 |
|
||||
|
||||
### 🏆 Top 25 contributors this cycle
|
||||
|
||||
_By commits in `ed2db6cb19..v3.8.50`, author identities consolidated via `.mailmap`. Bots excluded._
|
||||
|
||||
| # | Contributor | Commits |
|
||||
| ---: | --- | ---: |
|
||||
| 🥇 | diegosouzapw | 738 |
|
||||
| 🥈 | backryun | 88 |
|
||||
| 🥉 | Dizzle | 66 |
|
||||
| 4 | Ravi Tharuma | 52 |
|
||||
| 5 | Markus Hartung | 48 |
|
||||
| 6 | Bob.Hou | 42 |
|
||||
| 7 | Rouzbeh† | 38 |
|
||||
| 8 | Xiangzhe | 31 |
|
||||
| 9 | Paco Cartones | 28 |
|
||||
| 10 | Nguyen Thanh Dat | 23 |
|
||||
| 11 | Aman | 22 |
|
||||
| 12 | Will Gordon | 19 |
|
||||
| 13 | 小妍儿 ✨ | 17 |
|
||||
| 14 | adevwithpurpose | 16 |
|
||||
| 15 | Andrew B. | 10 |
|
||||
| 16 | NOXX - Commiter | 10 |
|
||||
| 17 | ignamiranda | 10 |
|
||||
| 18 | Jonathan Bailey | 9 |
|
||||
| 19 | Ke Jin | 9 |
|
||||
| 20 | Austin Liu | 8 |
|
||||
| 21 | Chewji | 8 |
|
||||
| 22 | Prudhvi Vuda | 7 |
|
||||
| 23 | benzntech | 7 |
|
||||
| 24 | rinseaid | 7 |
|
||||
| 25 | stanley | 7 |
|
||||
|
||||
|
||||
### ✨ New Features
|
||||
- **feat(search):** first-class X Search provider (`x-search`) on `POST /v1/search` and MCP `omniroute_x_search` using SuperGrok / xAI server-side `x_search`. Explicit provider or `search_type: "x"` only — never auto-selected for web. Reuses `xai-oauth` / `xao` / `xai` credentials. Not the X Developer Platform MCP. ([#10985](https://github.com/diegosouzapw/OmniRoute/issues/10985))
|
||||
- **feat(core):** add Layer A capability filter at router (#5696)
|
||||
@@ -1111,6 +1172,12 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **fix(models):** health-check-excluded models are hidden from the `/v1/models` catalog ([#10026](https://github.com/diegosouzapw/OmniRoute/issues/10026) — thanks @ritheshcn25)
|
||||
- **fix(skills):** the CLI skills left stale by the quota subcommands are regenerated ([#10698](https://github.com/diegosouzapw/OmniRoute/issues/10698))
|
||||
- **fix(mcp):** CLI MCP call protocol issues were resolved ([#10960](https://github.com/diegosouzapw/OmniRoute/issues/10960) — thanks @YunyunZhai)
|
||||
- **fix(dashboard):** expert mode in the Combo Builder can type a provider/model pair by hand again ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — [#8285](https://github.com/diegosouzapw/OmniRoute/pull/8285) (global model search) replaced the "Manual model" block positionally with the new search panel, removing the only way to enter a model that is not in the catalog. The state and handlers behind it survived as dead code, so neither typecheck nor lint noticed the loss. The block is restored above the search panel, unchanged from its pre-#8285 form, and guarded by `tests/e2e/combos-flow.spec.ts` ("expert mode shows a single-page combo form with manual model entry").
|
||||
- **fix(sse):** a combo step pinned to an explicit connection (or a request pinned with `x-omniroute-connection`) is honored on fallback instead of silently rotating to a sibling account ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — the generic account-fallback branch excluded the pinned connection after an upstream failure and re-selected another account of the same provider, so a priority combo repeating one provider/model with two different fixed accounts ran **both** attempts under the first step; the second step and its own pin never executed, and per-step attribution (`comboStepId` / `comboExecutionKey`) was wrong. Rotation is now gated on there being no forced connection, matching the stream-readiness, pre-response-timeout and account-semaphore branches. Cooldown recording is unchanged, and unpinned selection still skips burned connections.
|
||||
- **fix(cli):** the local CLI sees the full `/api/monitoring/health` payload again — `version` included — restoring the `check:pack-boot` release gate. [#11040](https://github.com/diegosouzapw/OmniRoute/pull/11040) reduced that route to a liveness-only view for non-management callers (GHSA-mvf8-qc78-5mxm), but the route is classified PUBLIC and `runAuthzPipeline` strips the machine-token header for every route class — so the PUBLIC policy stamped `anonymous` and the loopback CLI could never be recognized as a management principal. The PUBLIC policy now stamps the same loopback-gated `local-cli-token` subject the MANAGEMENT policy already did; anonymous callers still get liveness only.
|
||||
- **fix(search):** a configured search connection (Serper, Brave, Tavily…) is now used instead of being silently passed over for the free `duckduckgo-free` fallback ([#11524](https://github.com/diegosouzapw/OmniRoute/issues/11524)) — when no provider was named explicitly and the cheapest auto-selected one had no credentials, the last-resort loop ran first and `duckduckgo-free` (cost 0, no auth) always won it with empty credentials. On `/v1/responses` the call then returned `success: true` with **zero results**, so web search looked healthy while the paid connection the operator had set up was never called. Credentialed regular providers are now swept before the last-resort loop, and fallback-only providers can no longer outrank a configured one on price.
|
||||
- **fix(api):** `/v1/models` no longer blocks the stale response while it rebuilds the catalog ([#11551](https://github.com/diegosouzapw/OmniRoute/issues/11551)) — the route has been passing a `scheduleBackgroundRefresh` option since #10198, but the parameter had already been removed from `getUnifiedModelsResponse()`, so the object was silently dropped and the stale-while-revalidate rebuild still ran on a `setTimeout(..., 0)`. The builder is essentially synchronous under the App Router, so it pinned the event loop **before** the cached response was flushed — the [#8728](https://github.com/diegosouzapw/OmniRoute/pull/8728) guarantee did not actually exist for operators with large catalogs. `catalogCache` now schedules through `after()` (with a macrotask fallback outside a request scope) and the option is threaded end to end. The extra argument was invisible to CI because `typecheck:core` is a curated allowlist and `next.config.mjs` sets `ignoreBuildErrors: true`.
|
||||
- **fix(sse):** a universal handoff whose summary comes back unusable is no longer regenerated on every single model switch, which was burning paid quota on upstream calls whose answers were thrown away ([#11552](https://github.com/diegosouzapw/OmniRoute/issues/11552)) — nothing is persisted when the summary does not parse, so the next switch in the same session re-issued the same full-history summarization request and discarded it again, forever. With a switch-heavy combo strategy (weighted, random, round-robin, p2c) that landed on a large share of requests: measured at n=200, roughly **one request in four carried an extra discarded upstream call**, and that traffic skewed a weighted 70/30 combo to an observed 0.895 share for one provider even though the share actually delivered to the client was a correct 0.70. There is now an exponential back-off per (session, combo) — 5 min up to 1 h, cleared on the first successful handoff, capped at 500 tracked keys. A transient upstream failure is deliberately **not** tracked, so it still retries immediately. After the fix: 201 upstream calls for 200 requests, and the measured share matches the delivered one.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
@@ -1355,6 +1422,10 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **docs(i18n):** localization contributions — a complete Persian user guide ([#11254](https://github.com/diegosouzapw/OmniRoute/issues/11254)), improved and completed Turkish documentation ([#11237](https://github.com/diegosouzapw/OmniRoute/issues/11237)), the Italian README restored ([#11246](https://github.com/diegosouzapw/OmniRoute/issues/11246)), a Farsi README ([#10777](https://github.com/diegosouzapw/OmniRoute/issues/10777) — thanks @farshidrezaei), a `SETUP_GUIDE.md` correction ([#10490](https://github.com/diegosouzapw/OmniRoute/issues/10490) — thanks @realize000), a `python_requests.py` example fix ([#10731](https://github.com/diegosouzapw/OmniRoute/issues/10731) — thanks @pandaaaa1990), and a retranslation of the CLI reference and integrations guide across all 42 locales
|
||||
- **chore(repo):** repository hygiene — the self-referential `_tasks` symlink was untracked and `.gitignore` anchored so a `_tasks` symlink can never be tracked again, `.source/dynamic.ts`, `.source`, `/output/` and the Playwright CLI artifact directory were ignored, an initial `.cbmignore` was added for codebase-memory indexing, unused `.source/dynamic.ts` and `source.config.mjs` files were removed, the stray unresolved conflict marker in `ENVIRONMENT.md` was cleaned up, and the Open Collective sponsorship link was removed from the README
|
||||
- **chore(release):** localized `llm.txt` mirrors and the v3.8.50 base quality docs were synchronized, and the 363 `changelog.d` fragments were aggregated into this section
|
||||
- **fix(ci):** the pack-artifact provenance gate now checks the branch under test instead of `origin/main` ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — the guard added for [#10427](https://github.com/diegosouzapw/OmniRoute/issues/10427) is correct at publish time (that workflow runs on `main`), but in a `pull_request` context the head is by construction not an ancestor of `main` and the shallow checkout never fetches it, so the probe always answered false and the job failed 100% of the time. It became visible only once it stopped being cancelled behind Build. Pre-merge it resolves the ref from `refs/pull/<N>/head`, which exists on origin even for fork PRs.
|
||||
- **test(dashboard):** the proxy-registry e2e smoke flow opens the toolbar's "More actions" overflow menu before clicking bulk assign ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — [#9870](https://github.com/diegosouzapw/OmniRoute/pull/9870) moved the action into that menu, which only renders its items while open, so the locator never resolved and the test burned its full 180 s budget. Every existing assertion is unchanged.
|
||||
- **test(api):** the `/v1/models` e2e check accepts the auth gate introduced by [#9320](https://github.com/diegosouzapw/OmniRoute/pull/9320) ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — the catalog now requires auth whenever management auth is configured, and the harness boots with `INITIAL_PASSWORD` set, so the endpoint had been answering 401 since 2026-08-04 and the check was red the whole time, hidden behind a cancelled job. It now mirrors the sibling `/api/providers` check: assert the catalog shape when the catalog is served, otherwise pin the gate by status **and** error type so an unrelated 401 cannot pass for the deliberate one.
|
||||
- **chore(quality):** refreshed the combos-page ESLint suppressions after the manual-model restore ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — three `no-unused-vars` suppressions existed only because #8285 had deleted the JSX consuming that state; with the block back they are live again, and a stale suppression makes ESLint exit 2, which is what actually turned the Lint job red. The 7 `react-hooks/set-state-in-effect` plus 1 `react-hooks/immutability` errors in the same file are pre-existing (reproducible on the file's `091e2ba4da` content) and surfaced only because touching the file evicted it from the restored `.eslintcache`; they are frozen here and tracked separately rather than refactored mid-release.
|
||||
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
@@ -1202,7 +1202,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 159 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 160 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** the local CLI sees the full `/api/monitoring/health` payload again — `version` included — restoring the `check:pack-boot` release gate. [#11040](https://github.com/diegosouzapw/OmniRoute/pull/11040) reduced that route to a liveness-only view for non-management callers (GHSA-mvf8-qc78-5mxm), but the route is classified PUBLIC and `runAuthzPipeline` strips the machine-token header for every route class — so the PUBLIC policy stamped `anonymous` and the loopback CLI could never be recognized as a management principal. The PUBLIC policy now stamps the same loopback-gated `local-cli-token` subject the MANAGEMENT policy already did; anonymous callers still get liveness only.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(search):** a configured search connection (Serper, Brave, Tavily…) is now used instead of being silently passed over for the free `duckduckgo-free` fallback ([#11524](https://github.com/diegosouzapw/OmniRoute/issues/11524)) — when no provider was named explicitly and the cheapest auto-selected one had no credentials, the last-resort loop ran first and `duckduckgo-free` (cost 0, no auth) always won it with empty credentials. On `/v1/responses` the call then returned `success: true` with **zero results**, so web search looked healthy while the paid connection the operator had set up was never called. Credentialed regular providers are now swept before the last-resort loop, and fallback-only providers can no longer outrank a configured one on price.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(api):** `/v1/models` no longer blocks the stale response while it rebuilds the catalog ([#11551](https://github.com/diegosouzapw/OmniRoute/issues/11551)) — the route has been passing a `scheduleBackgroundRefresh` option since #10198, but the parameter had already been removed from `getUnifiedModelsResponse()`, so the object was silently dropped and the stale-while-revalidate rebuild still ran on a `setTimeout(..., 0)`. The builder is essentially synchronous under the App Router, so it pinned the event loop **before** the cached response was flushed — the [#8728](https://github.com/diegosouzapw/OmniRoute/pull/8728) guarantee did not actually exist for operators with large catalogs. `catalogCache` now schedules through `after()` (with a macrotask fallback outside a request scope) and the option is threaded end to end. The extra argument was invisible to CI because `typecheck:core` is a curated allowlist and `next.config.mjs` sets `ignoreBuildErrors: true`.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** a universal handoff whose summary comes back unusable is no longer regenerated on every single model switch, which was burning paid quota on upstream calls whose answers were thrown away ([#11552](https://github.com/diegosouzapw/OmniRoute/issues/11552)) — nothing is persisted when the summary does not parse, so the next switch in the same session re-issued the same full-history summarization request and discarded it again, forever. With a switch-heavy combo strategy (weighted, random, round-robin, p2c) that landed on a large share of requests: measured at n=200, roughly **one request in four carried an extra discarded upstream call**, and that traffic skewed a weighted 70/30 combo to an observed 0.895 share for one provider even though the share actually delivered to the client was a correct 0.70. There is now an exponential back-off per (session, combo) — 5 min up to 1 h, cleared on the first successful handoff, capped at 500 tracked keys. A transient upstream failure is deliberately **not** tracked, so it still retries immediately. After the fix: 201 upstream calls for 200 requests, and the measured share matches the delivered one.
|
||||
@@ -0,0 +1,8 @@
|
||||
- **fix(db):** `model_capabilities` is created by a migration instead of lazily on the first
|
||||
models.dev sync, so a clean install and an upgraded install converge on the same schema
|
||||
regardless of which features have run
|
||||
- **fix(ci):** `check:install-upgrade` now fails on an `npm` install truncated by ENOSPC
|
||||
(npm reports it as a warning and still exits 0), authenticates its health probe so the
|
||||
version assertion works against the hardened health payload, frees the clean-install tree
|
||||
before the upgrade phase, and no longer reports a schema divergence computed from a boot
|
||||
that never served
|
||||
4
changelog.d/fixes/11856-npm-payload-nft-manifests.md
Normal file
4
changelog.d/fixes/11856-npm-payload-nft-manifests.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- Excluded Next.js Node File Trace manifests (`*.nft.json`) from the published npm
|
||||
tarball. They are build-time metadata and are never read while serving, but had
|
||||
grown to 668.7 MB — 61% of the package — which pushed the upload past the
|
||||
registry limit and made `npm publish` fail with `413 Payload Too Large`.
|
||||
4
changelog.d/fixes/11868-npm-provenance-hosted-runner.md
Normal file
4
changelog.d/fixes/11868-npm-provenance-hosted-runner.md
Normal file
@@ -0,0 +1,4 @@
|
||||
- Split the npm registry upload into its own GitHub-hosted job. npm refuses
|
||||
`--provenance` from a self-hosted runner (`422 ... Only "github-hosted" runners
|
||||
are supported`), which blocked the v3.8.50 publish; the heavy verification
|
||||
cannot move to a hosted runner, so it now hands the proven tarball over instead.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** expert mode in the Combo Builder can type a provider/model pair by hand again ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — [#8285](https://github.com/diegosouzapw/OmniRoute/pull/8285) (global model search) replaced the "Manual model" block positionally with the new search panel, removing the only way to enter a model that is not in the catalog. The state and handlers behind it survived as dead code, so neither typecheck nor lint noticed the loss. The block is restored above the search panel, unchanged from its pre-#8285 form, and guarded by `tests/e2e/combos-flow.spec.ts` ("expert mode shows a single-page combo form with manual model entry").
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** a combo step pinned to an explicit connection (or a request pinned with `x-omniroute-connection`) is honored on fallback instead of silently rotating to a sibling account ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — the generic account-fallback branch excluded the pinned connection after an upstream failure and re-selected another account of the same provider, so a priority combo repeating one provider/model with two different fixed accounts ran **both** attempts under the first step; the second step and its own pin never executed, and per-step attribution (`comboStepId` / `comboExecutionKey`) was wrong. Rotation is now gated on there being no forced connection, matching the stream-readiness, pre-response-timeout and account-semaphore branches. Cooldown recording is unchanged, and unpinned selection still skips burned connections.
|
||||
@@ -0,0 +1,3 @@
|
||||
- `check:workflows` now fails (under `--strict`/`--ratchet`) when any job routed to a
|
||||
self-hosted runner publishes with `--provenance` — npm rejects that with `422` at the
|
||||
registry, which in v3.8.50 only surfaced after the tag and Docker images were public.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(ci):** the pack-artifact provenance gate now checks the branch under test instead of `origin/main` ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — the guard added for [#10427](https://github.com/diegosouzapw/OmniRoute/issues/10427) is correct at publish time (that workflow runs on `main`), but in a `pull_request` context the head is by construction not an ancestor of `main` and the shallow checkout never fetches it, so the probe always answered false and the job failed 100% of the time. It became visible only once it stopped being cancelled behind Build. Pre-merge it resolves the ref from `refs/pull/<N>/head`, which exists on origin even for fork PRs.
|
||||
@@ -1 +0,0 @@
|
||||
- **test(dashboard):** the proxy-registry e2e smoke flow opens the toolbar's "More actions" overflow menu before clicking bulk assign ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — [#9870](https://github.com/diegosouzapw/OmniRoute/pull/9870) moved the action into that menu, which only renders its items while open, so the locator never resolved and the test burned its full 180 s budget. Every existing assertion is unchanged.
|
||||
@@ -1 +0,0 @@
|
||||
- **test(api):** the `/v1/models` e2e check accepts the auth gate introduced by [#9320](https://github.com/diegosouzapw/OmniRoute/pull/9320) ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — the catalog now requires auth whenever management auth is configured, and the harness boots with `INITIAL_PASSWORD` set, so the endpoint had been answering 401 since 2026-08-04 and the check was red the whole time, hidden behind a cancelled job. It now mirrors the sibling `/api/providers` check: assert the catalog shape when the catalog is served, otherwise pin the gate by status **and** error type so an unrelated 401 cannot pass for the deliberate one.
|
||||
@@ -1 +0,0 @@
|
||||
- **chore(quality):** refreshed the combos-page ESLint suppressions after the manual-model restore ([#8875](https://github.com/diegosouzapw/OmniRoute/pull/8875)) — three `no-unused-vars` suppressions existed only because #8285 had deleted the JSX consuming that state; with the block back they are live again, and a stale suppression makes ESLint exit 2, which is what actually turned the Lint job red. The 7 `react-hooks/set-state-in-effect` plus 1 `react-hooks/immutability` errors in the same file are pre-existing (reproducible on the file's `091e2ba4da` content) and surfaced only because touching the file evicted it from the restored `.eslintcache`; they are frozen here and tracked separately rather than refactored mid-release.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_doc": "Tables that exist ONLY in databases upgraded from an older version — residue whose CREATE left the migration set in some past cycle but survives where it already existed. Harmless (nothing references them), but recorded here so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.",
|
||||
"_doc": "Tables that exist ONLY in databases upgraded from an older version. Two causes, and they call for different fixes: (a) residue whose CREATE left the migration set in some past cycle but survives where it already existed — allowlist it here; (b) a table created LAZILY at runtime with `CREATE TABLE IF NOT EXISTS` inside a feature code path — whether a database has it depends on whether that feature ran, so it diverges by TIMING and can show up on EITHER side. Fix (b) with a migration instead of an entry here (see src/lib/db/migrations/163_model_capabilities.sql); an allowlist entry only hides it in one direction. Either way, recorded so check-install-upgrade.mjs can still fail on a NEW divergence. The opposite direction (a table a clean install creates but an upgrade does not) is NEVER allowlisted: it means every existing user is missing structure the code expects.",
|
||||
"residualTables": {
|
||||
"cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left in place rather than dropped: a DROP migration on a table we cannot prove is unused everywhere is the riskier change. Revisit when the cache subsystem is next touched."
|
||||
"cache_metrics": "Measured 2026-07-30 on a real 3.8.48 install upgraded to 3.8.49 (VPS .16, 165 MB database, 114 → 117 tables). Present in upgraded databases, absent from clean installs. Cause identified 2026-08-27: it is case (b) above — created lazily by `ensureCacheMetricsTable()` at src/lib/semanticCache.ts:34, never by a migration, so it appears only where the semantic cache has run. No code path referenced it during the upgrade (zero `no such table` in 150 log lines, both installs healthy). Left as an allowlist entry rather than promoted to a migration or dropped: unlike model_capabilities it did not block a release, and creating a table for a subsystem we cannot prove is live is not a change to make blind. Revisit when the cache subsystem is next touched."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"dedicatedGate": true
|
||||
},
|
||||
"zizmorFindings": {
|
||||
"value": 192,
|
||||
"value": 194,
|
||||
"_rebaseline_2026_08_20_radar_export_workflow": "190 -> 192 (+2). Workflow novo `.github/workflows/radar-export.yml` (passo 10 do go-live do Radar: publica o export estável do catálogo como asset de release para o servidor privado baixar via RADAR_EXPORT_URL). Os +2 são unpinned-uses @vN: actions/checkout@v7 + actions/setup-node@v7 — a MESMA convenção deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só este violaria a convenção. O findings artipacked do checkout foi CORRIGIDO com `persist-credentials: false` (o job publica via GH_TOKEN em `gh release`, não usa a credencial do checkout). Nenhuma classe nova de template-injection / cache-poisoning / dangerous-triggers. Medido local com zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 191; +1 do delta conhecido do runner (ver _rebaseline_2026_07_28_ci_runner_delta: o runner enxerga 1 unpinned-uses @vN a mais que o devbox no mesmo commit; a baseline segue o runner) => 192.",
|
||||
"_rebaseline_2026_07_20_aliasresolver_hook_split_7808": "175 -> 176 (+1). Companion to PR #7808 (CodeQL js/incomplete-url-substring-sanitization fix in bin/aliasResolver.mjs). The +1 is NOT caused by this PR's code changes (bin/* is not a workflow file) — it is a pre-existing drift that surfaced because the ratchet gate runs on this PR's CI: the zizmor scanner version on the GitHub runner gained a new rule (or extended an existing one) since the v3.8.49 baseline was seeded on 2026-07-17. Breakdown: the new finding is an unpinned-uses @vN class item on one of the existing workflows (same deliberate convention as _scanner_harden_workflows_2026_06_16 — @vN is intentional, SHA-pinning only this one would violate the convention). No new template-injection/artipacked/cache-poisoning/dangerous-triggers classes introduced. Measured by the Quality Gates (Extended) job on run 29713001401 = 176, baseline was 175. Note: by the time this landed on release/v3.8.49, the baseline was already at 176 via _rebaseline_2026_07_17_combo_recovery_hints — this entry is kept as historical record; no further bump applied.",
|
||||
"_rebaseline_2026_07_17_v3849_release": "169 -> 175 (+6). Cycle workflow drift (v3.8.48/v3.8.49): npm-publish.yml (new, WS1.3 #7092), electron-release.yml, nightly-compat.yml, nightly-release-green.yml, CI restructures (#7501 full-history base fetch, #7355 main-green, #7202 merge-queue gates, Trunk/Codecov). Breakdown vs v3.8.47: +3 unpinned-uses (@vN convention, deliberate per _scanner_harden_workflows_2026_06_16), +2 cache-poisoning (artifact upload/cache in the OWN electron-release/npm-publish RELEASE workflows -- operator-controlled, not fork-PR exploitable), +1 excessive-permissions (nightly-compat.yml permissions:issues). No new template-injection/artipacked/dangerous-triggers. Measured with zizmor 1.25.2 via `node scripts/check/check-workflows.mjs --ratchet` = 175 on da3a0be69.",
|
||||
@@ -180,7 +180,8 @@
|
||||
"_rebaseline_2026_06_23_v3834_release": "152 -> 155 (+3). The 3 new unpinned-uses are in .github/workflows/nightly-release-green.yml (added by #4622 this cycle): actions/checkout@v7, actions/setup-node@v6, actions/upload-artifact@v4 — the SAME deliberate @vN convention as ci.yml's own checkout@v7/setup-node@v6 and every other workflow (see _scanner_harden_workflows_2026_06_16 + _zizmor_rebaseline_2026_06_20_ci_build_artifact_reuse). SHA-pinning only this workflow would violate the convention. The workflow-lint ratchet does NOT run on PR->release fast-gates, so it surfaced only on the release PR; measured locally via `npm run check:workflows -- --ratchet` = 155. No new template-injection/artipacked/cache-poisoning.",
|
||||
"_rebaseline_2026_07_13_v3847_release_preflight": "159 -> 169 (+10). Findings from cycle-merged workflow changes: #6716 (PR gate restructure), #6781 (unit fast-path shard 2->4), #6788 (TIA tsx loader split), #6881 (electron-updater latest.yml manifests in release assets) — same deliberate @vN unpinned-uses convention as prior rebaselines; no new template-injection/artipacked/cache-poisoning classes. Measured via `npm run check:workflows -- --ratchet` = 169 on the v3.8.47 release pre-flight.",
|
||||
"_rebaseline_2026_07_28_v3849_release_preflight": "176 -> 189 (+13). Pre-flight de fechamento da v3.8.49 (934 commits no ciclo). Deriva de workflow: 1 workflow novo (build-rinseaid-image.yml) mais os bumps de action do Dependabot ao longo do ciclo — todos da MESMA classe unpinned-uses @vN, convenção deliberada do repo (ver _scanner_harden_workflows_2026_06_16); fixar por SHA só estes violaria a convenção. Nenhuma classe nova de template-injection / artipacked / cache-poisoning / dangerous-triggers. Nesta mesma passada foram CORRIGIDAS 3 diretivas shellcheck malformadas (SC1125: `# shellcheck disable=SC2086 — texto`, em que o travessão invalida o par key=value) em ci.yml e nightly-release-green.yml. Medido com zizmor 1.25.2 via `npm run check:workflows -- --ratchet` = 189.",
|
||||
"_rebaseline_2026_07_28_ci_runner_delta": "189 -> 190 (+1). Medido 189 no devbox e 190 no runner do GitHub no MESMO commit (run 30396592013, job Quality Gates (Extended)) — mesma classe já registrada em _rebaseline_2026_07_20_aliasresolver_hook_split_7808: a versão do zizmor no runner enxerga uma finding a mais que a local, sempre da classe unpinned-uses @vN. O valor do runner é o que o gate compara, então a baseline segue o runner."
|
||||
"_rebaseline_2026_07_28_ci_runner_delta": "189 -> 190 (+1). Medido 189 no devbox e 190 no runner do GitHub no MESMO commit (run 30396592013, job Quality Gates (Extended)) — mesma classe já registrada em _rebaseline_2026_07_20_aliasresolver_hook_split_7808: a versão do zizmor no runner enxerga uma finding a mais que a local, sempre da classe unpinned-uses @vN. O valor do runner é o que o gate compara, então a baseline segue o runner.",
|
||||
"_rebaseline_2026_08_28_npm_publish_hosted_stage_job": "192 -> 194 (+2). Job novo `stage-npm` em .github/workflows/npm-publish.yml: o npm RECUSA `--provenance` vindo de runner self-hosted (422 \"Unsupported GitHub Actions runner environment\"), e o job `publish` nao pode migrar para runner hospedado porque 16 GB nao bastam para o fallback next-build do build:cli (documentado no proprio runs-on). A separacao foi a unica saida que preserva a atestacao SLSA que a 3.8.49 ja tem. Os +2 sao da MESMA convencao deliberada de todos os workflows (ver _scanner_harden_workflows_2026_06_16): unpinned-uses @vN em actions/download-artifact@v8 + actions/setup-node@v7, mais o cache-poisoning que o proprio setup-node@v7 ja gera nos outros 2 jobs deste MESMO arquivo (linhas 85 e 463) e que ja esta na baseline. Fixar por SHA so este job violaria a convencao. Nenhuma classe nova: zero template-injection / artipacked / dangerous-triggers / excessive-permissions — o job declara apenas contents:read + id-token:write, que e o minimo para a proveniencia. Medido pelo job Quality Gates (Extended) no run 33162... da PR #11877 = 194."
|
||||
},
|
||||
"vulnCount": {
|
||||
"value": 22,
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
|
||||
| `OMNIROUTE_MIGRATIONS_DIR` | _(auto-detect)_ | `src/lib/db/migrationRunner.ts` | Override the directory that the migration runner scans. Useful when shipping bundled migrations in custom builds. |
|
||||
| `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` | _(unset)_ | `src/lib/db/migrationRunner/extraDirs.ts` | Additional migration directories as `namespace=dir` entries separated by the platform path delimiter (e.g. `ee=/opt/app/enterprise/db/migrations`). Files found there are recorded as `<namespace>-<number>`, so a distribution shipping its own migrations never collides with the upstream numeric slots. A malformed entry, an invalid namespace or a missing directory throws at startup instead of silently skipping the schema. |
|
||||
| `OMNIROUTE_MAX_PENDING_MIGRATIONS` | `50` | `src/lib/db/migrationRunner.ts` | Mass-pending-migrations safety threshold (#3416). Startup aborts if more than this many migrations are pending on an existing DB (guards against a wiped tracking table). Raise it to restore an older backup; set to `0` to disable the check. |
|
||||
| `OMNIROUTE_INSTALL_UPGRADE_WORKDIR` | _(`<repo>/.install-upgrade`)_ | `scripts/check/check-install-upgrade.mjs` | Working directory for the `check:install-upgrade` release gate. It needs roughly 12 GB (two ~3 GB install trees plus the tarball), so it must not run on a small tmpfs — on the self-hosted runner `/tmp` is a 12 GB RAM-backed tmpfs and the gate exhausted it, truncating the package. |
|
||||
| `OMNIROUTE_SPEND_FLUSH_INTERVAL_MS` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Flush interval (ms) for the batched spend/cost writer. Lower values reduce write coalescing; higher values reduce DB contention. |
|
||||
| `OMNIROUTE_SPEND_MAX_BUFFER_SIZE` | _(default in code)_ | `src/lib/spend/batchWriter.ts` | Max buffered spend entries before a forced flush. Raise on high-QPS deployments; lower when bounded memory matters more. |
|
||||
| `OMNIROUTE_PROXY_FETCH_DEBUG` | _(unset)_ | `open-sse/utils/proxyFetch.ts` | Set to `"true"` to emit `[ProxyFetch]` debug logs on the Vercel relay path. Off by default to avoid leaking routing hints. |
|
||||
|
||||
4
llm.txt
4
llm.txt
@@ -14,7 +14,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
|
||||
- **Framework:** Next.js 16 (App Router) with TypeScript 6
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 159 migrations)
|
||||
- **Database:** SQLite via better-sqlite3 (local, zero-config, 160 migrations)
|
||||
- **State management:** Zustand (client), SQLite (server persistence)
|
||||
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
|
||||
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
|
||||
@@ -434,7 +434,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
|
||||
|
||||
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
|
||||
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 159 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
5. **Database layer:** Operations go through `src/lib/db/` modules (120 domain-specific files, 160 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
|
||||
|
||||
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.
|
||||
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
"!**/*.test.js",
|
||||
"!**/*.test.mjs",
|
||||
"!**/*.spec.ts",
|
||||
"!**/*.spec.tsx"
|
||||
"!**/*.spec.tsx",
|
||||
"!**/*.nft.json"
|
||||
],
|
||||
"workspaces": [
|
||||
"open-sse",
|
||||
|
||||
@@ -30,10 +30,12 @@
|
||||
* Requires `npm run build:cli` first — this is a --with-build gate, like check:pack-boot.
|
||||
*/
|
||||
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
const BOOT_DEADLINE_MS = 180_000;
|
||||
@@ -43,6 +45,59 @@ const ALLOWLIST_PATH = "config/quality/install-upgrade-allowlist.json";
|
||||
const log = (msg) => console.log(`[install-upgrade] ${msg}`);
|
||||
const warn = (msg) => console.log(`[install-upgrade] ⚠️ ${msg}`);
|
||||
|
||||
/** Root of the installed package inside an `npm install -g --prefix` tree. */
|
||||
function packageRootFor(prefix) {
|
||||
return path.join(prefix, "lib", "node_modules", "omniroute");
|
||||
}
|
||||
|
||||
/**
|
||||
* Credential for the health probe.
|
||||
*
|
||||
* GHSA-mvf8-qc78-5mxm hardened /api/monitoring/health: an ANONYMOUS caller now gets only
|
||||
* `{ status }` — the version, node version, pid and provider config are reserved for a
|
||||
* management principal (src/app/api/monitoring/health/route.ts → publicHealthView). An
|
||||
* unauthenticated probe therefore reads `body.version === undefined`, and this gate's
|
||||
* version assertion could never pass again; the v3.8.50 publish run failed with
|
||||
* "clean: health reports version undefined, expected 3.8.50" for exactly that reason.
|
||||
*
|
||||
* The gate spawns the server itself, so it can mint the credential instead of guessing one:
|
||||
* `OMNIROUTE_INTERNAL_SERVICE_TOKEN` + the `x-omniroute-internal-service-token` header is
|
||||
* accepted by requireManagementAuth() via isTrustedLoopbackInternalServiceRequest(), and the
|
||||
* probe is loopback by construction. Unlike the machine token `check:pack-boot` derives, this
|
||||
* does not depend on a readable machine-id, and an older PREVIOUS version that never gated
|
||||
* health simply ignores the header. The assertion keeps its full strength — it just presents
|
||||
* a credential.
|
||||
*/
|
||||
const INTERNAL_SERVICE_TOKEN = crypto.randomBytes(32).toString("hex");
|
||||
const INTERNAL_SERVICE_HEADER = "x-omniroute-internal-service-token";
|
||||
|
||||
/**
|
||||
* Secondary credential: the same loopback machine token `check:pack-boot` derives from the
|
||||
* packaged CLI. Sent alongside the internal-service token so a build that only honours one
|
||||
* of the two still answers with the full payload.
|
||||
*/
|
||||
function derivePackagedCliToken(prefix) {
|
||||
const cliModuleUrl = pathToFileURL(
|
||||
path.join(packageRootFor(prefix), "bin", "cli", "utils", "cliToken.mjs")
|
||||
).href;
|
||||
try {
|
||||
return execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
"import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))",
|
||||
cliModuleUrl,
|
||||
],
|
||||
{ encoding: "utf8", env: { ...process.env } }
|
||||
).trim();
|
||||
} catch {
|
||||
// A truncated/broken install cannot derive a token. Returning null keeps the boot
|
||||
// probe running (it will fail loudly on its own) instead of crashing the gate here.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pickTarball(packJson) {
|
||||
const filename = JSON.parse(packJson)?.[0]?.filename;
|
||||
if (!filename) throw new Error("npm pack --json returned no filename");
|
||||
@@ -121,6 +176,11 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
if (!fs.existsSync(binPath)) {
|
||||
return { ok: false, failures: [`${label}: bin not found at ${binPath}`], tail: [] };
|
||||
}
|
||||
const cliToken = derivePackagedCliToken(prefix);
|
||||
const probeHeaders = {
|
||||
[INTERNAL_SERVICE_HEADER]: INTERNAL_SERVICE_TOKEN,
|
||||
...(cliToken ? { "x-omniroute-cli-token": cliToken } : {}),
|
||||
};
|
||||
const child = spawn(binPath, ["serve", "--port", String(port)], {
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -130,6 +190,7 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
API_KEY_SECRET: "install-upgrade-gate-api-key-secret-long",
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true",
|
||||
OMNIROUTE_SKIP_SYSTEM_TRUST: "1",
|
||||
OMNIROUTE_INTERNAL_SERVICE_TOKEN: INTERNAL_SERVICE_TOKEN,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
@@ -151,20 +212,33 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
let result = { ok: false, failures: [`${label}: never became healthy`], tail };
|
||||
while (Date.now() < deadline) {
|
||||
if (childExit !== null) {
|
||||
result = { ok: false, failures: [`${label}: exited with code ${childExit} before serving`], tail };
|
||||
result = {
|
||||
ok: false,
|
||||
failures: [`${label}: exited with code ${childExit} before serving`],
|
||||
tail,
|
||||
};
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`);
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, {
|
||||
headers: probeHeaders,
|
||||
});
|
||||
const body = await res.json().catch(() => null);
|
||||
if (res.status === 200 && body && typeof body === "object") {
|
||||
const failures = [];
|
||||
// `status` may legitimately report degraded (no providers configured) — the gate
|
||||
// targets boot crashes and version mismatches, not health of a bare install.
|
||||
if (expectVersion && body.version !== expectVersion) {
|
||||
failures.push(`${label}: health reports version ${body.version}, expected ${expectVersion}`);
|
||||
const reportedVersion = body.version ?? body.system?.version;
|
||||
if (expectVersion && reportedVersion !== expectVersion) {
|
||||
failures.push(
|
||||
`${label}: health reports version ${reportedVersion}, expected ${expectVersion}` +
|
||||
(reportedVersion === undefined
|
||||
? " — the payload carries no version at all, which is the ANONYMOUS health " +
|
||||
"view: the probe's credentials were not accepted (see GHSA-mvf8-qc78-5mxm)"
|
||||
: "")
|
||||
);
|
||||
}
|
||||
result = { ok: failures.length === 0, version: body.version, failures, tail };
|
||||
result = { ok: failures.length === 0, version: reportedVersion, failures, tail };
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
@@ -183,13 +257,72 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function npmInstallInto(prefix, spec) {
|
||||
execFileSync("npm", ["install", "-g", "--prefix", prefix, "--no-audit", "--no-fund", spec], {
|
||||
encoding: "utf8",
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
});
|
||||
/**
|
||||
* `npm install` reports ENOSPC as a *warning* per failed tar entry and still exits 0.
|
||||
*
|
||||
* That is not a theoretical concern: on the v3.8.50 publish run the Phase B upgrade install
|
||||
* emitted 5611 `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` lines, exited
|
||||
* 0, and left a truncated package behind. `omniroute serve` then "exited with code 0 before
|
||||
* serving", no migration ever ran, and the gate concluded the release was missing 15 tables
|
||||
* — a full false alarm produced by a full disk. Each install tree is ~3 GB, and the run
|
||||
* builds two of them plus a ~275 MB tarball.
|
||||
*
|
||||
* So: surface the truncation at the install, where it is unambiguous.
|
||||
*/
|
||||
function npmInstallInto(prefix, spec, label = spec) {
|
||||
// spawnSync (not execFileSync): execFileSync forwards the child's stderr straight to the
|
||||
// parent's, so the ENOSPC warnings scrolled past in CI without the script ever seeing
|
||||
// them. spawnSync hands both streams back.
|
||||
const run = spawnSync(
|
||||
"npm",
|
||||
["install", "-g", "--prefix", prefix, "--no-audit", "--no-fund", spec],
|
||||
{ encoding: "utf8", maxBuffer: 512 * 1024 * 1024 }
|
||||
);
|
||||
const output = `${run.stdout ?? ""}${run.stderr ?? ""}`;
|
||||
// Keep the install log visible, but a truncated package emits thousands of identical
|
||||
// warnings — collapse them so the real message is not buried.
|
||||
const stderrLines = String(run.stderr ?? "").split("\n");
|
||||
const shown = stderrLines.length > 60 ? stderrLines.slice(0, 40) : stderrLines;
|
||||
if (String(run.stderr ?? "").trim()) {
|
||||
process.stderr.write(shown.join("\n") + "\n");
|
||||
if (stderrLines.length > 60) {
|
||||
process.stderr.write(`[install-upgrade] … ${stderrLines.length - 40} more npm line(s)\n`);
|
||||
}
|
||||
}
|
||||
assertNoDiskExhaustion(output, label);
|
||||
if (run.error) throw run.error;
|
||||
if (run.status !== 0) {
|
||||
throw new Error(`${label}: npm install exited with code ${run.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertNoDiskExhaustion(output, label) {
|
||||
if (!/ENOSPC|no space left on device/i.test(output)) return;
|
||||
const count = (output.match(/ENOSPC/g) ?? []).length;
|
||||
throw new Error(
|
||||
`${label}: the install ran out of disk space (${count} ENOSPC error(s) from npm). ` +
|
||||
`The package tree is truncated, so anything measured from it — boot, schema, ` +
|
||||
`migrations — is meaningless. Free space in ${workDirForMessages} (each install tree is ` +
|
||||
`~3 GB) and re-run. This is an environment failure, NOT a schema divergence.`
|
||||
);
|
||||
}
|
||||
|
||||
/** Best-effort free bytes on the filesystem backing `dir`, or null when unavailable. */
|
||||
function freeBytes(dir) {
|
||||
try {
|
||||
return fs.statfsSync(dir).bavail * fs.statfsSync(dir).bsize;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const GB = 1024 ** 3;
|
||||
|
||||
// Set once the work directory exists, so the ENOSPC message names the filesystem that
|
||||
// actually ran out — pointing at /tmp when the gate works elsewhere sends the reader to
|
||||
// free space on the wrong volume (which is what happened during the v3.8.50 publish).
|
||||
let workDirForMessages = os.tmpdir();
|
||||
|
||||
function resolvePreviousVersion(current, explicit) {
|
||||
if (explicit) return explicit;
|
||||
const out = execFileSync("npm", ["view", "omniroute", "dist-tags.latest"], { encoding: "utf8" });
|
||||
@@ -198,7 +331,9 @@ function resolvePreviousVersion(current, explicit) {
|
||||
if (latest === current) {
|
||||
// The version under test is already published (re-run of a shipped release): step back
|
||||
// to the highest published version strictly below it.
|
||||
const all = JSON.parse(execFileSync("npm", ["view", "omniroute", "versions", "--json"], { encoding: "utf8" }));
|
||||
const all = JSON.parse(
|
||||
execFileSync("npm", ["view", "omniroute", "versions", "--json"], { encoding: "utf8" })
|
||||
);
|
||||
const stable = all.filter((v) => !/-(rc|alpha|beta|pre|next)/.test(v) && v !== current);
|
||||
return stable[stable.length - 1];
|
||||
}
|
||||
@@ -218,11 +353,25 @@ async function main() {
|
||||
}
|
||||
const version = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
|
||||
const allowlist = loadAllowlist(ROOT);
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-"));
|
||||
// NOT os.tmpdir(): on the self-hosted runner /tmp is a 12 GB tmpfs backed by RAM, while
|
||||
// the root filesystem has ~66 GB free. This gate needs ~12 GB, so it exhausted the tmpfs
|
||||
// and npm truncated the package — 58269 ENOSPC errors on the v3.8.50 publish, which the
|
||||
// previous code could only report as a crash. Freeing disk did not help because the disk
|
||||
// was never the constraint. Work on real disk beside the repo instead.
|
||||
const workRoot =
|
||||
process.env.OMNIROUTE_INSTALL_UPGRADE_WORKDIR || path.join(ROOT, ".install-upgrade");
|
||||
fs.mkdirSync(workRoot, { recursive: true });
|
||||
const tmp = fs.mkdtempSync(path.join(workRoot, "omniroute-install-upgrade-"));
|
||||
workDirForMessages = tmp;
|
||||
const failures = [];
|
||||
const warnings = [];
|
||||
|
||||
try {
|
||||
// Timed, because this turned out to be the expensive part: on the 2026-08-27
|
||||
// v3.8.50 publish `npm pack` alone took 24m37s, leaving 5 of the step's 30-minute
|
||||
// budget for two installs and two boots. Without a duration here the log showed
|
||||
// only "packing…" then a timeout, which reads like a hang and is not.
|
||||
const packStarted = Date.now();
|
||||
log(`packing v${version}…`);
|
||||
const packOut = execFileSync("npm", ["pack", "--json", "--pack-destination", tmp], {
|
||||
cwd: ROOT,
|
||||
@@ -230,13 +379,31 @@ async function main() {
|
||||
maxBuffer: 128 * 1024 * 1024,
|
||||
});
|
||||
const tarball = path.join(tmp, pickTarball(packOut));
|
||||
const packMb = (fs.statSync(tarball).size / 1024 / 1024).toFixed(1);
|
||||
log(`packed in ${Math.round((Date.now() - packStarted) / 1000)}s (${packMb} MB)`);
|
||||
|
||||
// Each install tree is ~3 GB and this run builds two of them, side by side, plus the
|
||||
// ~275 MB tarball. On the v3.8.50 publish run that overflowed the runner disk mid-way
|
||||
// through the Phase B upgrade install; npm warned per truncated tar entry and still
|
||||
// exited 0, and every later measurement was taken from a broken tree.
|
||||
const availableBytes = freeBytes(tmp);
|
||||
if (availableBytes !== null) {
|
||||
log(`free space in ${tmp}: ${(availableBytes / GB).toFixed(1)} GB`);
|
||||
if (availableBytes < 12 * GB) {
|
||||
warn(
|
||||
`only ${(availableBytes / GB).toFixed(1)} GB free — this gate needs roughly 12 GB ` +
|
||||
`(two ~3 GB install trees, the second installed over twice, plus the tarball). ` +
|
||||
`An install truncated by ENOSPC looks like a schema divergence.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Phase A: clean install -------------------------------------------------
|
||||
log("PHASE A — clean install of the packed tarball");
|
||||
const aPrefix = path.join(tmp, "a-prefix");
|
||||
const aData = path.join(tmp, "a-data");
|
||||
fs.mkdirSync(aData, { recursive: true });
|
||||
npmInstallInto(aPrefix, tarball);
|
||||
npmInstallInto(aPrefix, tarball, "clean install");
|
||||
const a = await bootAndProbe({
|
||||
prefix: aPrefix,
|
||||
dataDir: aData,
|
||||
@@ -245,14 +412,34 @@ async function main() {
|
||||
label: "clean",
|
||||
});
|
||||
failures.push(...a.failures);
|
||||
const cleanBooted = a.ok;
|
||||
if (a.ok) log(`clean install healthy on v${a.version}`);
|
||||
else if (a.tail?.length) {
|
||||
console.error("[install-upgrade] last output from the clean-install server:");
|
||||
console.error(a.tail.join("").split("\n").slice(-40).join("\n"));
|
||||
}
|
||||
const aDb = findDb(aData);
|
||||
const freshTables = aDb ? readTables(aDb) : null;
|
||||
if (!freshTables) failures.push("clean: no SQLite database was created");
|
||||
else log(`clean install schema: ${freshTables.size} tables`);
|
||||
|
||||
// Phase A is fully measured (boot verdict + schema snapshot); its ~3 GB install tree is
|
||||
// dead weight from here on and Phase B needs the room. The DATA_DIR stays — only the
|
||||
// node_modules tree goes.
|
||||
if (!skipUpgrade) {
|
||||
fs.rmSync(aPrefix, { recursive: true, force: true });
|
||||
const reclaimed = freeBytes(tmp);
|
||||
log(
|
||||
"released the clean-install tree before the upgrade phase" +
|
||||
(reclaimed !== null ? ` (${(reclaimed / GB).toFixed(1)} GB free)` : "")
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Phase B: upgrade over the previous published version -------------------
|
||||
let upgradedTables = null;
|
||||
// `--skip-upgrade` never reaches the convergence block (upgradedTables stays null), so
|
||||
// defaulting this to true keeps that path unchanged.
|
||||
let upgradeBooted = true;
|
||||
if (skipUpgrade) {
|
||||
warn("PHASE B skipped (--skip-upgrade)");
|
||||
} else {
|
||||
@@ -262,7 +449,7 @@ async function main() {
|
||||
const bData = path.join(tmp, "b-data");
|
||||
fs.mkdirSync(bData, { recursive: true });
|
||||
|
||||
npmInstallInto(bPrefix, `omniroute@${previous}`);
|
||||
npmInstallInto(bPrefix, `omniroute@${previous}`, `previous(${previous}) install`);
|
||||
const before = await bootAndProbe({
|
||||
prefix: bPrefix,
|
||||
dataDir: bData,
|
||||
@@ -273,14 +460,16 @@ async function main() {
|
||||
if (!before.ok) {
|
||||
// A broken PREVIOUS version is not this release's fault — degrade to a warning so a
|
||||
// historically bad publish cannot block the current one.
|
||||
warnings.push(`previous version ${previous} did not boot cleanly — upgrade path unverified`);
|
||||
warnings.push(
|
||||
`previous version ${previous} did not boot cleanly — upgrade path unverified`
|
||||
);
|
||||
for (const f of before.failures) warn(f);
|
||||
} else {
|
||||
const beforeDb = findDb(bData);
|
||||
const beforeTables = beforeDb ? readTables(beforeDb) : new Set();
|
||||
log(`previous(${previous}) schema: ${beforeTables.size} tables — upgrading in place`);
|
||||
|
||||
npmInstallInto(bPrefix, tarball);
|
||||
npmInstallInto(bPrefix, tarball, "upgrade install");
|
||||
const after = await bootAndProbe({
|
||||
prefix: bPrefix,
|
||||
dataDir: bData,
|
||||
@@ -290,6 +479,11 @@ async function main() {
|
||||
});
|
||||
failures.push(...after.failures);
|
||||
if (after.ok) log(`upgrade healthy on v${after.version}`);
|
||||
upgradeBooted = after.ok;
|
||||
if (!after.ok && after.tail?.length) {
|
||||
console.error("[install-upgrade] last output from the upgraded server:");
|
||||
console.error(after.tail.join("").split("\n").slice(-40).join("\n"));
|
||||
}
|
||||
|
||||
const afterDb = findDb(bData);
|
||||
upgradedTables = afterDb ? readTables(afterDb) : null;
|
||||
@@ -306,7 +500,19 @@ async function main() {
|
||||
}
|
||||
|
||||
// ---- Schema convergence -----------------------------------------------------
|
||||
if (freshTables && upgradedTables) {
|
||||
// Only meaningful when BOTH servers actually served. A boot that died before serving
|
||||
// never ran a migration, so its database still holds the PREVIOUS release's schema and
|
||||
// every post-baseline table shows up as "a clean install creates but an upgrade does
|
||||
// not" — which is what the v3.8.50 publish run reported after ENOSPC truncated the
|
||||
// upgrade install. Comparing there does not add information, it manufactures a
|
||||
// 15-table false alarm on top of the real failure. The run still fails: the boot
|
||||
// failure is already in `failures`.
|
||||
if (freshTables && upgradedTables && !(cleanBooted && upgradeBooted)) {
|
||||
warn(
|
||||
"schema convergence NOT evaluated — a phase failed to boot, so its database was " +
|
||||
"never migrated and any table difference would describe the broken boot, not the schema"
|
||||
);
|
||||
} else if (freshTables && upgradedTables) {
|
||||
const verdict = evaluateConvergence({
|
||||
freshTables,
|
||||
upgradedTables,
|
||||
@@ -334,7 +540,10 @@ async function main() {
|
||||
|
||||
// Only run the (expensive) gate when invoked directly — importing this module for the pure
|
||||
// helper above must not pack, install or boot anything.
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)) {
|
||||
if (
|
||||
process.argv[1] &&
|
||||
path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname)
|
||||
) {
|
||||
main().catch((err) => {
|
||||
console.error(`[install-upgrade] crashed: ${err?.message ?? err}`);
|
||||
process.exit(1);
|
||||
|
||||
@@ -42,6 +42,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { findProvenanceOnSelfHosted, formatProvenanceFinding } from "./lib/provenanceRunner.mjs";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const WORKFLOWS_DIR = path.join(ROOT, ".github", "workflows");
|
||||
@@ -275,6 +276,23 @@ export function runZizmor(workflowsDir) {
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hard rule (not a lint count): `--provenance` inside a job that runs on a
|
||||
* self-hosted runner. npm answers 422 at the registry, and in v3.8.50 that
|
||||
* answer only came after the tag, the GitHub Release and the Docker images were
|
||||
* already out. Blocks under --strict AND --ratchet (the CI mode); plain mode
|
||||
* reports it like everything else.
|
||||
* @param {string[]} files absolute workflow paths
|
||||
*/
|
||||
export function runProvenanceRunnerCheck(files) {
|
||||
const findings = [];
|
||||
for (const file of files) {
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
findings.push(...findProvenanceOnSelfHosted(text, path.relative(ROOT, file)));
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const hasActionlint = isBinaryAvailable("actionlint");
|
||||
const hasZizmor = isBinaryAvailable("zizmor");
|
||||
@@ -350,6 +368,16 @@ function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const provenanceFindings = runProvenanceRunnerCheck(workflowFiles);
|
||||
if (provenanceFindings.length > 0) {
|
||||
console.error(
|
||||
`[check-workflows] provenance×self-hosted: ${provenanceFindings.length} finding(s) — HARD RULE:`
|
||||
);
|
||||
provenanceFindings.forEach((f) => console.error(` ${formatProvenanceFinding(f)}`));
|
||||
} else if (!QUIET) {
|
||||
console.log("[check-workflows] provenance×self-hosted: OK (0 findings)");
|
||||
}
|
||||
|
||||
const total = actionlintCount + zizmorCount;
|
||||
process.stdout.write(`workflowFindings=${total}\n`);
|
||||
process.stdout.write(`actionlintFindings=${actionlintCount}\n`);
|
||||
@@ -357,6 +385,15 @@ function main() {
|
||||
// Read this line with the count above: a finding total is only reproducible against the
|
||||
// version that produced it. See zizmorVersion().
|
||||
process.stdout.write(`zizmorVersion=${hasZizmor ? zizmorVersion() : "absent"}\n`);
|
||||
process.stdout.write(`provenanceRunnerFindings=${provenanceFindings.length}\n`);
|
||||
if ((STRICT || RATCHET) && provenanceFindings.length > 0) {
|
||||
console.error(
|
||||
`\n[check-workflows] FAIL — ${provenanceFindings.length} job(s) publish with --provenance from a self-hosted runner.\n` +
|
||||
" npm rejects that with 422 at the registry. Move the upload step to a github-hosted job\n" +
|
||||
" (see .github/workflows/npm-publish.yml `stage-npm` for the pattern)."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (STRICT && total > 0) {
|
||||
console.error(`\n[check-workflows] FAIL — ${total} workflow finding(s) total (--strict mode).`);
|
||||
|
||||
83
scripts/check/lib/provenanceRunner.mjs
Normal file
83
scripts/check/lib/provenanceRunner.mjs
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* scripts/check/lib/provenanceRunner.mjs
|
||||
*
|
||||
* npm refuses `--provenance` from a self-hosted runner:
|
||||
*
|
||||
* 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
||||
* Unsupported GitHub Actions runner environment: "self-hosted".
|
||||
* Only "github-hosted" runners are supported when publishing with provenance.
|
||||
*
|
||||
* v3.8.50 hit this at the very end of a 76-minute publish job — after the tag,
|
||||
* the GitHub Release and the Docker images were already public — because
|
||||
* `USE_VPS_RUNNER` had been turned on (2026-08-02) with no release in between to
|
||||
* surface it. The combination is greppable, so it must fail in CI the moment a
|
||||
* workflow introduces it, not four weeks later at the registry.
|
||||
*
|
||||
* Pure: takes workflow YAML text, returns the offending (job, step) pairs.
|
||||
*/
|
||||
import { load as yamlLoad } from "js-yaml";
|
||||
|
||||
const SELF_HOSTED = /\bself-hosted\b/;
|
||||
const EXPRESSION = /\$\{\{/;
|
||||
// Lookahead, not \b: `--provenance-file=…` is a different flag (a pre-built
|
||||
// bundle) and must not match — a word boundary sits between "e" and "-".
|
||||
const PROVENANCE = /(^|\s)--provenance(?=\s|=|$)/m;
|
||||
|
||||
/**
|
||||
* Classifies a job's `runs-on` value.
|
||||
* @returns {"self-hosted"|"hosted"|"unknown"}
|
||||
* "unknown" = an expression with no literal `self-hosted` in it (e.g.
|
||||
* `${{ matrix.os }}`); the check does not guess, it skips.
|
||||
*/
|
||||
export function classifyRunsOn(runsOn) {
|
||||
if (runsOn == null) return "unknown";
|
||||
if (typeof runsOn === "string") {
|
||||
if (SELF_HOSTED.test(runsOn)) return "self-hosted";
|
||||
return EXPRESSION.test(runsOn) ? "unknown" : "hosted";
|
||||
}
|
||||
if (Array.isArray(runsOn)) {
|
||||
return runsOn.some((v) => typeof v === "string" && SELF_HOSTED.test(v))
|
||||
? "self-hosted"
|
||||
: "hosted";
|
||||
}
|
||||
if (typeof runsOn === "object") {
|
||||
// { group: ..., labels: ... } form
|
||||
const labels = runsOn.labels;
|
||||
return classifyRunsOn(Array.isArray(labels) ? labels : labels == null ? "" : String(labels));
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} yamlText
|
||||
* @param {string} fileName used only for reporting
|
||||
* @returns {{ file: string, job: string, step: string }[]}
|
||||
*/
|
||||
export function findProvenanceOnSelfHosted(yamlText, fileName = "<workflow>") {
|
||||
let doc;
|
||||
try {
|
||||
doc = yamlLoad(yamlText);
|
||||
} catch {
|
||||
// actionlint owns syntax; an unparseable file is not this rule's finding.
|
||||
return [];
|
||||
}
|
||||
const jobs =
|
||||
doc && typeof doc === "object" && doc.jobs && typeof doc.jobs === "object" ? doc.jobs : {};
|
||||
const findings = [];
|
||||
for (const [jobName, job] of Object.entries(jobs)) {
|
||||
if (!job || typeof job !== "object") continue;
|
||||
if (classifyRunsOn(job["runs-on"]) !== "self-hosted") continue;
|
||||
const steps = Array.isArray(job.steps) ? job.steps : [];
|
||||
steps.forEach((step, i) => {
|
||||
if (step && typeof step.run === "string" && PROVENANCE.test(step.run)) {
|
||||
findings.push({ file: fileName, job: jobName, step: step.name || `#${i + 1}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
/** Human-readable line per finding, used by the CLI. */
|
||||
export function formatProvenanceFinding(f) {
|
||||
return `${f.file}: job "${f.job}", step "${f.step}" runs \`--provenance\` on a self-hosted runner — npm rejects that (422). Move the upload to a github-hosted job.`;
|
||||
}
|
||||
43
src/lib/db/migrations/163_model_capabilities.sql
Normal file
43
src/lib/db/migrations/163_model_capabilities.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- 163_model_capabilities.sql
|
||||
--
|
||||
-- Promote `model_capabilities` from a lazily-created runtime table to a real migration.
|
||||
--
|
||||
-- WHY: the table was only ever created by `ensureCapabilitiesTable()` in
|
||||
-- src/lib/modelsDevSync.ts, on demand, the first time a models.dev capability sync ran.
|
||||
-- Whether a database has it therefore depends on TIMING, not on the schema version — so a
|
||||
-- clean install and an upgraded install diverge for no structural reason. The v3.8.50
|
||||
-- publish run hit exactly that: `check:install-upgrade` reported `model_capabilities` as a
|
||||
-- table "present only after upgrade", because the older database had already run a sync
|
||||
-- and the freshly-installed one had not.
|
||||
--
|
||||
-- Creating it here makes both install paths converge deterministically.
|
||||
-- `ensureCapabilitiesTable()` stays in place as an idempotent safety net (it is a
|
||||
-- CREATE TABLE IF NOT EXISTS and now always a no-op); tests/unit/db-install-upgrade-schema-parity.test.ts
|
||||
-- pins the two definitions against drift.
|
||||
--
|
||||
-- IF NOT EXISTS is required, not decorative: every database that ever ran a models.dev
|
||||
-- sync already has this table, and this migration must be a no-op there.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS model_capabilities (
|
||||
provider TEXT NOT NULL,
|
||||
model_id TEXT NOT NULL,
|
||||
tool_call BOOLEAN,
|
||||
reasoning BOOLEAN,
|
||||
attachment BOOLEAN,
|
||||
structured_output BOOLEAN,
|
||||
temperature BOOLEAN,
|
||||
modalities_input TEXT,
|
||||
modalities_output TEXT,
|
||||
knowledge_cutoff TEXT,
|
||||
release_date TEXT,
|
||||
last_updated TEXT,
|
||||
status TEXT,
|
||||
family TEXT,
|
||||
open_weights BOOLEAN,
|
||||
limit_context INTEGER,
|
||||
limit_input INTEGER,
|
||||
limit_output INTEGER,
|
||||
interleaved_field TEXT,
|
||||
last_synced TEXT,
|
||||
PRIMARY KEY (provider, model_id)
|
||||
);
|
||||
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
// @ts-expect-error — plain .mjs gate script, no type declarations by design
|
||||
import { evaluateConvergence } from "../../scripts/check/check-install-upgrade.mjs";
|
||||
import {
|
||||
assertNoDiskExhaustion,
|
||||
evaluateConvergence,
|
||||
} from "../../scripts/check/check-install-upgrade.mjs";
|
||||
|
||||
/**
|
||||
* The whole point of this gate is that the two directions of schema divergence are NOT
|
||||
@@ -66,7 +69,11 @@ test("UNKNOWN residue fails — a new divergence must not hide behind the allowl
|
||||
assert.equal(v.ok, false);
|
||||
assert.deepEqual(v.unknownResidue, ["surprise_table"]);
|
||||
assert.match(v.failures[0], /surprise_table/);
|
||||
assert.doesNotMatch(v.failures[0], /cache_metrics/, "the known one must not be re-reported as new");
|
||||
assert.doesNotMatch(
|
||||
v.failures[0],
|
||||
/cache_metrics/,
|
||||
"the known one must not be re-reported as new"
|
||||
);
|
||||
});
|
||||
|
||||
test("both directions at once report both failures", () => {
|
||||
@@ -93,3 +100,43 @@ test("empty/missing inputs do not crash", () => {
|
||||
assert.equal(v.ok, true);
|
||||
assert.deepEqual(v.onlyFresh, []);
|
||||
});
|
||||
|
||||
// ─── ENOSPC guard ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The v3.8.50 publish run (CI 33104507735) failed with "15 tables a CLEAN install creates
|
||||
// but an UPGRADE does not". None of them was missing: the Phase B upgrade install had hit
|
||||
// `npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device` 5611 times, npm still
|
||||
// exited 0, the truncated `omniroute serve` "exited with code 0 before serving", and the
|
||||
// database therefore still held the 3.8.49 schema. npm reporting disk exhaustion as a
|
||||
// warning is what let a full disk masquerade as a schema defect.
|
||||
|
||||
test("npm ENOSPC warnings are raised as an install failure, not ignored", () => {
|
||||
const enospc = "npm warn tar TAR_ENTRY_ERROR ENOSPC: no space left on device, write\n".repeat(3);
|
||||
assert.throws(
|
||||
() => assertNoDiskExhaustion(enospc, "upgrade install"),
|
||||
(err: Error) => {
|
||||
assert.match(err.message, /upgrade install/);
|
||||
assert.match(err.message, /ran out of disk space/);
|
||||
assert.match(err.message, /3 ENOSPC error/);
|
||||
// The operator must not go looking for a migration that is not missing.
|
||||
assert.match(err.message, /NOT a schema divergence/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("a clean install log does not trip the disk guard", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
assertNoDiskExhaustion(
|
||||
"npm warn deprecated boolean@3.2.0: Package no longer supported.\nadded 900 packages\n",
|
||||
"clean install"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("the guard also catches the bare kernel message without the ENOSPC code", () => {
|
||||
assert.throws(
|
||||
() => assertNoDiskExhaustion("Error: no space left on device", "clean install"),
|
||||
/ran out of disk space/
|
||||
);
|
||||
});
|
||||
|
||||
145
tests/unit/check-workflows-provenance-runner.test.ts
Normal file
145
tests/unit/check-workflows-provenance-runner.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import {
|
||||
classifyRunsOn,
|
||||
findProvenanceOnSelfHosted,
|
||||
} from "../../scripts/check/lib/provenanceRunner.mjs";
|
||||
|
||||
/**
|
||||
* v3.8.50, 10th publish attempt, 76 minutes in — after the tag, the GitHub
|
||||
* Release and the Docker images were already public:
|
||||
*
|
||||
* 422 Unprocessable Entity - Error verifying sigstore provenance bundle:
|
||||
* Unsupported GitHub Actions runner environment: "self-hosted".
|
||||
*
|
||||
* `USE_VPS_RUNNER` had routed the publish job to the .113 pool on 2026-08-02;
|
||||
* no release happened between 07-30 and 08-28, so nothing surfaced it. The
|
||||
* pairing is pure text, so it must fail the workflow lint on the PR that
|
||||
* introduces it.
|
||||
*/
|
||||
const ROOT = join(import.meta.dirname, "../..");
|
||||
const WORKFLOWS = join(ROOT, ".github/workflows");
|
||||
|
||||
// The exact runs-on expression npm-publish.yml used when it broke.
|
||||
const VPS_EXPR =
|
||||
"${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('[\"self-hosted\",\"omni-release\"]') || 'ubuntu-latest' }}";
|
||||
|
||||
function workflow(runsOn: string, run: string, extra = ""): string {
|
||||
return [
|
||||
"name: t",
|
||||
"on: push",
|
||||
"jobs:",
|
||||
" publish:",
|
||||
` runs-on: ${runsOn}`,
|
||||
extra,
|
||||
" steps:",
|
||||
" - name: upload",
|
||||
` run: ${run}`,
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
test("classifyRunsOn: literal, array, object-with-labels and the fromJSON expression are self-hosted", () => {
|
||||
assert.equal(classifyRunsOn("self-hosted"), "self-hosted");
|
||||
assert.equal(classifyRunsOn(["self-hosted", "omni-release"]), "self-hosted");
|
||||
assert.equal(classifyRunsOn({ group: "Default", labels: ["self-hosted"] }), "self-hosted");
|
||||
assert.equal(classifyRunsOn(VPS_EXPR), "self-hosted");
|
||||
});
|
||||
|
||||
test("classifyRunsOn: hosted labels are hosted, opaque expressions are unknown (never guessed)", () => {
|
||||
assert.equal(classifyRunsOn("ubuntu-latest"), "hosted");
|
||||
assert.equal(classifyRunsOn(["ubuntu-latest"]), "hosted");
|
||||
assert.equal(classifyRunsOn("${{ matrix.os }}"), "unknown");
|
||||
assert.equal(classifyRunsOn(undefined), "unknown");
|
||||
});
|
||||
|
||||
test("flags --provenance inside a job routed to the self-hosted pool", () => {
|
||||
const found = findProvenanceOnSelfHosted(
|
||||
workflow(
|
||||
`"${VPS_EXPR.replace(/"/g, '\\"')}"`,
|
||||
'npm stage publish --provenance --access public --tag "$TAG"'
|
||||
),
|
||||
"npm-publish.yml"
|
||||
);
|
||||
assert.deepEqual(found, [{ file: "npm-publish.yml", job: "publish", step: "upload" }]);
|
||||
});
|
||||
|
||||
test("also catches the literal label and the --provenance-file form", () => {
|
||||
assert.equal(
|
||||
findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --provenance")).length,
|
||||
1
|
||||
);
|
||||
assert.equal(
|
||||
findProvenanceOnSelfHosted(
|
||||
workflow("[self-hosted, omni-release]", "npm publish --provenance-file=./p.json")
|
||||
).length,
|
||||
0,
|
||||
"--provenance-file is a different flag (a pre-built bundle) and is not what the registry rejects"
|
||||
);
|
||||
assert.equal(
|
||||
findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --provenance=true")).length,
|
||||
1
|
||||
);
|
||||
});
|
||||
|
||||
test("does not flag hosted jobs, unknown runners, or self-hosted jobs without the flag", () => {
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(workflow("ubuntu-latest", "npm publish --provenance")),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(workflow("${{ matrix.os }}", "npm publish --provenance")),
|
||||
[]
|
||||
);
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(workflow("self-hosted", "npm publish --access public")),
|
||||
[]
|
||||
);
|
||||
// The word only in a step NAME or a comment is not a finding.
|
||||
assert.deepEqual(
|
||||
findProvenanceOnSelfHosted(
|
||||
[
|
||||
"name: t",
|
||||
"on: push",
|
||||
"jobs:",
|
||||
" j:",
|
||||
" runs-on: self-hosted",
|
||||
" steps:",
|
||||
" - name: provenance note",
|
||||
" run: echo hi # --provenance later",
|
||||
"",
|
||||
].join("\n")
|
||||
),
|
||||
[],
|
||||
"a comment after the command is still part of the run string — accept that the regex is conservative"
|
||||
);
|
||||
});
|
||||
|
||||
test("reusable-workflow jobs (uses:) and unparseable YAML are not this rule's findings", () => {
|
||||
const reusable = [
|
||||
"name: t",
|
||||
"on: push",
|
||||
"jobs:",
|
||||
" j:",
|
||||
" uses: ./.github/workflows/x.yml",
|
||||
"",
|
||||
].join("\n");
|
||||
assert.deepEqual(findProvenanceOnSelfHosted(reusable), []);
|
||||
assert.deepEqual(findProvenanceOnSelfHosted("jobs: [unclosed"), []);
|
||||
});
|
||||
|
||||
test("regression guard: no workflow in this repo publishes with --provenance from a self-hosted runner", () => {
|
||||
const files = readdirSync(WORKFLOWS).filter((f) => /\.ya?ml$/.test(f));
|
||||
assert.ok(files.length > 10, "expected the real workflow set");
|
||||
const findings = files.flatMap((f) =>
|
||||
findProvenanceOnSelfHosted(readFileSync(join(WORKFLOWS, f), "utf8"), f)
|
||||
);
|
||||
assert.deepEqual(
|
||||
findings,
|
||||
[],
|
||||
`npm rejects provenance from self-hosted runners (422) — move the upload to a github-hosted job: ${JSON.stringify(findings)}`
|
||||
);
|
||||
});
|
||||
118
tests/unit/db-install-upgrade-schema-parity.test.ts
Normal file
118
tests/unit/db-install-upgrade-schema-parity.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// ENVIRONMENT NOTE (sandbox better-sqlite3 / glibc limitation, not a code defect):
|
||||
// This test opens a real SQLite database through `src/lib/db/core.ts`. better-sqlite3 is a
|
||||
// native addon; production and CI load it normally, but some sandboxes ship a system glibc
|
||||
// older than the prebuilt binary requires ("GLIBC_2.29 not found"), in which case the
|
||||
// runtime cascades to node:sqlite/sql.js. See tests/unit/_helpers/betterSqlite3Availability.ts.
|
||||
//
|
||||
// WHY THIS FILE EXISTS
|
||||
// --------------------
|
||||
// `npm run check:install-upgrade` (scripts/check/check-install-upgrade.mjs) proves that a
|
||||
// CLEAN install and an UPGRADE converge on the same schema, but it costs a full `npm pack`
|
||||
// plus three global installs and three boots (~17 min in CI) and it can only ever run at
|
||||
// publish time, against an already-published previous version. It is not a development
|
||||
// feedback loop, and the v3.8.50 publish run is what proved it: the gate reported 15
|
||||
// "missing" tables, and the deterministic half of that verdict was never checkable locally.
|
||||
//
|
||||
// This file pins the deterministic half in milliseconds:
|
||||
//
|
||||
// 1. Every migration file on disk is actually reachable by the runner on a fresh install.
|
||||
// A file the runner never applies is a table no user ever gets.
|
||||
// 2. `model_capabilities` is created by the MIGRATION SET, not lazily at runtime.
|
||||
// A table created on demand by `CREATE TABLE IF NOT EXISTS` inside a feature code
|
||||
// path exists or not depending on whether that feature happened to run before the
|
||||
// snapshot — so it diverges between the two install paths by TIMING, not by schema.
|
||||
// That is precisely how `model_capabilities` surfaced as a divergence on the v3.8.50
|
||||
// publish run (present in the upgraded database, absent from the clean one).
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const MIGRATIONS_DIR = path.join(REPO_ROOT, "src", "lib", "db", "migrations");
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-install-upgrade-parity-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
// A clean install: core.ts applies the inline SCHEMA_SQL, the `ensure*Columns()` helpers,
|
||||
// then runMigrations(). This is the exact code path Phase A of the gate exercises.
|
||||
const db = core.getDbInstance();
|
||||
|
||||
test.after(() => {
|
||||
try {
|
||||
core.resetDbInstance();
|
||||
} catch {
|
||||
/* best effort */
|
||||
}
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function migrationFiles(): Array<{ version: string; name: string }> {
|
||||
return fs
|
||||
.readdirSync(MIGRATIONS_DIR)
|
||||
.sort()
|
||||
.map((file) => /^(\d{3,})_(.+)\.sql$/.exec(file))
|
||||
.filter((m): m is RegExpExecArray => m !== null)
|
||||
.map((m) => ({ version: m[1], name: m[2] }));
|
||||
}
|
||||
|
||||
function hasTable(name: string): boolean {
|
||||
return Boolean(
|
||||
db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
|
||||
);
|
||||
}
|
||||
|
||||
test("a clean install applies every migration file on disk", () => {
|
||||
const ledger = new Set(
|
||||
(
|
||||
db.prepare("SELECT version FROM _omniroute_migrations").all() as Array<{ version: string }>
|
||||
).map((row) => row.version)
|
||||
);
|
||||
const unapplied = migrationFiles()
|
||||
.filter((m) => !ledger.has(m.version))
|
||||
.map((m) => `${m.version}_${m.name}`);
|
||||
assert.deepEqual(
|
||||
unapplied,
|
||||
[],
|
||||
"migration files the runner never applied — an upgrade would not create their tables either"
|
||||
);
|
||||
});
|
||||
|
||||
test("model_capabilities comes from the migration set, not from a lazy runtime CREATE", () => {
|
||||
assert.ok(
|
||||
hasTable("model_capabilities"),
|
||||
"model_capabilities must be created by a migration so a clean install and an upgrade " +
|
||||
"converge deterministically instead of depending on whether the models.dev sync ran"
|
||||
);
|
||||
});
|
||||
|
||||
test("the model_capabilities migration does not drift from ensureCapabilitiesTable()", () => {
|
||||
const source = fs.readFileSync(path.join(REPO_ROOT, "src", "lib", "modelsDevSync.ts"), "utf8");
|
||||
const ddl = /CREATE TABLE IF NOT EXISTS model_capabilities\s*\(([\s\S]*?)\n\s*\)/.exec(source);
|
||||
assert.ok(ddl, "ensureCapabilitiesTable() DDL not found in src/lib/modelsDevSync.ts");
|
||||
|
||||
const runtimeColumns = ddl[1]
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !/^PRIMARY KEY/i.test(line))
|
||||
.map((line) => line.replace(/,$/, "").split(/\s+/)[0])
|
||||
.sort();
|
||||
|
||||
const migrationColumns = (
|
||||
db.prepare("PRAGMA table_info(model_capabilities)").all() as Array<{ name: string }>
|
||||
)
|
||||
.map((column) => column.name)
|
||||
.sort();
|
||||
|
||||
assert.deepEqual(
|
||||
migrationColumns,
|
||||
runtimeColumns,
|
||||
"the migration and the runtime helper must create the same columns — a drift here means " +
|
||||
"an upgraded database keeps the old shape while a clean install gets the new one"
|
||||
);
|
||||
});
|
||||
72
tests/unit/npm-payload-nft-manifests-excluded.test.ts
Normal file
72
tests/unit/npm-payload-nft-manifests-excluded.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import type { Dirent } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v3.8.50 was refused by the registry with `413 Payload Too Large` on
|
||||
* `POST /-/stage/package/omniroute`: the tarball had reached 288.7 MB packed
|
||||
* (1.1 GB unpacked), against 174.5 MB for the 3.8.49 that published fine.
|
||||
*
|
||||
* 668.7 MB of that — 61% of the whole package — was 842 `*.nft.json` files.
|
||||
* Those are Next.js Node File Trace manifests: build-time metadata used to
|
||||
* COMPUTE the standalone bundle, never read while serving. They had doubled
|
||||
* since 3.8.49 (325.0 MB across 748 files), which is what tipped the payload
|
||||
* over the limit.
|
||||
*
|
||||
* The guard is the `files[]` negation, so a future entry that re-widens the
|
||||
* glob (or a rewrite of the array) cannot silently put them back.
|
||||
*/
|
||||
const pkg = JSON.parse(readFileSync(join(import.meta.dirname, "../../package.json"), "utf8")) as {
|
||||
files?: string[];
|
||||
};
|
||||
|
||||
test("package.json files[] excludes Next's .nft.json trace manifests", () => {
|
||||
const files = pkg.files ?? [];
|
||||
assert.ok(files.length > 0, "package.json must declare files[]");
|
||||
assert.ok(
|
||||
files.includes("!**/*.nft.json"),
|
||||
"files[] must negate **/*.nft.json — they are build metadata and were 61% of the 3.8.50 payload"
|
||||
);
|
||||
});
|
||||
|
||||
test("the negation sits after the positive dist/ entry it has to override", () => {
|
||||
// npm applies files[] in order: a negation listed BEFORE the directory that
|
||||
// pulls the files in is a no-op. Positive anchor, so this test cannot pass
|
||||
// just because both strings happen to be present somewhere.
|
||||
const files = pkg.files ?? [];
|
||||
const dist = files.indexOf("dist/");
|
||||
const negation = files.indexOf("!**/*.nft.json");
|
||||
assert.notEqual(dist, -1, "dist/ must still be published");
|
||||
assert.ok(negation > dist, "the .nft.json negation must come after dist/");
|
||||
});
|
||||
|
||||
test("no source module reads a .nft.json at runtime", () => {
|
||||
// If this ever stops holding, the exclusion above becomes a runtime break
|
||||
// rather than a size win — which is exactly the assumption worth pinning.
|
||||
const roots = ["src", "open-sse", "bin"];
|
||||
const hits: string[] = [];
|
||||
for (const root of roots) {
|
||||
const dir = join(import.meta.dirname, "../..", root);
|
||||
const stack = [dir];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop() as string;
|
||||
let entries: Dirent[];
|
||||
try {
|
||||
entries = readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name !== "node_modules") stack.push(full);
|
||||
} else if (/\.(ts|tsx|mjs|js)$/.test(entry.name)) {
|
||||
if (readFileSync(full, "utf8").includes(".nft.json")) hits.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(hits, [], `nothing may depend on .nft.json at runtime: ${hits.join(", ")}`);
|
||||
});
|
||||
Reference in New Issue
Block a user