@databehave/server 0.1.1 → 0.1.2

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.
@@ -1,242 +0,0 @@
1
- # OpenAPI fallback
2
-
3
- Paths declared in the OpenAPI document but absent from `endpoints` are
4
- auto-served by the walker. **Hand-written routes always win** — the
5
- walker silently skips any key already declared in `endpoints`. There
6
- is no precedence flag; the OAS doc is the source of truth for *gaps*,
7
- not for overrides. This rule is canonical here; other pages link back.
8
-
9
- This is intentional. The OAS document is your spec; @databehave/server treats it
10
- as the source of truth for *gaps*, not as an override.
11
-
12
- ## Input format
13
-
14
- @databehave/server accepts **JSON** OpenAPI documents only — convert
15
- YAML first via `yq -o=json openapi.yaml > openapi.json` and pass the
16
- JSON path or string to the loader. YAML inputs are rejected at boot;
17
- see [config/openapi.md](../config/openapi.md) for the canonical
18
- error wording.
19
-
20
- A leading UTF-8 byte-order mark (`U+FEFF`) is stripped before parsing,
21
- so files exported from Windows tooling load without manual cleanup.
22
- After BOM stripping, the input must parse as plain JSON or
23
- `JSON.parse` raises (caught and re-thrown with the file path
24
- prepended).
25
-
26
- ## Pipeline
27
-
28
- ```bash
29
- openapi.json ─▶ parser ─▶ paths.<p>.<m> ─▶ walker ─▶ generateFromOasSchema ─▶ response
30
- │
31
- ├─▶ onOpenApiWalkError (recoverable)
32
- └─▶ onOpenApiEmptySchema (advisory)
33
- ```
34
-
35
- The walker visits every `paths.*.<method>.responses['200'].content
36
- ['application/json'].schema` and feeds the OAS node to the in-server,
37
- zero-dep generator (`src/openapi/generate.ts`), which produces a
38
- deterministic placeholder JSON value. For non-200 responses (used by
39
- `mockMode`), the same generator feeds the per-status body builders.
40
- For richer mock data (distributions, datasets, FK-aware generation),
41
- install `@databehave/schema` separately and pre-generate fixtures —
42
- the server itself pulls no DSL dependency at runtime.
43
-
44
- ### What the walker visits
45
-
46
- - `paths['/']` (root) is **skipped**. Use an explicit `endpoints`
47
- entry if you need to serve `/`.
48
- - HTTP methods walked are exactly `get`, `post`, `put`, `delete`,
49
- `patch`. `head` and `options` are handled by the dispatcher
50
- (HEAD auto-derives from GET; OPTIONS is owned by CORS / admin).
51
- - Vendor extensions (`x-*` keys) on path items are ignored.
52
- - The OAS-only generator (`src/openapi/generate.ts`) is **not**
53
- depth-capped; cyclic `$ref`s are detected via a visited-pointer
54
- set and collapse to `{}` rather than infinite-loop. The
55
- auto-schema translator and the request validator both hard-cap
56
- recursion at **64** levels of nested `properties` / `items` /
57
- `allOf` / `oneOf` / `anyOf` and **fail loud** past that — see
58
- [`keywords.md` → Auto-schema translator](./keywords.md#auto-schema-translator-schema-set)
59
- and [`config/validation.md#security-guarantees`](../config/validation.md#security-guarantees).
60
- Hit either in practice → flatten the schema or declare the
61
- route in `endpoints` directly.
62
-
63
- ### Status code picked
64
-
65
- For mock-mode passthrough (no override) the walker picks the
66
- response body from `responses['200']`. For mock-mode injected
67
- statuses, the body comes from the smallest `2xx` declared in the
68
- spec when an exact match is missing, falling back to the OAS
69
- `default` response, falling back to the envelope
70
- `{ error: true, status: N }`. `204` always renders as an empty
71
- body regardless of what the spec declares.
72
-
73
- ### Per-status body generators
74
-
75
- `buildOpenApiResponseGenerators(doc)` exposes the per-status body
76
- table the dispatcher uses internally — keyed by `"METHOD path"`,
77
- each entry maps a status code to a memoised body generator. The
78
- function is exported so consumers wiring mock-mode into a custom
79
- dispatch can reuse the same body shapes the kit ships with. See
80
- [../config/mock-mode.md#body-resolution](../config/mock-mode.md#body-resolution)
81
- for how the dispatcher consumes the table.
82
-
83
- ## Deterministic seeds
84
-
85
- For a route `/users/:id` matched against `/users/42?expand=profile`:
86
-
87
- ```text
88
- seed = sha256(
89
- '/users/:id' // @databehave/server pattern, not the live URL
90
- + '|param:id=42' // path params first (sorted)
91
- + '|expand=profile' // query params (sorted)
92
- )
93
- ```
94
-
95
- Same URL → same JSON body, byte-for-byte, on every machine. This is
96
- what makes snapshot tests stable across CI runs.
97
-
98
- Per-status bodies use `endpoint|status` as the seed, so the 500 body
99
- for `/users/:id` is stable but different from the 404 body.
100
-
101
- ## Empty / permissive schemas (`schema: {}`)
102
-
103
- ```jsonc
104
- {
105
- "/api/v1/anything": {
106
- "get": {
107
- "responses": {
108
- "200": {
109
- "content": {
110
- "application/json": {
111
- "schema": {} // ← "any JSON value" per JSON Schema
112
- }
113
- }
114
- }
115
- }
116
- }
117
- }
118
- }
119
- ```
120
-
121
- Per JSON Schema spec this matches any value, so @databehave/server:
122
-
123
- 1. Does **not** raise this via `onOpenApiWalkError` — it is a valid
124
- OAS construct.
125
- 2. Serves a small stub body (`{}` for GET, `{ success: true, message:
126
- null }` for mutations).
127
- 3. Surfaces the gap via the dedicated `onOpenApiEmptySchema` callback
128
- so you can fix the spec.
129
-
130
- The default JSONC loader wires this to a friendly warning:
131
-
132
- ```text
133
- [@databehave/server] OAS response schema is empty for GET /api/v1/anything (status 200)
134
- — serving stub. Consider filling the schema in your OpenAPI document.
135
- ```
136
-
137
- To handle it yourself programmatically:
138
-
139
- ```ts
140
- createServer({
141
- openapi: doc,
142
- onOpenApiEmptySchema: (method, path, status) => {
143
- metrics.increment('@databehave/server.empty_schema', { method, path, status })
144
- },
145
- })
146
- ```
147
-
148
- ## Walker failures
149
-
150
- When the walker hits an OAS construct it genuinely cannot translate
151
- (unsupported keyword, unresolvable `$ref`, etc.), it falls back to a
152
- stub and reports via `onOpenApiWalkError`:
153
-
154
- ```text
155
- [@databehave/server] openapi walk failed for GET /api/v1/weird: $ref not found: #/components/schemas/Missing
156
- ```
157
-
158
- The route stays usable (the stub body keeps clients happy) but the
159
- gap is visible. The JSONC loader wires this to `console.warn` by
160
- default; override with:
161
-
162
- ```ts
163
- createServer({
164
- openapi: doc,
165
- onOpenApiWalkError: (method, path, err) => {
166
- // Fail CI on any walker error:
167
- throw err
168
- },
169
- })
170
- ```
171
-
172
- ## Skipping OAS-declared routes
173
-
174
- Routes that exist in `endpoints` (hand-written or static) are
175
- automatically skipped by the walker — no extra configuration needed.
176
- This is the recommended way to override an OAS path: declare it in
177
- `endpoints` and the walker leaves it alone.
178
-
179
- If you need to skip an OAS path without declaring a replacement
180
- (e.g. a deprecated endpoint), declare it as an `EndpointResponse`
181
- returning the deprecation message:
182
-
183
- ```jsonc
184
- "endpoints": {
185
- "GET /api/v1/legacy": {
186
- "response": {
187
- "status": 410,
188
- "json": { "error": "gone", "message": "Use /api/v2/replacement" }
189
- }
190
- }
191
- }
192
- ```
193
-
194
- ## Trust model
195
-
196
- @databehave/server assumes the OpenAPI document comes from a **trusted source**
197
- (your own repo, your own build pipeline). The walker passes `pattern` fields
198
- straight to `new RegExp(...)` and that compiled regex is consulted by
199
- `parse()` on incoming request bodies. A hostile OAS author could therefore
200
- supply a catastrophic-backtracking pattern (e.g. `(a+)+b`) that would slow
201
- down request validation against attacker-controlled input.
202
-
203
- Mock-body generation (`mock()`) does **not** evaluate the regex, so simply
204
- *loading* a hostile OAS is safe — but if you also turn on request-body
205
- validation in front of untrusted input, validate or sandbox patterns from
206
- untrusted OAS docs before passing them in.
207
-
208
- ### `pattern` requires a `maxLength` sibling
209
-
210
- To make this concrete, the generator refuses to mock a string node
211
- that declares a `pattern` without a `maxLength`. The boot-time error
212
- is:
213
-
214
- ```text
215
- @databehave/server/openapi: pattern at <pointer> requires a sibling maxLength to bound generation (ReDoS guard)
216
- ```
217
-
218
- This is a hard guard, not a warning — fixing the OAS to add a
219
- sensible `maxLength` is required before the kit will boot. The same
220
- guard fires on nested string nodes, e.g. `items.pattern` inside an
221
- array.
222
-
223
- The `pattern` source itself is also length-capped at **1024
224
- characters** in both the auto-schema translator and the request
225
- validator. A pattern longer than the cap is rejected at boot with:
226
-
227
- ```text
228
- @databehave/server/openapi: pattern length <N> exceeds cap of 1024 (ReDoS guard) (at <pointer>)
229
- ```
230
-
231
- The cap exists because regex engines have super-linear behaviour on
232
- sufficiently long pathological patterns even with `maxLength`-bounded
233
- input — capping the pattern source is the cheap belt-and-braces.
234
-
235
- ## Admin route picker
236
-
237
- The OAS document is also consumed by the admin panel. When
238
- `admin.enabled`, the kit serves `GET {admin.path}/openapi-routes`
239
- which returns `{ routes: { method, path, summary }[], discoveredAt }`
240
- derived from the same OAS doc the walker uses. The admin UI calls this
241
- endpoint to populate its route picker — no separate spec is needed.
242
- See [../admin/rest-api.md](../admin/rest-api.md).
@@ -1,78 +0,0 @@
1
- # Supported OAS keywords
2
-
3
- The kit ships **two** OAS readers and they support different
4
- keyword sets. Pick the one that matches your config:
5
-
6
- - The **OAS-only generator** runs by default — when `schema:` is
7
- absent from `databehave.jsonc`. Zero-dep, conservative subset.
8
- - The **auto-schema translator** runs when `schema:` is set. Lifts
9
- the OAS into the `@databehave/schema` IR, so it accepts a wider
10
- keyword set and can fail loudly on constructs it cannot model.
11
- See [auto-schema.md](./auto-schema.md).
12
-
13
- ## OAS-only generator (default)
14
-
15
- ```text
16
- $ref ……………… resolved against components.schemas
17
- circular refs collapse to {} (recursion guard)
18
- type: 'object' properties + required
19
- type: 'array' items
20
- type: 'string' + minLength / maxLength / pattern (with maxLength sibling — see fallback.md)
21
- type: 'integer' + minimum / maximum
22
- type: 'number' + minimum / maximum
23
- type: 'boolean'
24
- enum string enums sampled from the list, deterministically
25
- const literal value emitted verbatim
26
- allOf merged left-to-right
27
- oneOf first branch picked deterministically
28
- anyOf first branch picked deterministically
29
- nullable: true string types may emit null
30
- ```
31
-
32
- `format` **is** consumed for the five common cases:
33
-
34
- | `format` | Emitted value |
35
- | ------------ | --- |
36
- | `date` | `"2024-01-01"` |
37
- | `date-time` | `"2024-01-01T00:00:00Z"` |
38
- | `email` | `"user@example.com"` |
39
- | `uuid` | `"00000000-0000-4000-8000-000000000000"` |
40
- | `uri` / `url`| `"https://example.com/"` |
41
-
42
- Any other `format` falls through to the bare-type default
43
- (`"string"`, `0`, `false`, …). File an issue if you need
44
- format-aware sampling beyond this list.
45
-
46
- ## Auto-schema translator (`schema:` set)
47
-
48
- The translator (`translateOasToIR` in `src/openapi/translate.ts`)
49
- accepts everything the OAS-only generator does, plus:
50
-
51
- - `format`: `date`, `date-time`, `email`, `uuid`, `uri`, `url`.
52
- - `nullable: true` (lifts the IR node to `union(t, null)`).
53
- - `minLength`, `maxLength`, `pattern` (passed through).
54
- - `exclusiveMinimum`, `exclusiveMaximum` (boolean OAS-3.0 form
55
- and numeric OAS-3.1 form both accepted).
56
- - `minItems`, `maxItems`.
57
- - `discriminator` on `oneOf` / `anyOf` (resolves to the right
58
- branch when the input has the discriminator property).
59
- - `additionalProperties` (boolean and schema-shaped both honoured).
60
- - `example` / `examples` (first usable value picked when present).
61
-
62
- ### Unsupported — fail loud
63
-
64
- The translator refuses these keywords at boot rather than silently
65
- dropping them. The error wording is
66
- `@databehave/server: translateOasToIR failed for <route> responses[<status>]: unsupported keyword <name>`:
67
-
68
- - `if` / `then` / `else`
69
- - `dependentSchemas`, `dependentRequired`
70
- - `unevaluatedProperties`, `unevaluatedItems`
71
- - `propertyNames`
72
- - `patternProperties`
73
- - `contentEncoding`, `contentMediaType`
74
- - Remote `$ref` (anything not under `#/components/schemas/...`)
75
-
76
- The fail-loud list is intentionally short — the goal is to expose
77
- gaps in the IR, not silently drift away from the spec. File an
78
- issue if you hit one and need it modelled.
package/docs/recipes.md DELETED
@@ -1,258 +0,0 @@
1
- # Recipes
2
-
3
- Practical patterns for common mocking problems. Each example is
4
- self-contained and runnable against the public surface.
5
-
6
- ## Static health endpoint
7
-
8
- ```jsonc
9
- "endpoints": {
10
- "GET /health": {
11
- "response": {
12
- "status": 200,
13
- "json": { "ok": true }
14
- }
15
- }
16
- }
17
- ```
18
-
19
- No handler module to write, no OAS entry needed. Add the path to
20
- `mockMode.healthPaths` so `mockMode` does not inject an error status
21
- into your probes:
22
-
23
- ```jsonc
24
- "mockMode": {
25
- "enabled": true,
26
- "healthPaths": ["/health"]
27
- }
28
- ```
29
-
30
- ## Force a single endpoint to 500
31
-
32
- ```jsonc
33
- "endpoints": {
34
- "POST products/bulk-update": {
35
- "handler": "./src/routes/bulk-update.js",
36
- "status": 500
37
- }
38
- }
39
- ```
40
-
41
- The handler still loads (and stays valid for tests that call it
42
- directly), but live HTTP requests get a 500 with the OAS-derived
43
- error body. To remove the override, delete the `status` field.
44
-
45
- ## Force every endpoint to 500 (chaos mode)
46
-
47
- ```jsonc
48
- "mockMode": {
49
- "enabled": true,
50
- "defaultStatus": 500,
51
- "healthPaths": ["/health"]
52
- }
53
- ```
54
-
55
- Health probes pass through, everything else returns 500 with the
56
- OAS-derived error body. Useful for client-side error-path testing.
57
-
58
- ## In-process testing with `fetch()`
59
-
60
- ```ts
61
- import { describe, it, expect } from 'vitest'
62
- import { createServer } from '@databehave/server'
63
- import { loadConfig } from '@databehave/server'
64
-
65
- describe('mock server', () => {
66
- it('serves the OAS-derived east inventory', async () => {
67
- const { config } = await loadConfig('./databehave.jsonc')
68
- const server = createServer(config)
69
-
70
- const res = await server.fetch(
71
- new Request(
72
- 'http://localhost/api/v1/products/list?date_from=2024-01-01',
73
- ),
74
- )
75
-
76
- expect(res.status).toBe(200)
77
- expect(res.headers.get('x-mock-status')).toBe('200')
78
- const body = await res.json()
79
- expect(body).toHaveProperty('result')
80
- })
81
- })
82
- ```
83
-
84
- No sockets, no `listen()`. The same `Request` URL always yields the
85
- same JSON body — assert against it directly or use a snapshot.
86
-
87
- ## Auth short-circuit via `hooks.onRequest`
88
-
89
- ```ts
90
- import { createServer } from '@databehave/server'
91
-
92
- const server = createServer({
93
- routes: { /* ... */ },
94
- hooks: {
95
- onRequest: (req) => {
96
- if (req.headers.authorization !== 'Bearer test-token') {
97
- return { status: 401, json: { error: 'unauthorized' } }
98
- }
99
- // returning void → continue to the route handler
100
- },
101
- },
102
- })
103
- ```
104
-
105
- The handler is never reached for requests without the token. Pair
106
- with `mockMode.healthPaths` if you want probes to bypass auth.
107
-
108
- ## Inject a header on every response
109
-
110
- ```ts
111
- createServer({
112
- routes: { /* ... */ },
113
- hooks: {
114
- onResponse: (_req, res) => ({
115
- ...res,
116
- headers: { ...(res.headers ?? {}), 'x-build-id': process.env.BUILD_ID ?? 'local' },
117
- }),
118
- },
119
- })
120
- ```
121
-
122
- `onResponse` runs after the handler but before the runtime adapts the
123
- POJO into a web `Response`.
124
-
125
- ## Generate deterministic bodies with `databehave`
126
-
127
- ```ts
128
- // src/routes/products/list.ts
129
- import { mock, obj, str, decimal, arr, seedFor } from '@databehave/server'
130
- import type { Handler } from '@databehave/server'
131
-
132
- const Product = obj({
133
- category: str().in(['books', 'movies']),
134
- price: decimal(10, 2).min('0').max('1000').typically(400, 700),
135
- })
136
-
137
- const Body = obj({ result: arr(Product).length(48) })
138
-
139
- const handler: Handler = (req) => {
140
- const seed = seedFor({
141
- endpoint: '/api/v1/products/list',
142
- extra: Object.fromEntries(new URL(req.url).searchParams),
143
- })
144
- return { json: mock(Body, { seed }) }
145
- }
146
-
147
- export default handler
148
- ```
149
-
150
- Same URL → same body. `databehave` is re-exported from `@databehave/server` so a
151
- single import covers schema authoring and server wiring.
152
-
153
- ## Per-variant sub-routes (one handler each — no shared helpers)
154
-
155
- When granularity matters, write each endpoint as a separate handler
156
- with its own seed namespace. Do **not** extract a helper that loops
157
- over variants — you lose the ability to tune `min` / `max` / `typically`
158
- per endpoint without disturbing the others.
159
-
160
- ```jsonc
161
- "endpoints": {
162
- "GET products/list": "./src/routes/products/list.js",
163
- "GET products/featured": "./src/routes/products/featured.js",
164
- "GET products/summary": "./src/routes/products/summary.js"
165
- }
166
- ```
167
-
168
- Each handler controls its own `min` / `max` / `typically` ranges
169
- independently.
170
-
171
- ## Custom CORS origin reflector
172
-
173
- The JSONC form mirrors the request `Origin` by default. To customise:
174
-
175
- ```ts
176
- createServer({
177
- routes: { /* ... */ },
178
- cors: {
179
- origin: (o) => (o.endsWith('.example.com') ? o : 'https://app.example.com'),
180
- credentials: true,
181
- exposeHeaders: ['x-mock-status'],
182
- },
183
- })
184
- ```
185
-
186
- ## Bind to a random port in tests
187
-
188
- ```ts
189
- const server = createServer(config)
190
- const { port, close } = await server.listen({ port: 0 })
191
-
192
- // ... run tests against http://127.0.0.1:${port} ...
193
-
194
- await close()
195
- ```
196
-
197
- `port: 0` asks the OS for a free port; the actual port comes back in
198
- the handle.
199
-
200
- ## Inject a 503 on every route via the admin REST API
201
-
202
- Enable `admin` (see [admin.md](./admin.md)), then POST a global
203
- sticky override. Every subsequent request returns 503 until cleared.
204
-
205
- ```sh
206
- curl -sS -X POST http://127.0.0.1:8000/databehave/overrides \
207
- -H 'content-type: application/json' \
208
- -d '{"matcher":{"kind":"global"},"mode":{"kind":"http-status","status":503}}'
209
-
210
- curl -sS -X DELETE http://127.0.0.1:8000/databehave/overrides # clear
211
- ```
212
-
213
- ## Replay a captured response as a custom-body override
214
-
215
- Paste a real response payload as base64 JSON into a sticky `custom-body`
216
- override; the mock returns it verbatim, with the status of your choice.
217
-
218
- ```sh
219
- BODY_B64=$(printf '{"result":[{"id":1,"name":"replayed"}]}' | base64)
220
- curl -sS -X POST http://127.0.0.1:8000/databehave/overrides \
221
- -H 'content-type: application/json' \
222
- -d "{\"matcher\":{\"kind\":\"exact\",\"method\":\"GET\",\"path\":\"/api/v1/products/list\"},
223
- \"mode\":{\"kind\":\"custom-body\",\"status\":200,\"body\":$(echo "$BODY_B64" | base64 -d)}}"
224
- ```
225
-
226
- ## Simulate slow networks with a sticky delay
227
-
228
- A `delay` override wraps another mode (or passthrough). 3 seconds is
229
- enough to exercise loading indicators without a test timing out.
230
-
231
- ```sh
232
- curl -sS -X POST http://127.0.0.1:8000/databehave/overrides \
233
- -H 'content-type: application/json' \
234
- -d '{"matcher":{"kind":"global"},"mode":{"kind":"delay","ms":3000}}'
235
- ```
236
-
237
- ## Save and load named scenarios
238
-
239
- Snapshot the current sticky overrides under a name, then re-apply later
240
- in one step.
241
-
242
- ```sh
243
- curl -sS -X POST http://127.0.0.1:8000/databehave/scenarios \
244
- -H 'content-type: application/json' -d '{"name":"all-503"}'
245
-
246
- curl -sS -X POST http://127.0.0.1:8000/databehave/scenarios/all-503/load
247
- # → {"loaded":1}
248
- ```
249
-
250
- Scenarios live on disk under `admin.scenariosDir` (default
251
- `${cwd}/mock-scenarios`). See [admin.md](./admin.md#scenarios).
252
-
253
- ## Enable per-request access logs
254
-
255
- Set `"log": true` (shorthand for `{ "access": true }`) to emit one
256
- stdout line per request, with override-tag suffixes such as
257
- `[override:http-status]`. Full field reference, JSON format, and
258
- admin-traffic toggle live in [config/log.md](./config/log.md).
package/docs/stability.md DELETED
@@ -1,118 +0,0 @@
1
- # Stability Contract
2
-
3
- This document defines what `@databehave/server` guarantees across versions, in
4
- addition to the contract inherited from
5
- [`@databehave/schema`](../../schema/docs/stability.md).
6
-
7
- ## Public surface
8
-
9
- The **public surface** is exactly the symbols re-exported from `src/index.ts`,
10
- plus the runtime shape of `Config` and the JSONC config schema
11
- accepted by `loadConfig`. Anything reachable only through
12
- deeper paths (`@databehave/server/src/...`, `_internal`, identifiers prefixed with
13
- `_`) is **internal** and may change in any release, including `PATCH`.
14
-
15
- The set of public symbols is locked by `test/public-surface.test.ts`.
16
-
17
- The `admin` admin REST endpoints (`/overrides`, `/scenarios`,
18
- `/openapi-routes`, `/ui*`) and their request / response shapes are part
19
- of the public surface; the versioning rules below apply to them too.
20
-
21
- ## Versioning rules
22
-
23
- Same `PATCH` / `MINOR` / `MAJOR` rules as `@databehave/schema` apply, plus:
24
-
25
- | Bump | Additional triggers for `@databehave/server` |
26
- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
27
- | **PATCH** | Wire-level behaviour of `fetch(Request) → Response` for handlers that previously returned identical bodies. Header casing fixes. |
28
- | **MINOR** | New CORS options, new mock-mode hooks, new JSONC fields with backwards-compatible defaults. Additive `admin` admin REST endpoints, additive fields on admin request / response shapes, new `x-mock-*` headers. |
29
- | **MAJOR** | Default CORS policy changes that may close previously-permitted requests. Removal or renaming of JSONC fields. Changes to OAS path dispatch order. Removal / renaming of admin REST endpoints, breaking admin request / response shape changes, removal / renaming of `x-mock-*` headers, changes to sticky-override resolution priority. |
30
-
31
- ## Determinism contract
32
-
33
- For a fixed config (`Config`, JSONC, OAS document) and a fixed
34
- request, the **response body** generated by @databehave/server is deterministic
35
- across `PATCH` and `MINOR` releases. The same per-request seeding rules
36
- documented in `docs/seeding.md` apply.
37
-
38
- ## Mock-mode dispatch order
39
-
40
- Within a single config, mock-mode resolves the response status and body in
41
- this order, in this priority (first match wins):
42
-
43
- 1. Explicit `pathOverrides` entry keyed by `'<METHOD> /path'` (concrete path).
44
- 2. Explicit `pathOverrides` entry keyed by `'/path'` (any method).
45
- 3. Per-endpoint `status` set in JSONC `endpoints`.
46
- 4. `mockMode.defaultStatus` (when set).
47
-
48
- Body resolution, once a status is chosen, follows this priority:
49
-
50
- 1. Hand-written handler body (when registered for the route).
51
- 2. OAS `responses[N]` schema match by **specificity**:
52
- 1. Static path patterns (`/items/me`) before parametric (`/items/:id`).
53
- 2. Within the same kind, longer paths before shorter wildcards.
54
- 3. Lexical fallback for ties (deterministic, declaration-order
55
- independent).
56
- 3. Generic envelope `{ error: true, status: N }`.
57
-
58
- This order is part of the public contract; changes are `MAJOR`.
59
-
60
- ## CORS defaults
61
-
62
- - `cors: undefined` → no CORS headers added (handlers fully control).
63
- - `cors: {}` → permissive default for **mock development**:
64
- `Access-Control-Allow-Origin` mirrors the request `Origin` (or `*` if absent).
65
- This is intentionally lax because @databehave/server is a developer mock server.
66
- - `cors: { origin: ... }` → strict; the resolver function is authoritative.
67
- - `cors: { allowHeaders: [] }` → hard lockdown; preflight omits the header.
68
-
69
- When a deployment must run on a network where third parties can reach the
70
- mock server, set `cors: { origin: '...' }` explicitly. The README documents
71
- this.
72
-
73
- ## Fail-loud policy
74
-
75
- Configuration that is structurally valid but semantically nonsensical throws
76
- at config-load time, not at request time. Examples already enforced:
77
-
78
- - `pathOverrides` keys that do not start with `/` or `<METHOD> /`.
79
- - `pathOverrides` values that are not numbers.
80
- - OpenAPI documents that fail to parse as JSON (convert YAML first via `yq -o=json`).
81
-
82
- ## Logger injection
83
-
84
- Every diagnostic the kit emits goes through `LoadConfigOptions.logger.warn`
85
- (or the equivalent `MockModeConfig.logger`). The kit never calls
86
- `console.warn` / `console.error` directly. Callers may pipe to their preferred
87
- sink (pino, winston, structured JSON, /dev/null).
88
-
89
- The opt-in `log` feature (top-level `log` config, see
90
- [config.md](./config.md#log)) is **separate** from diagnostics. It writes
91
- one line per request to **stdout** by design, is off by default
92
- (`log: false`), and is intentionally not routed through the logger
93
- injection hook — access logs and diagnostic warnings have different
94
- audiences and different destinations. This split is intentional and is
95
- not a contract violation.
96
-
97
- ## Admin mode contract
98
-
99
- When `admin` is opt-in, the following defaults are part of the
100
- public contract — changing any default is **MAJOR**:
101
-
102
- - `enabled: false`
103
- - `path: '/databehave'`
104
- - `bind: 'loopback-only'`
105
- - `allowDestroy: true`
106
- - `cors: 'auto'`
107
-
108
- Additional rules:
109
-
110
- - **`x-mock-*` header semantics.** Adding a new header is **MINOR**.
111
- Removing or renaming a header, or changing the ErrorMode kind a
112
- header resolves to, is **MAJOR**.
113
- - **Sticky-override resolution priority.** The order
114
- `header > exact > path > global > existing mockMode > passthrough`
115
- is part of the contract — changes are **MAJOR**.
116
- - **Scenarios file format.** Adding new top-level fields to a saved
117
- scenario JSON is **MINOR**. Changing the directory layout, the file
118
- naming, or the meaning of an existing field is **MAJOR**.