Skip to content

Releases: qorpe/mockifyr

v0.18.0 — Hardening: bounded journal, probe-safe auth, private disclosure

Choose a tag to compare

@omercelikdev omercelikdev released this 28 Jul 11:45

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-entries kept as an alias); <=0 restores 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 Authorization headers) forever. Behavior change: previously unbounded; set --journal-limit 0 for 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 .git objects) (#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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 21:54

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 / quota bar (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)

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 21:16

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/apikeys issues an mfk_… 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_… or Authorization: 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 quotaPerHour per 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 with Retry-After. usedThisHour is 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.

Docs: CLI flags · Admin API

Full change: #213

🤖 Generated with Claude Code

v0.15.0 — Spec in, working sandbox out: OpenAPI import

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 20:34

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/import and 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 a Location header, 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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 19:46

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/document are 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/resources and 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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 19:19

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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 12:45

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 Started and 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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 12:13

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-Encoding header — 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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 11:30

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

Choose a tag to compare

@omercelikdev omercelikdev released this 27 Jul 10:58

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 matchersequalTo headers and query params, the first equalTo/equalToJson body pattern, and an automatic Content-Type: application/json when 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