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.
@@ -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
+ });
@@ -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.*