cachegate 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +12 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -8
- package/Dockerfile +24 -16
- package/README.md +505 -428
- package/package.json +1 -1
- package/server.js +688 -668
- package/test/env-path.test.js +41 -0
- package/OPEN_SOURCE_ROADMAP.md +0 -554
- package/ROADMAP.md +0 -281
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// server.js reads its env vars once at require time; set a key so
|
|
6
|
+
// requiring it here never trips the fail-closed check for unrelated
|
|
7
|
+
// reasons (resolveEnvPathFromArgv itself doesn't touch env at all).
|
|
8
|
+
process.env.MODEL_ROUTER_INTERNAL_KEY = 'test-internal-key';
|
|
9
|
+
|
|
10
|
+
const { resolveEnvPathFromArgv } = require('../server');
|
|
11
|
+
|
|
12
|
+
test('resolveEnvPathFromArgv returns undefined when --env-path is absent - default cwd behavior unchanged', () => {
|
|
13
|
+
const argv = ['node', 'server.js'];
|
|
14
|
+
assert.equal(resolveEnvPathFromArgv(argv), undefined);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('resolveEnvPathFromArgv resolves a space-separated --env-path value', () => {
|
|
18
|
+
const argv = ['node', 'server.js', '--env-path', './router/.env'];
|
|
19
|
+
assert.equal(resolveEnvPathFromArgv(argv), path.resolve('./router/.env'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('resolveEnvPathFromArgv resolves an --env-path=value (equals) form', () => {
|
|
23
|
+
const argv = ['node', 'server.js', '--env-path=./router/.env'];
|
|
24
|
+
assert.equal(resolveEnvPathFromArgv(argv), path.resolve('./router/.env'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('resolveEnvPathFromArgv resolves an absolute path unchanged', () => {
|
|
28
|
+
const absolute = path.resolve('/tmp/some/other/.env');
|
|
29
|
+
const argv = ['node', 'server.js', '--env-path', absolute];
|
|
30
|
+
assert.equal(resolveEnvPathFromArgv(argv), absolute);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('resolveEnvPathFromArgv returns undefined if --env-path is the last arg with no value after it', () => {
|
|
34
|
+
const argv = ['node', 'server.js', '--env-path'];
|
|
35
|
+
assert.equal(resolveEnvPathFromArgv(argv), undefined);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('resolveEnvPathFromArgv is unaffected by other, unrelated flags', () => {
|
|
39
|
+
const argv = ['node', 'server.js', '--some-other-flag', '--env-path', './x/.env'];
|
|
40
|
+
assert.equal(resolveEnvPathFromArgv(argv), path.resolve('./x/.env'));
|
|
41
|
+
});
|
package/OPEN_SOURCE_ROADMAP.md
DELETED
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
# Open-Source Release Roadmap — 20 Steps
|
|
2
|
-
|
|
3
|
-
**What this is:** the concrete execution plan for `ROADMAP.md`'s Phase 5
|
|
4
|
-
("open-source release"). That phase has existed as a single line —
|
|
5
|
-
"Public GitHub repo → npm package → Docker image → announce" — since
|
|
6
|
-
2026-08-23; this is what actually has to happen to make each of those
|
|
7
|
-
four words true, broken into 20 numbered steps with the sub-work under
|
|
8
|
-
each, so progress is trackable the same way the embedded build itself
|
|
9
|
-
was.
|
|
10
|
-
|
|
11
|
-
**Scope, precisely** (see `ROADMAP.md` section 6 and the 2026-08-29
|
|
12
|
-
open-source-scope discussion in the Decision Journal): **only the
|
|
13
|
-
embedded engine gets open-sourced** — routing, caching, metrics,
|
|
14
|
-
providers, the dashboard. Login, payment, multi-tenancy, and anything
|
|
15
|
-
else that makes the **standalone** hosted product ($ Phase 6) are never
|
|
16
|
-
part of this repo, this package, or this Docker image. If a step below
|
|
17
|
-
ever seems to call for adding one of those things, that's a sign the
|
|
18
|
-
step has drifted out of scope, not a sign the scope needs expanding.
|
|
19
|
-
|
|
20
|
-
**Status legend:** ⬜ not started · 🟨 in progress · ✅ done · ⏸ blocked/waiting
|
|
21
|
-
|
|
22
|
-
**Lesson applied from the embedded build's own history (see
|
|
23
|
-
`ROADMAP.md`'s "Note on an older numbering"):** that 20-step list was
|
|
24
|
-
never written down as one document and step 18's content couldn't be
|
|
25
|
-
reconstructed months later. This one is written down as one document,
|
|
26
|
-
here, on purpose — update status inline as steps close, don't let this
|
|
27
|
-
become a second undocumented list.
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## Group A — Decide before building anything (steps 1-5)
|
|
32
|
-
|
|
33
|
-
### 1. ✅ Decide the extraction method — **decided 2026-08-29: fresh, curated history**
|
|
34
|
-
- **Checked directly** (`git log --format="%an <%ae>" --all -- 210_apps/001_model_router/`):
|
|
35
|
-
the real commit history contains a real person's full name and
|
|
36
|
-
personal Gmail address in the author field, across 12+ commits — not
|
|
37
|
-
a hypothetical risk, a confirmed one. That alone settles the
|
|
38
|
-
decision: **do not preserve the original git history** when
|
|
39
|
-
extracting. A `git subtree split`/`filter-repo` approach would carry
|
|
40
|
-
that identity into a permanently public, unrescindable record the
|
|
41
|
-
moment the repo goes live.
|
|
42
|
-
- No leaked API keys or other secrets found in the history's actual
|
|
43
|
-
diffs (checked separately — see step 3) — the ONE real exposure is
|
|
44
|
-
the author metadata itself, which a fresh/squashed history sidesteps
|
|
45
|
-
entirely along with everything else.
|
|
46
|
-
- **Decision: fresh, curated history** — squash to a clean set of
|
|
47
|
-
milestone commits authored under a project/org identity, not
|
|
48
|
-
personal ones, when step 16 actually pushes this public.
|
|
49
|
-
|
|
50
|
-
### 2. ✅ Pick a license — **decided 2026-08-29: MIT**
|
|
51
|
-
- **Decision: MIT**, not a BSL-style source-available license, for this
|
|
52
|
-
first release. Reasoning, weighed against the alternative:
|
|
53
|
-
- Phase 5's whole stated purpose is trust/adoption ("top-of-funnel"
|
|
54
|
-
per section 3's own monetization table) - the two launch
|
|
55
|
-
communities already planned for (Hacker News, r/selfhosted) react
|
|
56
|
-
badly to source-available licenses on a first release from an
|
|
57
|
-
unknown project. BSL here risks damaging the exact thing Phase 5
|
|
58
|
-
exists to build.
|
|
59
|
-
- The competing-hosted-clone risk BSL would guard against is
|
|
60
|
-
theoretical today, not real: it requires someone to find this
|
|
61
|
-
project among much bigger established names (LiteLLM, Portkey,
|
|
62
|
-
OpenRouter), choose to build a hosted competitor on it instead of
|
|
63
|
-
those, and out-compete our own hosted tier on the same code with
|
|
64
|
-
none of our head start. Low-probability chain to defend against
|
|
65
|
-
before there's any traction worth defending.
|
|
66
|
-
- Real precedent (Elastic, MongoDB, Sentry) all started fully
|
|
67
|
-
permissive and only moved to a restrictive license YEARS later,
|
|
68
|
-
once a specific, material competitor was demonstrably siphoning
|
|
69
|
-
revenue - never before there was traction. Code already released
|
|
70
|
-
under MIT stays MIT forever, but nothing stops a FUTURE version
|
|
71
|
-
from adopting different terms later if a real competing clone
|
|
72
|
-
actually appears - so nothing is lost by not defending against a
|
|
73
|
-
threat that doesn't exist yet.
|
|
74
|
-
- MIT over Apache-2.0 specifically: LiteLLM, our closest named
|
|
75
|
-
competitor, already ships MIT - matching it lowers the evaluation
|
|
76
|
-
friction for the exact audience both projects are fishing from.
|
|
77
|
-
- Revisit ONLY if a real competing hosted clone materializes later -
|
|
78
|
-
that would apply to future versions/contributions, not retroactively.
|
|
79
|
-
- **Done:** `LICENSE` file added (MIT, "MemoCode" as copyright holder
|
|
80
|
-
as a placeholder - confirm the real legal entity name before step 15
|
|
81
|
-
actually makes the repo public).
|
|
82
|
-
|
|
83
|
-
### 3. ✅ Secrets and credential audit — **done 2026-08-29, clean**
|
|
84
|
-
- Grepped both the working tree AND the full commit history's diffs for
|
|
85
|
-
API key patterns (`sk-...`, `AIza...`, `xai-...`) and email addresses
|
|
86
|
-
— **no leaked keys found anywhere**, and the only email match was the
|
|
87
|
-
git author metadata already handled by step 1's decision, not
|
|
88
|
-
anything embedded in file content.
|
|
89
|
-
- `.env.example` confirmed to hold only placeholder values
|
|
90
|
-
(`sk-ant-api03-...` truncated, `your-random-internal-key`, etc.), no
|
|
91
|
-
real credentials.
|
|
92
|
-
- `MODEL_ROUTER_OWNER_EMAILS`-style references in the code/docs are the
|
|
93
|
-
env var NAME only, never a real value.
|
|
94
|
-
- Nothing to rotate. Re-run this same check once more right before
|
|
95
|
-
step 15 actually makes the repo public, as a final gate — a clean
|
|
96
|
-
result today doesn't exempt a later commit from the same scrutiny.
|
|
97
|
-
|
|
98
|
-
### 4. ✅ Strip MemoCode-specific coupling — **name decided 2026-08-29: cachegate**
|
|
99
|
-
- **Checked directly**: grepped all `.js` code (not docs) for
|
|
100
|
-
"memocode"/"MemoCode" — found only comments explaining context and
|
|
101
|
-
one branded startup log line (`🚀 MemoCode Model Router listening...`
|
|
102
|
-
in `server.js`), **zero functional coupling** — no hardcoded MemoCode
|
|
103
|
-
URLs, no assumption that only a MemoCode caller exists. The "drop-in
|
|
104
|
-
module" design claim holds up under inspection, not just assumption.
|
|
105
|
-
- **Naming — a real open decision, CEO's call (taste/brand, unlike the
|
|
106
|
-
license), not decided here.** Checked npm registry availability for
|
|
107
|
-
three neutral candidates: `cachegate` ✅ available, `routecache` ✅
|
|
108
|
-
available, `llm-routecache` ✅ available (`llm-relay` was tried first
|
|
109
|
-
and is already taken by an unrelated, similarly-scoped package).
|
|
110
|
-
- **Case for a neutral name** (not "MemoCode ___"): "MemoCode" is a
|
|
111
|
-
mnemonic/note-taking app name with no connection to "LLM gateway" —
|
|
112
|
-
a stranger evaluating self-hosted LLM routers on Hacker News or
|
|
113
|
-
r/selfhosted would find a "MemoCode Model Router" confusing (why
|
|
114
|
-
does a notes app also make infrastructure?), which works against
|
|
115
|
-
Phase 5's own adoption/trust goal. The eventual Phase 6 standalone
|
|
116
|
-
product will also want its own identity as an "OpenRouter/LiteLLM
|
|
117
|
-
competitor" (`ROADMAP.md` section 6's own framing) — easier to
|
|
118
|
-
establish that now than rename after people have already
|
|
119
|
-
starred/installed something.
|
|
120
|
-
- **Case for keeping MemoCode branding:** funnel value back to the
|
|
121
|
-
parent product; zero rename work.
|
|
122
|
-
- **Decided: `cachegate`.** Applied everywhere: `package.json`'s
|
|
123
|
-
`name`, `package-lock.json` regenerated, `README.md`'s title, and
|
|
124
|
-
the startup log line in `server.js` (was
|
|
125
|
-
`🚀 MemoCode Model Router listening...`, now `🚀 cachegate
|
|
126
|
-
listening...`). Confirmed clean afterward - grepped for any
|
|
127
|
-
remaining "memocode"/"MemoCode" reference in code or README, none
|
|
128
|
-
found. Full suite re-run after the rename: 96/96 passing.
|
|
129
|
-
|
|
130
|
-
### 5. ✅ Rewrite the README for a stranger, not a teammate — done 2026-08-29
|
|
131
|
-
- Existing `README.md` was already thorough and honest (features,
|
|
132
|
-
streaming caveats, dashboard limitations, all stated plainly) - most
|
|
133
|
-
of it needed no change. What was missing for an external reader:
|
|
134
|
-
- A "Why this instead of LiteLLM/Portkey/OpenRouter" section, pulling
|
|
135
|
-
in the honest positioning already drafted in `ROADMAP.md` section 2
|
|
136
|
-
(self-hosted-first, semantic cache with the LiteLLM caveat stated
|
|
137
|
-
plainly, Node.js-native, embeddable, the real 20-45%/47-90% savings
|
|
138
|
-
range instead of an inflated 86-95%).
|
|
139
|
-
- A "What this is NOT" section stating the scope boundary up front
|
|
140
|
-
(no hosted service, no login/billing/multi-tenancy, no 140-provider
|
|
141
|
-
reach, no vector-indexed semantic cache yet) - added, per this
|
|
142
|
-
doc's own step-14 principle, so nobody files an issue asking for
|
|
143
|
-
something never in scope.
|
|
144
|
-
- The quickstart assumed the MemoCode monorepo was already cloned
|
|
145
|
-
(`cd 210_apps/001_model_router` as step one) - now gives both a
|
|
146
|
-
standalone `git clone` path and an "embedded in your own app" path.
|
|
147
|
-
- One paragraph named MemoCode's own internal file
|
|
148
|
-
(`210_apps/000_backend/router-owner.mjs`) as the dashboard-gating
|
|
149
|
-
example - generalized to describe the PATTERN (front the dashboard
|
|
150
|
-
with your own app's login) without a path that won't exist in the
|
|
151
|
-
extracted repo.
|
|
152
|
-
- Title left as a placeholder pending step 4's naming decision.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## Group B — Make the code shippable outside this monorepo (steps 6-10)
|
|
157
|
-
|
|
158
|
-
### 6. 🟨 Package it as a real, standalone npm module — mostly done 2026-08-29
|
|
159
|
-
- `package.json`: `name` → `cachegate`, `description` rewritten to be
|
|
160
|
-
accurate standalone (no "for MemoCode" framing), `license: "MIT"`
|
|
161
|
-
added (matches step 2 - npm shows this on the package page without
|
|
162
|
-
needing to open `LICENSE`), `engines: {"node": ">=18.0.0"}` added.
|
|
163
|
-
Honest caveat: `>=18.0.0` is a reasonable floor given the
|
|
164
|
-
dependencies (Express 4, `redis` v4, `pg` v8 all support it), NOT a
|
|
165
|
-
claim this has been cross-version tested — only run against Node 22
|
|
166
|
-
in this sandbox.
|
|
167
|
-
- **`bin` entry added**: `"cachegate": "./server.js"`, plus a
|
|
168
|
-
`#!/usr/bin/env node` shebang line added to `server.js` (it had none)
|
|
169
|
-
and the file's execute bit set - both required for `npx cachegate` to
|
|
170
|
-
actually work once published (step 17). README's "Run it" section now
|
|
171
|
-
documents this as the zero-clone path.
|
|
172
|
-
- **Deferred on purpose, not forgotten:** the `0.1.0` → `1.0.0` semver
|
|
173
|
-
bump. Bumping it now, before the repo is actually public (step 16),
|
|
174
|
-
would sit oddly in the internal monorepo and communicate a stability
|
|
175
|
-
claim nothing has tested yet. Bump this at step 16/17, right when
|
|
176
|
-
it's actually true.
|
|
177
|
-
- Dependencies unchanged and already appropriate for a standalone
|
|
178
|
-
package - nothing in `package.json` assumed shared monorepo tooling
|
|
179
|
-
to begin with, verified by a fresh `npm install` + full test run in
|
|
180
|
-
an isolated worktree: 96/96 passing.
|
|
181
|
-
|
|
182
|
-
### 7. ✅ Standard OSS repo hygiene — done 2026-08-29
|
|
183
|
-
- `CONTRIBUTING.md` — local setup, how to run tests (including the two
|
|
184
|
-
real test-suite conventions a contributor needs to know: no test
|
|
185
|
-
calls a live provider, and metrics-writing tests need to isolate
|
|
186
|
-
their own state), and the scope boundary repeated up front (a PR
|
|
187
|
-
adding login/billing/multi-tenancy gets closed regardless of
|
|
188
|
-
quality — that's a separate product, not this engine).
|
|
189
|
-
- `CODE_OF_CONDUCT.md` — Contributor Covenant v2.1, adopted verbatim
|
|
190
|
-
(that's its actual intended use, not something to rewrite).
|
|
191
|
-
- `SECURITY.md` — points to GitHub's private vulnerability reporting
|
|
192
|
-
(Security tab → Report a vulnerability) rather than a fabricated
|
|
193
|
-
contact email; states concretely what counts as a security issue
|
|
194
|
-
for THIS project specifically (auth bypass, cross-deployment data
|
|
195
|
-
leak, a resource-exhaustion path rate limiting doesn't cover) versus
|
|
196
|
-
a regular bug. **Needs "Private vulnerability reporting" turned on**
|
|
197
|
-
in the repo's Settings once step 15 creates it — flagged inline in
|
|
198
|
-
the file itself as a maintainer note to remove once live.
|
|
199
|
-
- `.github/ISSUE_TEMPLATE/bug_report.md` + `feature_request.md` (the
|
|
200
|
-
feature template repeats the scope boundary up front too) +
|
|
201
|
-
`config.yml` (points to Security Advisories instead of a public
|
|
202
|
-
issue for vulnerabilities — has a placeholder `OWNER` in its URL,
|
|
203
|
-
flagged inline, to fill in at step 15).
|
|
204
|
-
- `.github/PULL_REQUEST_TEMPLATE.md` — a scope-boundary checkbox
|
|
205
|
-
reviewers can actually check against, plus what testing was done.
|
|
206
|
-
|
|
207
|
-
### 8. ✅ CI for the public repo — done 2026-08-29
|
|
208
|
-
- `.github/workflows/test.yml` added: runs on every push and PR
|
|
209
|
-
(deliberately NOT copying this monorepo's own root
|
|
210
|
-
`.github/workflows/e2e.yml`, which is `workflow_dispatch`-only — a
|
|
211
|
-
public OSS repo needs the automatic trigger, that's the whole point
|
|
212
|
-
of the trust signal this step exists for).
|
|
213
|
-
- **Real finding, not assumed:** `npm test` needs more than just Node.
|
|
214
|
-
`semanticCache.test.js` spawns its own throwaway `redis-server`
|
|
215
|
-
process directly (the binary isn't on `ubuntu-latest` by default —
|
|
216
|
-
added an install step); `metrics-postgres.test.js` needs a real
|
|
217
|
-
reachable Postgres. The workflow adds a Postgres service container
|
|
218
|
-
matching that test file's own default connection string exactly.
|
|
219
|
-
- **Second real finding, caught by actually stopping Postgres and
|
|
220
|
-
re-running the suite rather than trusting the code's own comment**:
|
|
221
|
-
`metrics-postgres.test.js`'s header comment claimed it "fails loudly
|
|
222
|
-
with ECONNREFUSED... rather than silently skipping" — checked
|
|
223
|
-
directly, and that's not what the code does. It probes connectivity
|
|
224
|
-
once and skips gracefully with a warning if Postgres isn't reachable,
|
|
225
|
-
and **every test still reports as passing either way** (96/96,
|
|
226
|
-
whether or not Postgres was running). Fixed the comment to describe
|
|
227
|
-
the real behavior, and noted the real consequence: without this
|
|
228
|
-
workflow's Postgres service, CI would report green while silently
|
|
229
|
-
never exercising that file at all — the service container isn't a
|
|
230
|
-
nice-to-have here, it's what makes the badge mean what it's supposed
|
|
231
|
-
to mean.
|
|
232
|
-
- Matrix across Node `18.x`/`20.x`/`22.x` — `package.json` declares
|
|
233
|
-
`engines: {"node": ">=18.0.0"}`; running against all three actually
|
|
234
|
-
backs that claim instead of leaving it asserted but untested.
|
|
235
|
-
- **Verified for real, not just written**: ran the actual suite locally
|
|
236
|
-
with both a live local Postgres (started, password/db created to
|
|
237
|
-
match the test's default string) and the pre-installed `redis-server`
|
|
238
|
-
binary present — 96/96 passing, ~11s (vs. ~1.3s when Postgres is
|
|
239
|
-
down and that file's tests silently skip) — confirms the difference
|
|
240
|
-
is real exercise, not a no-op.
|
|
241
|
-
- Added a CI-status badge to `README.md` (next to the license badge) —
|
|
242
|
-
has the same `OWNER` placeholder as `.github/ISSUE_TEMPLATE/config.yml`,
|
|
243
|
-
flagged inline, to fill in at step 15.
|
|
244
|
-
|
|
245
|
-
### 9. 🟨 Docker image — improved and documented; build NOT verified here, real limitation
|
|
246
|
-
- **Honest limitation, not glossed over**: this sandbox's network
|
|
247
|
-
egress policy blocks Docker Hub entirely (confirmed - `docker build`
|
|
248
|
-
fails pulling `node:20-slim` with a 403 policy denial from
|
|
249
|
-
`production.cloudfront.docker.com`, and the proxy status explicitly
|
|
250
|
-
lists it as a policy denial, not a transient failure worth retrying).
|
|
251
|
-
**The actual `docker build` + `docker run` + hit-`/health` verification
|
|
252
|
-
this step calls for could NOT be done from here** - that needs
|
|
253
|
-
whoever has real internet access (the CEO's local machine / DeepSeek's
|
|
254
|
-
lane) to run it for real before step 16 ships. Said plainly rather
|
|
255
|
-
than assumed to be fine.
|
|
256
|
-
- What WAS done, based on direct static review of the existing
|
|
257
|
-
`Dockerfile` (not a guess):
|
|
258
|
-
- **Added `.dockerignore`** - none existed. Without it, `COPY . .`
|
|
259
|
-
would copy a HOST-machine `node_modules` (if one happens to exist
|
|
260
|
-
at build time) straight over the image's own freshly-`npm ci`'d
|
|
261
|
-
one - a classic, easy-to-hit Docker footgun, especially likely
|
|
262
|
-
once this is a real repo other contributors build locally. Also
|
|
263
|
-
excludes `.env`, `.git`, `test/`, `.github/` - none of it belongs
|
|
264
|
-
in a runtime image.
|
|
265
|
-
- **Added a non-root `USER node`** - the official Node image already
|
|
266
|
-
ships this user (uid 1000); the Dockerfile just never used it,
|
|
267
|
-
running as root by default. Standard hardening for a public image.
|
|
268
|
-
- **Added a real `HEALTHCHECK`** - calls the existing `GET /health`
|
|
269
|
-
endpoint via Node's own `http` module (the slim base image has no
|
|
270
|
-
curl/wget), so `docker ps` actually reports `healthy`/`unhealthy`
|
|
271
|
-
instead of only "running."
|
|
272
|
-
- **Publish target decided: GitHub Container Registry (GHCR)**, not
|
|
273
|
-
Docker Hub - ties to the same repo/`GITHUB_TOKEN` already used for
|
|
274
|
-
CI with no separate account or credential to manage, and the image
|
|
275
|
-
inherits the repo's own visibility settings. Docker Hub has better
|
|
276
|
-
discoverability for someone specifically browsing Docker Hub's own
|
|
277
|
-
search, but the realistic audience here is someone already reading
|
|
278
|
-
this GitHub repo's README, not browsing Docker Hub cold - GHCR fits
|
|
279
|
-
that path better. Not closed off permanently: worth adding a Docker
|
|
280
|
-
Hub mirror later if search-driven discovery turns out to matter.
|
|
281
|
-
- README's "Run it" section gained a Docker subsection: `docker build`
|
|
282
|
-
+ `docker run` with `--env-file`, a note on what `HEALTHCHECK`
|
|
283
|
-
reports, and an explicit statement that Redis is NOT bundled in the
|
|
284
|
-
image (point `REDIS_URL` at an external instance; missing it disables
|
|
285
|
-
caching cleanly rather than failing to start).
|
|
286
|
-
- **Remaining, real, not done here:** the actual build-and-run
|
|
287
|
-
verification. Flagging this explicitly as an open item for whoever
|
|
288
|
-
picks this up next with real network access - do not treat this
|
|
289
|
-
step as fully closed until that verification actually happens.
|
|
290
|
-
|
|
291
|
-
### 10. ✅ Config and environment documentation — done 2026-08-29
|
|
292
|
-
- **Checked directly, not assumed**: grepped every `.js` file for
|
|
293
|
-
`process.env.` to build the actual, complete list of env vars the
|
|
294
|
-
code reads, then diffed it against `.env.example`.
|
|
295
|
-
- **Real finding**: `.env.example` (and the README's own `.env`
|
|
296
|
-
snippet) documented `ANTHROPIC_MODEL=claude-sonnet-4-5-20250929` as a
|
|
297
|
-
config value - **grepped for it and it's read NOWHERE in the code**.
|
|
298
|
-
The model is named per-request in the API call's own `model` field,
|
|
299
|
-
not configured via env at all. A new user setting this would see it
|
|
300
|
-
silently do nothing. Removed from both files, replaced with an
|
|
301
|
-
explicit note that the model is per-request, not env-configured.
|
|
302
|
-
- **Missing from `.env.example` entirely, now added**:
|
|
303
|
-
`ALLOW_INSECURE_LOCAL_DEV`, `EMBEDDING_MODEL` (which OpenAI embedding
|
|
304
|
-
model the semantic cache uses), `METRICS_LOG_PATH` (JSONL storage
|
|
305
|
-
location), `MEMOCODE_ROUTER_DATABASE_URL` (an alternate to
|
|
306
|
-
`DATABASE_URL` that takes priority when set - useful when embedding
|
|
307
|
-
this inside an app that already has its own `DATABASE_URL`), and
|
|
308
|
-
`ROUTER_TIERS_JSON` (override the default routing tiers).
|
|
309
|
-
- **Genuinely tested the quickstart, not just written it**: ran
|
|
310
|
-
`npm install`, copied `.env.example` to `.env`, filled in a test
|
|
311
|
-
internal key and a fake Anthropic key, ran `npm start` - boots clean,
|
|
312
|
-
logs "🚀 cachegate listening on port 4000." Hit `GET /health` (real
|
|
313
|
-
response, correctly reflecting config) and `POST
|
|
314
|
-
/v1/chat/completions` (reached Anthropic for real, got back a genuine
|
|
315
|
-
401 "API key is invalid" - proving the full pipeline works end to
|
|
316
|
-
end; only the placeholder key is fake, exactly what a real follower
|
|
317
|
-
would see before adding their own). Also confirmed `.env` and the
|
|
318
|
-
metrics `data/` directory stay gitignored, as claimed.
|
|
319
|
-
- README's `.env` snippet now points to `.env.example` for the full
|
|
320
|
-
option list instead of duplicating a partial, now-corrected copy of it.
|
|
321
|
-
|
|
322
|
-
---
|
|
323
|
-
|
|
324
|
-
## Group C — Keep embedded and public in sync, and safe (steps 11-14)
|
|
325
|
-
|
|
326
|
-
### 11. ✅ Decide the concrete sync mechanism — **decided 2026-08-29**
|
|
327
|
-
- `ROADMAP.md` section 6 states the *principle* ("wrap it, don't fork
|
|
328
|
-
it - same core logic, two thin deployment shells") but never names
|
|
329
|
-
an actual mechanism. Two directions were on the table:
|
|
330
|
-
- (a) the public repo becomes the source of truth, MemoCode vendors
|
|
331
|
-
it in via a script/subtree pull.
|
|
332
|
-
- (b) this monorepo directory stays the source of truth, a script
|
|
333
|
-
pushes/mirrors it out to the public repo on release.
|
|
334
|
-
- **Decided: (b) — this monorepo directory stays the source of truth.**
|
|
335
|
-
Reasoning:
|
|
336
|
-
- It matches reality, not a fresh ideal: every real day of
|
|
337
|
-
development on this router - the original 20-step build, the
|
|
338
|
-
failover feature, all ten OSS-prep steps so far - happened inside
|
|
339
|
-
this monorepo, through its own task-branch/PR/gate workflow
|
|
340
|
-
(`AGENTS.md`). Moving day-to-day development to the public repo
|
|
341
|
-
would mean either running two parallel workflows or abandoning the
|
|
342
|
-
one that's actually proven itself this week - neither is worth it
|
|
343
|
-
to satisfy a "public repo is canonical" ideal nobody needs yet.
|
|
344
|
-
- Option (a) would put every internal-only concern (this team's own
|
|
345
|
-
coordination-board conventions, anything MemoCode-embedding-
|
|
346
|
-
specific) through public PR review before it could land internally
|
|
347
|
-
- backwards for a team that needs to move fast on its own repo.
|
|
348
|
-
- This is also the well-established pattern for exactly this
|
|
349
|
-
situation, not a novel one: several real companies develop OSS
|
|
350
|
-
projects inside a private monorepo and mirror them out
|
|
351
|
-
(contributions flow back in via manual review + reapplication, not
|
|
352
|
-
automatic two-way merge) rather than developing directly in the
|
|
353
|
-
public repo.
|
|
354
|
-
- **The real gap this creates, named rather than ignored:** once
|
|
355
|
-
external contributors exist (post step 19's launch), their PRs land
|
|
356
|
-
on the PUBLIC repo first - there is no automatic path back into this
|
|
357
|
-
monorepo. Concrete resolution: an accepted external PR gets manually
|
|
358
|
-
reapplied to `210_apps/001_model_router/` as its own normal task
|
|
359
|
-
branch here (same `AGENTS.md` workflow as any other change - it goes
|
|
360
|
-
through this project's own gate even though it originated externally,
|
|
361
|
-
on purpose, not as an oversight: an external diff doesn't get to skip
|
|
362
|
-
this codebase's own verification standard just because GitHub already
|
|
363
|
-
approved it). This creates a real but bounded divergence window
|
|
364
|
-
between the two repos - acceptable, disclosed, and the discipline is
|
|
365
|
-
keeping that window short (reapply promptly), not eliminating it.
|
|
366
|
-
- **The actual repeatable command** (implemented for real in step 12,
|
|
367
|
-
sketched here so the decision isn't just prose): a single script,
|
|
368
|
-
`sync-oss-release.sh`, invoked as `./sync-oss-release.sh <path-to-public-repo-checkout>`,
|
|
369
|
-
that does, in order: (1) copy this directory's tracked files into the
|
|
370
|
-
target checkout, excluding anything `.gitignore`d; (2) run the same
|
|
371
|
-
secrets grep step 3 already established, failing loudly rather than
|
|
372
|
-
publishing on a hit; (3) bump `package.json`'s version per step 6's
|
|
373
|
-
deferred semver plan; (4) commit and leave the push to a human/CI
|
|
374
|
-
step, never auto-pushed. One command, not a remembered sequence of
|
|
375
|
-
manual copy-paste steps.
|
|
376
|
-
|
|
377
|
-
### 12. ✅ Build the actual sync script/workflow — done and genuinely tested 2026-08-29
|
|
378
|
-
- `sync-oss-release.sh` built per step 11's decision: mirrors this
|
|
379
|
-
directory's git-tracked files into a target checkout (removing
|
|
380
|
-
everything else there first, except its own `.git/`), scans for
|
|
381
|
-
secrets before touching anything, optionally bumps `package.json`'s
|
|
382
|
-
version (`--version X.Y.Z`), and commits in the TARGET repo without
|
|
383
|
-
pushing. Refuses to run against a path that isn't a git repository,
|
|
384
|
-
specifically so pointing it at the wrong path can't wipe something
|
|
385
|
-
unrelated.
|
|
386
|
-
- **Actually tested end-to-end, not just written** - a real target git
|
|
387
|
-
repo was created in scratch space and run through every real
|
|
388
|
-
scenario: a clean sync with `--version 0.9.0` (files copied
|
|
389
|
-
correctly, version bumped, a stale target-only file correctly
|
|
390
|
-
removed, real commit created); a resync with no version flag
|
|
391
|
-
(correctly left the version as whatever's currently in this
|
|
392
|
-
directory); an identical third run (correctly reported "nothing
|
|
393
|
-
changed," no empty commit); the non-git-directory safety guard
|
|
394
|
-
(correctly refused); and the secrets-scan abort path (a real fake
|
|
395
|
-
key was planted in `README.md` and the script was run against it).
|
|
396
|
-
- **Real bug found by that last test, not assumed to work**: the
|
|
397
|
-
secrets-scan regex FAILED to catch the planted key on the first
|
|
398
|
-
attempt. Root cause: the character class `[a-zA-Z0-9]{20,}` used
|
|
399
|
-
after `sk-`/`xai-` doesn't allow hyphens - and a real Anthropic key
|
|
400
|
-
looks like `sk-ant-api03-<random>`, where the hyphens immediately
|
|
401
|
-
after `sk-` broke the match after only 3 characters. Fixed to
|
|
402
|
-
`[a-zA-Z0-9_-]{20,}` (matching `AIza`'s pattern, which already had
|
|
403
|
-
this right), re-tested, and confirmed it now correctly aborts.
|
|
404
|
-
- **This is the exact same pattern used in steps 1, 3, and 8's manual
|
|
405
|
-
audits** - meaning those "clean" results were reached with the same
|
|
406
|
-
blind spot. Re-ran the full audit (working tree + entire commit
|
|
407
|
-
history) with the CORRECTED pattern before concluding anything:
|
|
408
|
-
still genuinely clean. The earlier conclusion holds, but only because
|
|
409
|
-
it was actually re-verified just now, not assumed to still be valid
|
|
410
|
-
once the flaw in the method was found.
|
|
411
|
-
- No separate "checklist for what needs re-verifying after a sync" was
|
|
412
|
-
needed beyond what the script itself already does (secrets scan is
|
|
413
|
-
automatic on every run) - a maintainer still reviews the target
|
|
414
|
-
repo's diff before pushing (the script's own final message says so),
|
|
415
|
-
which covers README drift and anything else worth a human glance.
|
|
416
|
-
|
|
417
|
-
### 13. ✅ Security review pass, specifically for "now public" risk — done 2026-08-29
|
|
418
|
-
- **The one real, serious finding, confirmed live not assumed**: sent
|
|
419
|
-
an actual oversized POST body during this review and got back a raw
|
|
420
|
-
HTML page containing a FULL STACK TRACE with this server's own
|
|
421
|
-
absolute filesystem paths - Express's own default error handler,
|
|
422
|
-
reached because nothing here ever caught an error raised before a
|
|
423
|
-
route's own try/catch (the concrete trigger: `express.json()`
|
|
424
|
-
rejecting a too-large body). Fixed with a catch-all JSON error
|
|
425
|
-
handler registered last, re-tested with the identical request:
|
|
426
|
-
`{"error":"Request body too large."}`, 413, no stack trace. This is
|
|
427
|
-
exactly the class of thing "worked fine as an internal tool nobody
|
|
428
|
-
attacked" and would not have survived being public.
|
|
429
|
-
- **Second real finding**: `express.json({ limit: '50mb' })` was
|
|
430
|
-
applied GLOBALLY and BEFORE the `/v1` auth check - an anonymous
|
|
431
|
-
caller could force up to 50MB of JSON parsing per request before
|
|
432
|
-
ever being rejected with 401. Fixed three ways: scoped to `/v1` only
|
|
433
|
-
(the sole route that reads a body - checked directly, every other
|
|
434
|
-
route is a body-less GET); moved after `requireInternalKey` and the
|
|
435
|
-
rate limiter in the middleware chain, so a request is rejected by a
|
|
436
|
-
cheap check before any parsing happens; limit dropped from 50mb to a
|
|
437
|
-
configurable 2mb default (`JSON_BODY_LIMIT`) - checked that this
|
|
438
|
-
router has no image/multimodal support, so even a very long text
|
|
439
|
-
conversation fits comfortably under that. Live-verified: an
|
|
440
|
-
unauthenticated 3MB POST now gets rejected in 16ms with a 401,
|
|
441
|
-
vs. previously being fully parsed first.
|
|
442
|
-
- **Third finding, lower severity**: `/stats` and `/dashboard/data`
|
|
443
|
-
had `requireInternalKey` but no rate limit at all, unlike `/v1`.
|
|
444
|
-
Added a separate, more generous `readEndpointLimiter`
|
|
445
|
-
(`READ_RATE_LIMIT_MAX`, default 120/window) - lower stakes than `/v1`
|
|
446
|
-
(no provider spend on the line) but still real server work a
|
|
447
|
-
leaked/shared key shouldn't be able to hammer unbounded.
|
|
448
|
-
- Free hardening added while in here: `app.disable('x-powered-by')` -
|
|
449
|
-
no reason to hand a public-facing service's framework fingerprint to
|
|
450
|
-
every caller.
|
|
451
|
-
- **Confirmed clean, no fix needed** (checked directly, not assumed):
|
|
452
|
-
no `.stack` or raw error object is ever sent to a client anywhere in
|
|
453
|
-
the existing route handlers - every error path already used
|
|
454
|
-
`err.message` only. `ALLOW_INSECURE_LOCAL_DEV` already refuses to
|
|
455
|
-
start without either a real key or this explicit opt-in, AND already
|
|
456
|
-
prints a loud runtime `console.warn` when active - no doc-only
|
|
457
|
-
warning to strengthen, the code itself already enforces it. No CORS
|
|
458
|
-
middleware exists, which is the CORRECT default here (an admin/data
|
|
459
|
-
API with bearer-token auth has no reason to allow arbitrary
|
|
460
|
-
cross-origin browser access) - not a gap to fix.
|
|
461
|
-
- Also clarified in `server.js`'s own rate-limiter comment: the
|
|
462
|
-
"shared ceiling across all callers" caveat is specific to a
|
|
463
|
-
single-caller EMBEDDED deployment - a standalone self-hoster with
|
|
464
|
-
distinct per-caller IPs gets real per-caller separation from the
|
|
465
|
-
same default, not the same limitation.
|
|
466
|
-
- All fixes verified live (server started, real HTTP requests sent and
|
|
467
|
-
checked) in addition to the automated suite - 96/96 passing
|
|
468
|
-
throughout, confirming none of this changed any existing behavior
|
|
469
|
-
other than the three things it was meant to fix.
|
|
470
|
-
|
|
471
|
-
### 14. ✅ Explicit non-goals, written down where a stranger will read them — done 2026-08-29
|
|
472
|
-
- Most of the literal content already existed from step 5 (the README's
|
|
473
|
-
"What this is NOT" section) and step 7 (`CONTRIBUTING.md`,
|
|
474
|
-
`.github/ISSUE_TEMPLATE/feature_request.md`) - checked all three
|
|
475
|
-
directly rather than assuming step 14 was redundant with them.
|
|
476
|
-
- **Real gap found in two of the three, not assumed fine**: both the
|
|
477
|
-
README's closing line and `CONTRIBUTING.md`'s scope section said, in
|
|
478
|
-
effect, "if you want a hosted service, fork it" - which actively
|
|
479
|
-
*invites* the exact outcome this section (and step 2's license
|
|
480
|
-
reasoning) exists to discourage. Technically true under MIT, but
|
|
481
|
-
saying it right next to "this is not a hosted service" undermines
|
|
482
|
-
the whole point of drawing the boundary. Fixed both: still honest
|
|
483
|
-
that MIT permits it (never claimed otherwise), but reframed as "not
|
|
484
|
-
a licensing restriction, a project-scope one" - this repo specifically
|
|
485
|
-
isn't going to grow into a hosted competitor to its own paid product,
|
|
486
|
-
full stop, rather than a soft invitation to go build one.
|
|
487
|
-
`.github/ISSUE_TEMPLATE/feature_request.md` already had this right -
|
|
488
|
-
checked, no change needed there.
|
|
489
|
-
- README's closing line also now explicitly separates the two real,
|
|
490
|
-
legitimate PR-worthy gaps (140-provider reach, vector-indexed
|
|
491
|
-
semantic cache) from the one boundary that isn't a gap at all,
|
|
492
|
-
pointing to `CONTRIBUTING.md` before someone opens a PR for it.
|
|
493
|
-
|
|
494
|
-
---
|
|
495
|
-
|
|
496
|
-
## Group D — Ship it (steps 15-18)
|
|
497
|
-
|
|
498
|
-
### 15. ⬜ Create the real public GitHub repository
|
|
499
|
-
- Public repo under whichever account/org is decided, real description,
|
|
500
|
-
topics/tags for discoverability (`llm-gateway`, `semantic-cache`,
|
|
501
|
-
`self-hosted`, `openai-compatible`, `llm-proxy`).
|
|
502
|
-
- Branch protection on the default branch (require CI green + review
|
|
503
|
-
before merge - the repo's own credibility depends on this from day
|
|
504
|
-
one, unlike an internal task branch nobody outside the team sees).
|
|
505
|
-
|
|
506
|
-
### 16. ⬜ Push the extracted, cleaned codebase
|
|
507
|
-
- First real push/release using whatever step 1 decided (curated
|
|
508
|
-
history or fresh start).
|
|
509
|
-
- Tag it `v1.0.0` (or whatever step 6 decided) - this is the actual
|
|
510
|
-
"it's out" moment everything before this step has been preparing for.
|
|
511
|
-
|
|
512
|
-
### 17. ⬜ Publish the npm package
|
|
513
|
-
- `npm publish` under the name step 4 chose, confirming it installs
|
|
514
|
-
cleanly in a throwaway empty directory (not just "works from this
|
|
515
|
-
monorepo").
|
|
516
|
-
|
|
517
|
-
### 18. ⬜ Publish the Docker image
|
|
518
|
-
- Push to the registry step 9 chose, confirm `docker pull` + `docker
|
|
519
|
-
run` works cold, from a machine that has never seen this project's
|
|
520
|
-
source at all.
|
|
521
|
-
|
|
522
|
-
---
|
|
523
|
-
|
|
524
|
-
## Group E — Tell people, then keep it alive (steps 19-20)
|
|
525
|
-
|
|
526
|
-
### 19. ⬜ Announce
|
|
527
|
-
- Per `ROADMAP.md`'s existing plan: Hacker News (Show HN post),
|
|
528
|
-
r/LocalLLaMA, r/selfhosted, Dev.to - each gets its own framing (Show
|
|
529
|
-
HN wants the honest savings number and the self-hosted angle;
|
|
530
|
-
r/selfhosted wants the Docker one-liner front and center; Dev.to
|
|
531
|
-
supports a longer "why we built this" writeup).
|
|
532
|
-
- Write the honest version of "why this exists" - the real story (an
|
|
533
|
-
internal tool for MemoCode that turned out to be worth its own
|
|
534
|
-
release), not a manufactured origin story.
|
|
535
|
-
|
|
536
|
-
### 20. ⬜ Post-launch triage plan
|
|
537
|
-
- Decide who watches the new public repo's issues/PRs day-to-day (the
|
|
538
|
-
same "one agent holds the standing watch" model already used for
|
|
539
|
-
this team's own PRs, per `AGENTS.md`) - a public repo with no
|
|
540
|
-
visible maintainer activity in the first weeks reads as abandoned.
|
|
541
|
-
- Define what "Phase 5 done" actually means so it doesn't stay
|
|
542
|
-
open-ended forever: e.g., shipped + stable for some real window with
|
|
543
|
-
no critical issues, or a first external contribution merged -
|
|
544
|
-
pick a concrete bar, not a vibe.
|
|
545
|
-
- This is also the moment Phase 6 (the standalone/hosted, NOT
|
|
546
|
-
open-sourced) becomes buildable for real - it depends on this
|
|
547
|
-
engine being proven stable under outside use, not just internal
|
|
548
|
-
dogfooding.
|
|
549
|
-
|
|
550
|
-
---
|
|
551
|
-
|
|
552
|
-
*Written 2026-08-29. Cross-referenced from `ROADMAP.md`'s Phase 5. Update
|
|
553
|
-
status markers inline as each step closes - this document IS the tracker,
|
|
554
|
-
not a one-time plan to be superseded by a better one later.*
|