Releases: qorpe/mockifyr
Release list
v0.18.0 — Hardening: bounded journal, probe-safe auth, private disclosure
First release from the project's new home: github.com/qorpe/mockifyr (old URLs redirect; images now publish to ghcr.io/qorpe/mockifyr — previous tags remain pullable from the old path).
Bounded request journal (#220)
- The per-tenant journal is now bounded — default 1000 entries, oldest evicted first, matching the reference engine's journal-cap semantics (proven differentially: both engines capped at 3 retain the same newest three).
--journal-limit <n>configures it (--max-request-journal-entrieskept as an alias);<=0restores unbounded.--journal-disabled(alias--no-request-journal) records nothing — for load tests.GET /__admin/requests/{id}now resolves through a tenant-gated id index — O(1) instead of scanning the whole journal.- A long-running host no longer accumulates every request (and its
Authorizationheaders) forever. Behavior change: previously unbounded; set--journal-limit 0for the old behavior.
Probe-safe admin auth (#218)
/__admin/health is exempt from admin Basic auth — Kubernetes/OpenShift probes can't carry credentials, and a 401 health check sent pods into restart loops. Exact-path exemption; every other admin route stays guarded.
Also
SECURITY.md+ GitHub private vulnerability reporting enabled (#217).- GitSyncTests teardown fixed on Windows (read-only
.gitobjects) (#219).
Validation: TDD differential eviction test, 6 new unit tests, Stryker 100% on the journal store, 581 tests green.
Docs: CLI — request journal · Securing the admin API
Full changes: #221, #222, #223
🤖 Generated with Claude Code
v0.17.0 — Sandbox UI (G19e): the integration sandbox is complete
Sandbox UI (G19e, ADR 0011)
The final vertical of the integration sandbox — G19 is complete. The dashboard gains a Sandbox sidebar group with two screens:
Resources (/resources)
- Collections rail with live counts; paged document table (50/page).
- JSON document editor — create/replace, versions advance on save; client-side validation mirrors the server's guards.
- Seed a collection from a pasted JSON array (transactional), reset one collection or everything, all behind confirmations.
- New collections are created by writing their first document.
Access (/access)
- Issue and revoke sandbox API keys with an optional hourly quota.
- The token appears exactly once in a reveal dialog with a copy button and an explicit warning — afterwards only the display prefix is listed, alongside a live
used / quotabar (warns at 80%, danger at 100%). - The host-side contract is spelled out in place:
--sandbox-auth,X-Api-Key/Bearer, 429 + rate headers, never valid on the admin API.
Dashboard
- A "Spin up a sandbox" quick-start: import spec → seed data → issue key → copy base URL.
All six locales (en/tr/fr/ar/zh/ja), RTL-safe, command-palette entries, tables scroll inside their cards.
Verification
Verified in-browser against a live --sandbox-auth host: a key issued through the dialog authenticated real requests with X-RateLimit-Remaining counting down and the usage bar showing 3/50; a document edit saved through the dialog advanced the server-side version. tsc/lint/build clean.
The G19 series in this train: v0.13.0 resources · v0.14.0 state directive · v0.15.0 OpenAPI import · v0.16.0 API keys + quotas · v0.17.0 Sandbox UI.
Docs: The dashboard · Admin API
Full change: #214
🤖 Generated with Claude Code
v0.16.0 — Sandbox access: API keys + quotas (G19d)
Sandbox access (G19d, ADR 0011)
The fourth vertical of the integration sandbox: hand out per-consumer API keys so external teams can hit a tenant's mock surface without knowing anything about hosts or headers.
API keys — --sandbox-auth
POST /__admin/apikeysissues anmfk_…token (256-bit CSPRNG) shown exactly once — afterwards only a 12-character display prefix is stored and listed. At rest: salted SHA-256, verified in constant time.- A key presented as
X-Api-Key: mfk_…orAuthorization: Bearer mfk_…selects the key's tenant ahead of the host/header chain. No credentials → everything resolves exactly as before; an invalid or revoked key is an honest 401, never a silent fall-through to another tenant. - A sandbox key is not admin authentication —
/__admin/*refuses it on both carriers. - Keys survive restarts on every persistence backend (file root-dir, LiteDB, Postgres, Redis).
Quotas
- Optional
quotaPerHourper key: fixed hourly window, exact under parallel load — N requests across the budget never admit more than the budget. - Counted responses carry
X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset; past the budget the caller gets a realistic 429 withRetry-After.usedThisHouris visible in the key listing.
Validation
No oracle exists for sandbox keys, so per the established precedent: 10 unit tests (constant-time verification, pinned hash format, exact window boundaries on a test clock, 8×50 parallel admits exactly 100 of a 100 budget, persistence round-trips) + 5 wire self-tests against a restarted real host (tenant isolation with two keys, admin refusal, an exact parallel 429 boundary, restart survival). Stryker on the new pure logic: 28/29 killed, the lone survivor analyzed as an equivalent mutant in docs/parity/g19-sandbox.md.
Full change: #213
🤖 Generated with Claude Code
v0.15.0 — Spec in, working sandbox out: OpenAPI import
The third G19 sandbox vertical (ADR 0011): hand Mockifyr an OpenAPI 3.0/3.1 document and get a working mock — or, with Stateful CRUD on, a working sandbox whose POST creates what GET returns.
OpenAPI import (G19c)
POST /__admin/openapi/importand a new OpenAPI channel in the dashboard's Add-stub flow — paste JSON or YAML, import, done- One stub per operation: declared examples serve as-is; example-less schemas synthesize samples (Faker-backed
uuid/email/uri, enum-first, deterministic dates) ?stateful=true: resource-shaped path pairs (/things+/things/{id}) become a live CRUD set wired to the state directive — create answers 201 with aLocationheader, list renders count+items- Imported stubs are ordinary mappings — generated as mapping JSON and read back through the same reader as any bundle: dialect compliance by construction, listable/editable/exportable like anything else
Enterprise guards, delivered
- External
$refs are refused before parsing with the offending pointer named — remote references are never fetched (no SSRF surface) - Spec bombs bounce: 5 MiB size guard + 32-level recursion guard → typed 413/422s, never a hang
- Transactional: on any refusal, nothing is created
Validation: golden-file fixtures pin the generated output byte-for-byte (petstore + a real-world-shaped YAML spec); wire tests prove import by serving — including the full stateful CRUD loop driven from YAML; Stryker 97.3 % with all five survivors analyzed as equivalents in the parity docs; 210 oracle-backed differential tests green untouched; verified in-browser end-to-end.
Docs: admin API and dashboard guides updated.
🤖 Generated with Claude Code
v0.14.0 — Stateful stubs: POST creates what GET returns
The second G19 sandbox vertical (ADR 0011): the opt-in state response directive turns matched stubs into a mini backend — dynamic CRUD state, the most-requested capability class for stub servers.
The state directive (G19b)
"response": {
"status": 201,
"body": "{\"id\":\"{{state.id}}\",\"order\":{{state.body}} }",
"state": { "operation": "create", "collection": "orders" }
}- Five operations on tenant-scoped collections:
create,read,update,delete,list id/documentare template expressions over the request (path segments, body, query); absent create ids are generated, an absent document stores the request body verbatim- Results render as
{{state.id}},{{state.body}},{{state.version}},{{state.count}},{{state.list}}— declaring the directive enables templating, no transformer needed - Misses behave like a real API: unknown ids answer a per-stub
missStatus(default 404); serve-time guards answer 413 over the body cap and 422 for non-JSON — nothing half-lands - State is shared with
/__admin/resourcesand the upcoming dashboard screens — what a stub creates, the admin API sees immediately - Fully opt-in: no directive → zero behavior change (proven by test and by the untouched differential suites)
Architecture kept honest
The engine never interprets state: StateDirective is pure data, applied by the templating renderer behind the existing IResponseRenderer seam — the reference-parity surface does not move.
Validation (ADR 0011 addendum matrix): end-to-end wire loop with a real HttpClient (POST→GET→PUT→LIST→DELETE, tenant isolation, guard statuses, zero-change proof), 12-case semantics table, Stryker 100 % on the directive logic (44/44), 206 oracle-backed differential tests green untouched.
Docs: responses guide with copy-paste examples.
🤖 Generated with Claude Code
v0.13.0 — Sandbox foundations: tenant-scoped resource collections
The first vertical of G19 — integration sandbox (ADR 0011): Mockifyr gains a tenant- and collection-scoped JSON document store behind /__admin/resources — the data plane the upcoming state directive (G19b) will read and write to make POST /orders create what GET /orders/{id} returns.
Sandbox resources (G19a)
- Collections of JSON documents, per tenant: create/replace (last-write-wins with version + timestamps), read, delete, per-collection and per-tenant reset
- Paginated listing from day one (
limit/offset, clamped 1..500) and a collections overview with counts - Transactional seed import: POST a JSON array — every item is validated before anything lands; explicit
ids honored, absent ids generated - Bounded by design: per-collection document cap (
--resource-limit, default 1000, oldest evicted first) and per-document body cap (--resource-max-body, default 1 MiB) with an honest 413 - Bodies are opaque JSON — validated well-formed at the edge, stored and re-served byte-for-byte (unicode/emoji proven over the wire)
- Honest error surface: 404 / 413 / 422 with typed codes
Enterprise-readiness, delivered not promised
Built under ADR 0011's binding addendum: exact-boundary validation (collection 64 / id 256 / UTF-8 byte cap), hostile and unicode ids as opaque keys, concurrency-safe store, opt-in everything — existing users see zero change.
Validation: 25 store + 8 handler unit tests, 7 wire self-tests over a real Kestrel host, Stryker 100 % on both new pure-logic files (24/24 + 24/24); no oracle exists (the reference engine has no resource concept), and the full differential suite stays green untouched — the proof the parity surface did not move.
Docs: admin API reference and CLI flags updated; design + criteria in docs/decisions/0011 and docs/parity/g19-sandbox.md.
🤖 Generated with Claude Code
v0.12.3 — Recorded repeats replay in order
Recording the same request more than once used to leave disconnected duplicate stubs. Following the reference oracle's behavior, repeats are now chained into a generated scenario.
Recorder
- Repeated requests become a scenario chain: the first capture serves at
Startedand advances, each later capture serves from the prior state — a replay returns the recorded responses in recorded order - An upstream whose answer changed between repeats now replays faithfully; distinct requests stay scenario-free
- Repeat identity is method + URL + request body — exactly what the generated request pattern matches on
Oracle-verified
The initial plan was to deduplicate; the real reference oracle refuted it and showed the scenario-chain shape instead, which Mockifyr now generates identically. Pinned by Recording_RepeatedIdenticalRequests_CaptureLikeTheOracle (structural bundle comparison on both sides), with the chain rules unit-tested and mutation-tested to 100 % (32/32).
Validation: 194 oracle-backed differential tests (one new), 292 unit tests, all green.
Docs: record & playback guide updated; docs/parity/g12-transport.md records the learned behavior and closes the G9-era "repeats → scenarios" deferral.
🤖 Generated with Claude Code
v0.12.2 — Faithful recording of compressed APIs
A bug found live while recording a real public API from a browser: compressed upstream responses were baked into generated stubs as raw gzip bytes — unreadable in the dashboard, unreplayable ever after.
Fix
- Recording now decodes compressed upstream bodies (gzip, deflate/zlib, brotli) into the generated stub and drops the
Content-Encodingheader — the stub holds the payload a client actually sees - The live pass-through to the caller stays byte-identical (your browser still receives the compressed wire response while recording)
- An encoding that cannot be decoded falls back to the raw capture — never worse than before
Oracle-verified
Pinned with a new differential test (Recording_AGzippedUpstreamResponse_GeneratesAReplayableStub): record → stop → reload → replay against an always-gzip upstream on both Mockifyr and the real reference oracle. Red before the fix (oracle replayed the upstream text; Mockifyr replayed invalid gzip), green after — and re-verified end-to-end against jsonplaceholder, the exact scenario that surfaced it.
Validation: 193 oracle-backed differential tests (one new), 285 unit tests, live re-test of the original browser-driven recording.
🤖 Generated with Claude Code
v0.12.1 — Recordings flow, completed
The Recordings screen recorded fine but dead-ended in the dashboard — captured stubs could be listed, not used. This release closes the loop.
Recordings fixes
- View JSON works: each captured stub expands into a read-only, syntax-highlighted view of the generated mapping (it was an empty
<details>that did nothing) - Captured stubs can now be saved: per-row Add to stubs and a header Import all (N) — both through the standard bulk-import path; imported rows leave the list and the stub counts update live
- Switching tenants clears the captured list, so one tenant's capture is never one accidental click from another
- Hint corrected (all six locales): while recording, every request is proxied to the target and captured — existing stubs answer again once the recording stops
Oracle-verified semantics
The old hint claimed only unmatched requests are proxied. A new differential test tried to prove that — and the real reference oracle refuted it: it proxies every request during a live session, matched-stub requests included. Mockifyr already behaved identically (no server change); the behavior is now pinned by Recording_ProxiesEveryRequest_EvenOnesAnExistingStubWouldMatch and recorded in docs/parity/g12-transport.md, including faithful 404 replay of captured upstream misses.
Validation: 192 oracle-backed differential tests (one new), 285 unit tests, in-browser end-to-end (record → snapshot → view → import → replay against a live upstream).
Docs: record & playback and dashboard guides updated.
🤖 Generated with Claude Code
v0.12.0 — Test a stub without leaving the dashboard
The stub editor gains a built-in request runner (#203) — try a stub the moment you write it, no curl or Postman needed.
Stub test runner
- A Test button on HTTP and GraphQL stubs opens a request dialog: method, URL and Send on top; Params / Headers / Body tabs below
- Pre-filled from the stub's matchers —
equalToheaders and query params, the firstequalTo/equalToJsonbody pattern, and an automaticContent-Type: application/jsonwhen the body is JSON; unsaved editor changes count - A real request: it runs the full serving pipeline — environment
{{key}}s resolve server-side, scenarios advance, and the call lands in the request journal like any traffic;{{key}}s also preview inline before sending - Response view in the journal's visual language: status chip, duration, size, headers, pretty-printed body — plus Copy body, Copy as curl, and a body Beautify
- Connection failures and timeouts surface inline; closing and reopening re-seeds from the stub's current configuration
- gRPC/WebSocket stubs hide the button (a plain HTTP probe can't exercise them); all six dashboard locales translated
Validation: verified in-browser against a live host — matched 200 with server-resolved environment values, unmatched 404, network-error banner, reopen-reset, and both test calls visible in the journal. tsc/lint/build clean; no engine or server change, differential suite green.
Docs: dashboard guide updated.
🤖 Generated with Claude Code