cachegate 1.0.0 → 1.1.0
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/OPEN_SOURCE_ROADMAP.md +855 -554
- 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
CHANGED
|
@@ -1,554 +1,855 @@
|
|
|
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.
|
|
499
|
-
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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 — live 2026-08-29: https://github.com/iDebunk/cachegate
|
|
499
|
+
- **Owner decided: `iDebunk`** (the same GitHub account already used for
|
|
500
|
+
this monorepo's own repos - confirmed via `get_me` that it's a user
|
|
501
|
+
account, not a separate org, so "existing org" and "personal account"
|
|
502
|
+
were never actually two different choices here). Reasoning: matches
|
|
503
|
+
the common real-world pattern of hosting OSS directly under a
|
|
504
|
+
company's own account; a personal-account or brand-new-org
|
|
505
|
+
alternative would either tie a strategic asset to one individual or
|
|
506
|
+
invent a naming decision with no present need - and a repo can be
|
|
507
|
+
transferred to a different owner later at zero cost if that ever
|
|
508
|
+
changes, so this wasn't a one-way door.
|
|
509
|
+
- **Real limitation hit while executing this**: this session's own
|
|
510
|
+
`mcp__github__create_repository` call failed with a 403 ("Resource
|
|
511
|
+
not accessible by integration") - the connected GitHub App lacks
|
|
512
|
+
repository-creation scope, confirmed not a transient error. The CEO
|
|
513
|
+
created the repo manually via github.com/new instead (public, no
|
|
514
|
+
auto-init) - noted here so a future session doesn't waste a retry
|
|
515
|
+
loop on the same wall.
|
|
516
|
+
- **Real finding on branch protection's status-check picker**: the CI
|
|
517
|
+
workflow's `strategy: matrix` (Node 18.x/20.x/22.x, added in step 8)
|
|
518
|
+
means GitHub creates THREE separate status checks - `test (18.x)`,
|
|
519
|
+
`test (20.x)`, `test (22.x)` - not one called "Tests" as the
|
|
520
|
+
workflow's own top-level `name:` field might suggest. Searching
|
|
521
|
+
"Tests" in the branch-protection UI finds nothing; searching "test"
|
|
522
|
+
(the job id) finds all three. All three were required, matching the
|
|
523
|
+
actual reason the matrix exists (a single required check would let
|
|
524
|
+
the other two silently break without ever blocking a merge).
|
|
525
|
+
- **Also decided**: "Require branches to be up to date before merging"
|
|
526
|
+
left OFF for now - real value only once there's enough concurrent PR
|
|
527
|
+
traffic for it to matter, and it's a single checkbox to enable later
|
|
528
|
+
when that's true. "Require approvals: 1" kept ON, with admin bypass
|
|
529
|
+
intact (default), so external PRs need real review without locking
|
|
530
|
+
the maintainer out of merging their own work.
|
|
531
|
+
- Topics added via the repo's "About" gear (only appears once the repo
|
|
532
|
+
has content - see step 16): `llm-gateway`, `semantic-cache`,
|
|
533
|
+
`self-hosted`, `openai-compatible`, `llm-proxy`.
|
|
534
|
+
|
|
535
|
+
### 16. ✅ Push the extracted, cleaned codebase — done 2026-08-29
|
|
536
|
+
- Pushed from the CEO's own local machine using `sync-oss-release.sh`
|
|
537
|
+
(this session's own repo-scope restrictions block direct git push
|
|
538
|
+
from here too, same category of limitation as step 15's repo
|
|
539
|
+
creation) - the script itself worked exactly as tested: clean
|
|
540
|
+
secrets scan, version bumped to `1.0.0` (the real first-release
|
|
541
|
+
moment step 6's deferred plan was waiting for), 40 files mirrored,
|
|
542
|
+
one real commit.
|
|
543
|
+
- **Two real, Windows-specific findings, worth keeping for next time**:
|
|
544
|
+
(1) the script's shebang/`set -euo pipefail` line broke with
|
|
545
|
+
`$'\r': command not found` - Git on Windows had checked the file out
|
|
546
|
+
with CRLF line endings, which no bash interpreter handles in a
|
|
547
|
+
script. Fixed locally with `sed -i 's/\r$//' sync-oss-release.sh`
|
|
548
|
+
before running; added `.gitattributes` (forcing LF on `*.sh` and text
|
|
549
|
+
files generally) to this directory so a future Windows checkout gets
|
|
550
|
+
LF from git itself rather than needing the same manual `sed` fix
|
|
551
|
+
again. (2) `cp`-based file mirroring on
|
|
552
|
+
Windows/NTFS loses the Unix executable bit - `server.js` (needed for
|
|
553
|
+
its `npx cachegate` shebang) and `sync-oss-release.sh` itself both
|
|
554
|
+
landed as `644` in the initial commit. Fixed via
|
|
555
|
+
`git update-index --chmod=+x <file>` before pushing, which sets the
|
|
556
|
+
bit at the git level regardless of the filesystem - confirmed via
|
|
557
|
+
`git ls-files -s` showing `100755` before the push went out.
|
|
558
|
+
- First CI run on the real public repo passed for real (all three
|
|
559
|
+
Node-version matrix jobs green, ~1 minute) - genuine external
|
|
560
|
+
validation of step 8's workflow, not just this sandbox's own testing.
|
|
561
|
+
- Commit message rewritten before pushing to read as an actual first
|
|
562
|
+
release ("Initial public release of cachegate") rather than the
|
|
563
|
+
script's own generic ongoing-sync message, which is what every
|
|
564
|
+
*later* sync should look like, not this one-time moment.
|
|
565
|
+
|
|
566
|
+
### 17. ✅ Publish the npm package — live 2026-08-29: `npm install cachegate`
|
|
567
|
+
- Published from the CEO's own machine (this session has no npm
|
|
568
|
+
credentials, same category of limitation as steps 15-16's GitHub
|
|
569
|
+
access - noted once there, applies here too without repeating it).
|
|
570
|
+
- **Real wall hit**: npm's registry now requires either account 2FA or
|
|
571
|
+
a Granular Access Token with "Bypass two-factor authentication"
|
|
572
|
+
checked before it accepts a publish (403 otherwise) - npm's own
|
|
573
|
+
token-creation UI has also changed since older guides describe it
|
|
574
|
+
(no more separate "Automation" token type; it's a checkbox on the
|
|
575
|
+
one Granular Access Token form now, under Packages and scopes →
|
|
576
|
+
Permissions → Read and write → All packages, since an unpublished
|
|
577
|
+
package can't be individually selected yet).
|
|
578
|
+
- **A `"bin[cachegate]" script name was cleaned` warning appeared on
|
|
579
|
+
every publish attempt - checked directly rather than assumed
|
|
580
|
+
cosmetic**: queried the live registry after publishing
|
|
581
|
+
(`registry.npmjs.org/cachegate/1.0.0`) and confirmed the published
|
|
582
|
+
`bin` field is `{"cachegate":"server.js"}` - npm just stripped the
|
|
583
|
+
`./` prefix from `"./server.js"` as routine normalization. Not
|
|
584
|
+
corruption, not related to the earlier Windows CRLF finding from
|
|
585
|
+
step 16 - a real, if noisy, false alarm worth having actually
|
|
586
|
+
checked rather than left as an assumption either way.
|
|
587
|
+
- **Verified for real, not just published-and-assumed-fine**: installed
|
|
588
|
+
in a brand new, unrelated directory (`npm init -y` + `npm install
|
|
589
|
+
cachegate`) and confirmed `require('cachegate')` resolves and exposes
|
|
590
|
+
`{ app, isAuthConfigured }` - proves it installs and loads correctly
|
|
591
|
+
from the real registry, not just "works in the folder it was built
|
|
592
|
+
in."
|
|
593
|
+
- **Follow-up gap, caught by DeepSeek's read-only review and fixed
|
|
594
|
+
2026-08-29**: `sync-oss-release.sh --version X.Y.Z` bumps THIS
|
|
595
|
+
directory's own `package.json` (the monorepo source) in place before
|
|
596
|
+
copying, by design (see the script's own header comment) - but that
|
|
597
|
+
bump, from the original `1.0.0` publish, was never committed back
|
|
598
|
+
here. The monorepo's committed `package.json` sat at the stale
|
|
599
|
+
`0.1.0` while npm had `1.0.0` live, and the CEO's local
|
|
600
|
+
`memocode-fresh` checkout carried the real bump only as an
|
|
601
|
+
uncommitted working-tree change. Fixed by committing `package.json`
|
|
602
|
+
(and regenerating `package-lock.json` to match) at `1.0.0` here, so
|
|
603
|
+
the source of truth matches what's actually published instead of
|
|
604
|
+
lagging behind it silently.
|
|
605
|
+
|
|
606
|
+
### 18. ✅ Publish the Docker image — live 2026-08-29: `ghcr.io/idebunk/cachegate:latest`
|
|
607
|
+
- Pushed from the CEO's own machine (same credential/scope limitation
|
|
608
|
+
as steps 15-17, not repeated here). Built with `docker build`, tagged
|
|
609
|
+
both `:1.0.0` and `:latest`, pushed to GHCR with `docker login
|
|
610
|
+
ghcr.io` using a classic GitHub PAT (`write:packages` scope) as the
|
|
611
|
+
password prompt - a separate credential from the npm token in step
|
|
612
|
+
17. Real digest for both tags:
|
|
613
|
+
`sha256:c657784e882ceecd427974c4fb0b72a6953cf45da8fab66a3ae4cc4e2cd4e99f`.
|
|
614
|
+
- **Real wall hit**: GHCR images pushed under a personal account (not
|
|
615
|
+
an org) default to **Private** visibility even though the linked
|
|
616
|
+
repo is Public, and are not auto-linked to that repo's sidebar or
|
|
617
|
+
the pretty `/pkgs/container/<name>` URL (which 404s until linked).
|
|
618
|
+
Fixed the load-bearing part - visibility - via the package's own
|
|
619
|
+
Danger Zone (`github.com/<user>?tab=packages` → click the package
|
|
620
|
+
name → Package settings). The cosmetic repo-link
|
|
621
|
+
(`/pkgs/container/cachegate` resolving, sidebar showing the package)
|
|
622
|
+
was left as-is after confirming it has no effect on public
|
|
623
|
+
pullability - GitHub's package-listing UI showed inconsistent counts
|
|
624
|
+
between `?tab=packages` and a `?repo_name=` filtered variant of the
|
|
625
|
+
same page even after a refresh, which looks like a stale/broken
|
|
626
|
+
query-string filter on GitHub's own UI rather than a real state
|
|
627
|
+
problem, and wasn't worth chasing further.
|
|
628
|
+
- **Update 2026-08-30 - actually closed, not just deprioritized**: used
|
|
629
|
+
the package's own "Connect Repository" button (`Package settings` →
|
|
630
|
+
`Link this package to a repository`) and connected it to
|
|
631
|
+
`iDebunk/cachegate` specifically - worth naming plainly that the repo
|
|
632
|
+
picker also offered the private `2000_1010_memocode01` monorepo (same
|
|
633
|
+
owner, so GHCR's "must be the same owner" rule doesn't rule it out on
|
|
634
|
+
its own), which would have been a real mistake to pick, not just a
|
|
635
|
+
wrong label - a public package's page pointing at a private internal
|
|
636
|
+
repo defeats the entire point of the sync-out design. Verified after
|
|
637
|
+
connecting: the package page now shows `cachegate`'s actual README
|
|
638
|
+
content, confirming the correct repo, not the monorepo's.
|
|
639
|
+
- **Verified for real, cold, from a machine that has never built this
|
|
640
|
+
project**: `docker logout ghcr.io` (drops all local credentials),
|
|
641
|
+
then `docker rmi` both local tags, then `docker pull
|
|
642
|
+
ghcr.io/idebunk/cachegate:latest` - succeeded with zero credentials,
|
|
643
|
+
proving the image is genuinely public. First `docker run` +
|
|
644
|
+
`curl /health` attempt failed (`Exited (1)`, then later a timing-
|
|
645
|
+
related empty reply) - checked `docker logs` rather than guessing,
|
|
646
|
+
and found the real cause: the app's own security hardening from step
|
|
647
|
+
13 correctly refuses to start with an open `/v1` endpoint unless
|
|
648
|
+
`MODEL_ROUTER_INTERNAL_KEY` is set (working as designed, not a
|
|
649
|
+
packaging bug). Reran with that env var set, `docker ps` showed
|
|
650
|
+
`(healthy)`, and `curl http://localhost:4001/health` returned
|
|
651
|
+
`{"status":"healthy","redis_connected":false,...}` - a genuine cold
|
|
652
|
+
pull-run-verify cycle, not published-and-assumed-fine.
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Group E — Tell people, then keep it alive (steps 19-20)
|
|
657
|
+
|
|
658
|
+
### 19. ✅ Announce — posted live 2026-08-31
|
|
659
|
+
- All four posts drafted with real, distinct framing (see the original
|
|
660
|
+
drafting notes below), delivered as a published Artifact ("launch
|
|
661
|
+
kit") plus a standalone downloadable `.html` file.
|
|
662
|
+
- **Posted for real, live, from the CEO's own accounts** - the outward-
|
|
663
|
+
facing, hard-to-reverse action this session genuinely can't do
|
|
664
|
+
itself (no browser session or credentials on any of these
|
|
665
|
+
platforms). Confirmed done by the CEO directly; URLs weren't
|
|
666
|
+
collected/independently verified this pass, noted plainly rather
|
|
667
|
+
than implied otherwise.
|
|
668
|
+
- **HN title had to be trimmed live**: the original draft title (~98
|
|
669
|
+
chars) exceeded Hacker News's 80-character limit - shortened to
|
|
670
|
+
`Show HN: cachegate – self-hosted, cost-aware caching proxy for LLMs`
|
|
671
|
+
(67 chars) on the spot, same core pitch intact.
|
|
672
|
+
- **r/selfhosted was correctly skipped, not posted to**: its submit
|
|
673
|
+
flow required a mandatory flair, and one option was literally
|
|
674
|
+
"Release (No AI)" - cachegate was built with substantial AI
|
|
675
|
+
assistance (this whole roadmap's own execution is direct evidence),
|
|
676
|
+
so that flair genuinely didn't apply. Rather than force an
|
|
677
|
+
ill-fitting flair or imply something untrue, the CEO skipped that
|
|
678
|
+
specific subreddit outright - the right call, not a shortcut.
|
|
679
|
+
- **r/SelfHostedAI posted instead** - a different, more narrowly-
|
|
680
|
+
focused subreddit (self-hosted AI tooling specifically) with no such
|
|
681
|
+
flair conflict, reached mid-session while navigating Reddit's submit
|
|
682
|
+
flow. A reasonable, arguably better-targeted substitution for the
|
|
683
|
+
fourth platform, not the originally-planned r/selfhosted.
|
|
684
|
+
- **A near-miss caught before it mattered**: an early attempt landed on
|
|
685
|
+
`reddit.com/r/Art/submit` (a real subreddit for visual artwork, not
|
|
686
|
+
self-hosted software) - caught immediately from the page's own
|
|
687
|
+
"Title, Artist Name, Medium, Year" format requirement before
|
|
688
|
+
anything was submitted there.
|
|
689
|
+
- Final four actually posted: **Hacker News** (Show HN), **r/LocalLLaMA**,
|
|
690
|
+
**r/SelfHostedAI** (substituted for r/selfhosted), **Dev.to**.
|
|
691
|
+
|
|
692
|
+
Original drafting notes (why each framing was chosen): Show HN leads
|
|
693
|
+
with the honest savings range and the "what it doesn't do yet" list up
|
|
694
|
+
front (that crowd asks about gaps in the first three comments
|
|
695
|
+
regardless); r/LocalLLaMA leads with the one objection that sub raises
|
|
696
|
+
immediately - no local-model backend yet - framed as an open
|
|
697
|
+
contribution gap, not a hidden limitation; the self-hosted-targeted
|
|
698
|
+
post puts the `docker run` one-liner and the healthcheck/no-telemetry
|
|
699
|
+
details before any explanation; Dev.to is the one long-form piece with
|
|
700
|
+
the real origin story (an internal MemoCode cost-control tool that
|
|
701
|
+
turned out worth its own release) and the fullest explanation of the
|
|
702
|
+
exact/semantic cache distinction.
|
|
703
|
+
|
|
704
|
+
### 20. ✅ Post-launch triage plan — decided 2026-08-29
|
|
705
|
+
|
|
706
|
+
**Who watches, and how, stated plainly instead of assumed:**
|
|
707
|
+
`AGENTS.md`'s "one agent holds the standing watch" model (a Claude
|
|
708
|
+
session subscribed to every open PR via `subscribe_pr_activity`) is
|
|
709
|
+
what this team already uses internally - but it does not extend to
|
|
710
|
+
`cachegate` automatically. This session's own repo access is scoped to
|
|
711
|
+
a fixed list of repos for the environment it runs in, `cachegate`
|
|
712
|
+
is not on that list, and adding it hit a real, already-documented
|
|
713
|
+
limitation earlier in this same roadmap (`add_repo` on
|
|
714
|
+
`iDebunk/cachegate` failing with an unresolved approval prompt, back
|
|
715
|
+
in steps 15-16). So, honestly, for now:
|
|
716
|
+
- **The CEO is the standing watch**, via GitHub's own native "Watch →
|
|
717
|
+
All Activity" on the repo plus notifications, not a Claude session -
|
|
718
|
+
that's the real, working mechanism today, not an aspiration.
|
|
719
|
+
- **Response SLA**: first reply to any new issue or PR within 48
|
|
720
|
+
hours, even if just "looked at this, need more detail" or "queued,
|
|
721
|
+
will get to the actual fix by \<date\>" - a fast acknowledgment is
|
|
722
|
+
what prevents "reads as abandoned," not a fast fix.
|
|
723
|
+
- **Revisit if the environment's repo scope ever includes
|
|
724
|
+
`iDebunk/cachegate`** (an access-grant change outside this session's
|
|
725
|
+
own control, not something to keep retrying) - at that point, a
|
|
726
|
+
Claude session picking up the exact same standing-watch model used
|
|
727
|
+
internally becomes possible for real, and is the right thing to
|
|
728
|
+
switch to.
|
|
729
|
+
|
|
730
|
+
**"Phase 5 done" - one concrete bar, not a vibe:**
|
|
731
|
+
Phase 5 (this whole roadmap) is done when **either** of these is true,
|
|
732
|
+
whichever comes first, not both required:
|
|
733
|
+
- 4 consecutive weeks live with no open critical issue (a security
|
|
734
|
+
vulnerability, data loss, or the router silently returning wrong
|
|
735
|
+
answers) at any point during that window, **or**
|
|
736
|
+
- the first external (non-`iDebunk`) pull request is merged.
|
|
737
|
+
A quiet repo with zero critical bugs for a month is success on its own
|
|
738
|
+
terms; so is a total stranger trusting the code enough to send a real
|
|
739
|
+
patch before that clock runs out. Either is real evidence the release
|
|
740
|
+
worked - waiting for both would just be moving the goalpost.
|
|
741
|
+
|
|
742
|
+
**Phase 6 unlock:** per `ROADMAP.md`, Phase 6 (a standalone/hosted
|
|
743
|
+
product built on this same engine, explicitly NOT open-sourced itself)
|
|
744
|
+
becomes buildable once the bar above is actually met - not before, and
|
|
745
|
+
not automatically the moment it is either. Meeting the bar makes
|
|
746
|
+
Phase 6 *plannable* for real, using evidence from actual outside use
|
|
747
|
+
instead of internal dogfooding alone; starting to build it is still a
|
|
748
|
+
separate, deliberate decision when that time comes.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
752
|
+
## Follow-ups discovered after the original 20 steps closed
|
|
753
|
+
|
|
754
|
+
Not renumbered into the list above - the original 20 are a closed,
|
|
755
|
+
honest historical record (see the scaffold-first rule in `AGENTS.md`).
|
|
756
|
+
New real work discovered later gets its own dated entry here instead.
|
|
757
|
+
|
|
758
|
+
### 21. ✅ Docker Hub mirror — live 2026-08-30: `docker.io/shipman/cachegate`
|
|
759
|
+
|
|
760
|
+
Step 9 originally decided GHCR over Docker Hub, explicitly leaving the
|
|
761
|
+
door open: *"Not closed off permanently: worth adding a Docker Hub
|
|
762
|
+
mirror later if search-driven discovery turns out to matter."* Revisited
|
|
763
|
+
because GHCR's own download counter turned out to be unreliable for
|
|
764
|
+
this purpose (checked directly - the live package page shows "Total
|
|
765
|
+
downloads: 0" even after step 18's own confirmed cold pull, so either
|
|
766
|
+
anonymous pulls aren't counted or there's a reporting lag; not
|
|
767
|
+
confirmed which) and Docker Hub remains a real, separate discovery
|
|
768
|
+
surface people specifically search.
|
|
769
|
+
|
|
770
|
+
- **Real wall hit, caught before it caused real confusion**: the plan
|
|
771
|
+
going in assumed the CEO's Docker Hub username was `cachegate`, matching
|
|
772
|
+
the project name. It isn't - `docker login` reported the real account
|
|
773
|
+
as `shipman`. First push to `cachegate/cachegate` correctly failed
|
|
774
|
+
(`insufficient_scope: authorization failed` - `shipman` has no write
|
|
775
|
+
access to a `cachegate` namespace). **Target corrected to
|
|
776
|
+
`docker.io/shipman/cachegate`** - Docker Hub auto-created the repo on
|
|
777
|
+
first push to that personal namespace, no separate creation step
|
|
778
|
+
needed.
|
|
779
|
+
- **Also fixed while in here**: added standard OCI labels to the
|
|
780
|
+
Dockerfile (`org.opencontainers.image.source`, `.description`,
|
|
781
|
+
`.licenses`). `image.source` specifically is what GHCR reads to link
|
|
782
|
+
a package to its repo automatically on every future push - the
|
|
783
|
+
durable alternative to clicking "Connect Repository" by hand once,
|
|
784
|
+
which doesn't survive a re-publish. Same three-line cost, fixes the
|
|
785
|
+
cosmetic repo-link gap left open back at step 18 for good. GHCR's
|
|
786
|
+
already-published `:1.0.0`/`:latest` tags were deliberately left
|
|
787
|
+
untouched - not worth rewriting an immutable version tag for a
|
|
788
|
+
cosmetic label; it reaches GHCR naturally on the next real version
|
|
789
|
+
bump.
|
|
790
|
+
- **Login was actually via Docker's device-code browser flow**, not a
|
|
791
|
+
pasted token at a password prompt - `docker login docker.io` opened
|
|
792
|
+
a one-time code + browser confirmation instead. Functionally
|
|
793
|
+
equivalent (an access token was still created and available as a
|
|
794
|
+
fallback), just a different, newer CLI flow than GHCR's or npm's.
|
|
795
|
+
- **Not a CI automation, on purpose, matching GHCR's own precedent**:
|
|
796
|
+
pushed manually from the CEO's own machine, same credential/scope
|
|
797
|
+
category as steps 15-18 - not something this session can do directly.
|
|
798
|
+
- **Verified for real, cold, same rigor as step 18**: `docker logout
|
|
799
|
+
docker.io` (drops all local credentials), `docker rmi` all local
|
|
800
|
+
tags, `docker pull docker.io/shipman/cachegate:latest` - succeeded
|
|
801
|
+
with zero credentials. Real digest:
|
|
802
|
+
`sha256:4d8548dda952ec1dceff6ff6a9c69e1c86d55193150395465f387c8da171a55e`.
|
|
803
|
+
First `curl /health` attempt hit the same timing gap seen at step 18
|
|
804
|
+
(container 1 second old, still `health: starting`) - not re-guessed
|
|
805
|
+
as a real bug this time, waited for `docker ps` to show `(healthy)`,
|
|
806
|
+
then `curl http://localhost:4002/health` returned
|
|
807
|
+
`{"status":"healthy","redis_connected":false,...}` - a genuine cold
|
|
808
|
+
pull-run-verify cycle on a second, independent registry.
|
|
809
|
+
|
|
810
|
+
### 22. 🟨 `--env-path` flag - built 2026-08-31, not yet republished
|
|
811
|
+
|
|
812
|
+
Decided after a design discussion with the CEO: the cwd-only `.env`
|
|
813
|
+
lookup (`npx cachegate` reads `.env` from wherever you happen to be
|
|
814
|
+
standing, not a fixed path) was a real, repeated point of confusion -
|
|
815
|
+
see step 21's own README work on "Wiring this into your app." Weighed
|
|
816
|
+
against a second candidate (per-app keys / usage tracked separately per
|
|
817
|
+
caller) and deliberately NOT building that one: the README already
|
|
818
|
+
states this is "a single-operator, self-hosted admin tool, not a
|
|
819
|
+
multi-tenant product," and real per-app key custody is explicitly
|
|
820
|
+
named in "What this is NOT" as reserved for the separate, closed,
|
|
821
|
+
hosted product this engine may one day sit under - building it into
|
|
822
|
+
the free engine now would cannibalize that product's own future
|
|
823
|
+
differentiation, not just cost more effort.
|
|
824
|
+
|
|
825
|
+
- **What was built**: an optional `--env-path <file>` /
|
|
826
|
+
`--env-path=<file>` flag, parsed from `process.argv` before
|
|
827
|
+
`dotenv.config()` runs. Backward compatible by construction - no flag
|
|
828
|
+
given behaves exactly as before (reads `.env` from the current
|
|
829
|
+
directory); flag given reads from wherever it points instead,
|
|
830
|
+
resolved relative to the invoking directory or absolute as given.
|
|
831
|
+
`resolveEnvPathFromArgv` extracted as its own small, pure, exported
|
|
832
|
+
function specifically so this could be unit-tested directly rather
|
|
833
|
+
than through a heavier process-spawning test.
|
|
834
|
+
- **Verified twice, not once**: 6 new unit tests
|
|
835
|
+
(`test/env-path.test.js`) covering both flag syntaxes
|
|
836
|
+
(`--env-path X` and `--env-path=X`), absolute paths, the flag missing
|
|
837
|
+
entirely, the flag present with no value after it, and other
|
|
838
|
+
unrelated flags not interfering. Full suite: 105/105 passing. Then a
|
|
839
|
+
real, live smoke test on top of the unit tests - not just trusting
|
|
840
|
+
them - starting `node server.js --env-path <path>` from a directory
|
|
841
|
+
with no `.env` at all, pointing at a real `.env` (with a distinct
|
|
842
|
+
`PORT=4099`) in a completely different location, and confirming via
|
|
843
|
+
`curl /health` that it actually started on the port from that other
|
|
844
|
+
file, not the default.
|
|
845
|
+
- **`README.md` updated** in both places that described the old
|
|
846
|
+
cwd-only behavior as absolute ("there's no separate config path for
|
|
847
|
+
this one") - both now correctly describe the flag as an option.
|
|
848
|
+
- **Version bumped to `1.1.0`** (`package.json` + regenerated
|
|
849
|
+
`package-lock.json`) - a real, backward-compatible feature addition,
|
|
850
|
+
not a patch.
|
|
851
|
+
- **Not done yet, on purpose**: this needs republishing to all three
|
|
852
|
+
registries (npm, GHCR, Docker Hub) before it's real for anyone
|
|
853
|
+
outside this monorepo, same cost and rigor as every other publish
|
|
854
|
+
step in this document - not marking this ✅ until that's actually
|
|
855
|
+
done and cold-verified again.
|