@crvouga/mockingbird-service-posthog 0.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/CHANGELOG.md +5 -0
- package/README.md +169 -0
- package/dist/chunk-A34UTVQR.js +356 -0
- package/dist/chunk-A34UTVQR.js.map +7 -0
- package/dist/chunk-KO6LMI55.js +2945 -0
- package/dist/chunk-KO6LMI55.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +1038 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1319 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/package.json +91 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-posthog
|
|
2
|
+
|
|
3
|
+
Stateful mock of **PostHog** for test suites: remote feature-flag evaluation (`/flags` v2 and
|
|
4
|
+
the legacy `/decide` shape), remote config, event capture (`/batch/`, `/e/`, `/i/v0/e/`),
|
|
5
|
+
session-recording intake, the posthog-js asset and survey endpoints, and the slice of the
|
|
6
|
+
management API our tooling and crons call (feature-flag list/create/patch, HogQL). Every flag
|
|
7
|
+
is set per test through admin routes, so paths our in-app overrides cannot reach (strict
|
|
8
|
+
booleans, `getConfig` payloads, EMR server gates, member-app variants) become controllable, and
|
|
9
|
+
a stack run never reaches `us.i.posthog.com`.
|
|
10
|
+
|
|
11
|
+
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/posthog/SUPPORT.md)
|
|
12
|
+
- The contract (`openapi.yaml`) is hand-authored from the wire shapes of posthog-node 5.52.2,
|
|
13
|
+
`@posthog/core` 1.54.0 (posthog-react-native 4.72.1's base) and posthog-js 1.433.2, and from
|
|
14
|
+
our own raw fetches. All three SDKs are proven against it (`posthog.sdk.test.ts`).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -D @crvouga/mockingbird-service-posthog
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
|
|
23
|
+
`npx mockingbird-posthog serve`, `createServer` from `./server` (Node), or `createRuntime` with
|
|
24
|
+
any Fetch server.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Point the app's PostHog host at the mock (port 8795 by default):
|
|
29
|
+
|
|
30
|
+
| App | Variables |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| backend (flags, capture, website purchase sink) | `POSTHOG_HOST`, `POSTHOG_API_KEY` |
|
|
33
|
+
| EMR backend | `POSTHOG_HOST`, `POSTHOG_API_KEY` |
|
|
34
|
+
| EMR frontend (browser posthog-js and the server raw fetch) | `NEXT_PUBLIC_POSTHOG_HOST`, `NEXT_PUBLIC_POSTHOG_KEY` |
|
|
35
|
+
| member app | `public-config.json[stage]` host, or `?POSTHOG_HOST=&POSTHOG_API_KEY=` on web (G-P1) |
|
|
36
|
+
| makor supplement-management | `POSTHOG_HOST` |
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx mockingbird-posthog serve --port 8795 --import-flags dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createRuntime } from "@crvouga/mockingbird-service-posthog"
|
|
44
|
+
|
|
45
|
+
const posthog = createRuntime()
|
|
46
|
+
const admin = (path: string, body: unknown, method = "PUT") =>
|
|
47
|
+
posthog.fetch(
|
|
48
|
+
new Request(`http://posthog.test/__admin${path}`, { method, body: JSON.stringify(body) }),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// Per-worker namespaces: map each worker's project token (the SDKs cannot add headers)…
|
|
52
|
+
await admin("/credentials", { credentials: { phc_worker1: "w1" } })
|
|
53
|
+
// …then set flags in that namespace.
|
|
54
|
+
await admin("/flags/shop-coupons?namespace=w1", { default: true })
|
|
55
|
+
await admin("/flags/rx-category-intake?namespace=w1", {
|
|
56
|
+
default: true,
|
|
57
|
+
payload: { categories: ["trt"] },
|
|
58
|
+
overrides: [{ distinct_id: "42", value: false }, { email: "qa@example.test", value: "beta" }],
|
|
59
|
+
})
|
|
60
|
+
await admin("/flags/mobile-smart-links?namespace=w1", undefined, "DELETE") // absent again
|
|
61
|
+
|
|
62
|
+
// The app's posthog-node `getFeatureFlag("shop-coupons", "7")` now answers true.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Routes
|
|
66
|
+
|
|
67
|
+
| Route | Behaviour |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `POST /flags/?v=2` (`&config=true`) | posthog-node, posthog-react-native, posthog-js. Body `{token, distinct_id, person_properties?, groups?, flag_keys_to_evaluate?, …}`. Answers `{flags: {<key>: {key, enabled, variant, reason, metadata: {id, version, description, payload?}}}, errorsWhileComputingFlags, requestId, evaluatedAt}`; `payload` is the JSON **string** and only accompanies an enabled flag. `config=true` adds the remote-config fields. |
|
|
70
|
+
| `POST /flags?v=2` | Same route without the trailing slash: the EMR frontend's raw fetch, body `{api_key, distinct_id, person_properties?}`. |
|
|
71
|
+
| `POST /flags/` (no `v`, or `v=1`), `POST /decide/?v=3` | The legacy shape `{featureFlags: {k: bool \| variant}, featureFlagPayloads: {k: "<json>"}, errorsWhileComputingFlags, config, …}` (makor). `/decide/?v=4` answers the v2 shape. |
|
|
72
|
+
| `GET /array/{token}/config`, `…/config.js` | Remote config: `{supportedCompression: ["gzip","gzip-js"], hasFeatureFlags: true, analytics: {endpoint: "/i/v0/e/"}, sessionRecording: false \| {endpoint: "/s/"}, surveys: false, …}`; the `.js` form sets `window._POSTHOG_REMOTE_CONFIG[token]`. `hasFeatureFlags` is always true (false makes posthog-react-native skip flag loading). |
|
|
73
|
+
| `POST /batch/` | `{api_key, batch: [{event, distinct_id, properties, timestamp, uuid}], sent_at}`, usually `Content-Encoding: gzip`. |
|
|
74
|
+
| `POST /e/`, `POST /i/v0/e/` | One event, an array of events, or `{api_key, batch}`; bodies may be raw gzip (`gzip-js`), base64 `data=` forms (`compression=base64`), or JSON. Events with a known `uuid` are deduplicated. |
|
|
75
|
+
| `POST /s/` | Session recordings: counted (`GET /__admin/recordings`), never stored. |
|
|
76
|
+
| `GET /static/recorder.js`, `/static/{ver}/recorder.js` | An inert script. |
|
|
77
|
+
| `GET /api/surveys/?token=`, `/api/web_experiments/?token=` | `{surveys: []}`, `{experiments: []}`. |
|
|
78
|
+
| `GET/POST /api/projects/{id}/feature_flags/`, `PATCH …/{flagId}/` | Bearer personal key required. List pages `{count, next, previous, results}` (`limit`/`offset`, what feature-flags-cli follows); flags carry `filters.groups` / `multivariate` / `payloads` derived from the flag model, and create/patch parse `filters` back (see below). `flagId` is the numeric id or the key. |
|
|
79
|
+
| `POST /api/projects/{id}/query/` | HogQL `{query: {kind: "HogQLQuery", query}}` → `{results, columns}` from canned answers (`PUT /__admin/settings {"queryResults": […]}`), `{results: []}` otherwise. |
|
|
80
|
+
|
|
81
|
+
Every route answers with and without its trailing slash. Errors use PostHog's
|
|
82
|
+
`{type, code, detail, attr}` body: 401 `invalid_api_key` without a project token, 400
|
|
83
|
+
`missing_distinct_id`, 400 `invalid_payload` for an undecodable body, 404 `not_found`.
|
|
84
|
+
|
|
85
|
+
### Flag semantics
|
|
86
|
+
|
|
87
|
+
- **Absent vs `false`.** A flag whose `default` is omitted (or `null`) is not returned at all
|
|
88
|
+
unless an override matches; `default: false` returns `enabled: false`. The member app's
|
|
89
|
+
`hasFlag` falls through to static defaults only for the absent case. Inactive or deleted
|
|
90
|
+
flags are never returned (as in PostHog).
|
|
91
|
+
- **Targeting:** overrides in order, by exact `distinct_id` or by `person_properties.email`
|
|
92
|
+
(case-insensitive), then the default.
|
|
93
|
+
- **Variants:** a string value is a variant (`enabled: true, variant: "<key>"`); our backend,
|
|
94
|
+
EMR and makor clients all read it as `true`.
|
|
95
|
+
- **Payloads:** any JSON; stored and sent as the JSON string PostHog uses. The backend's
|
|
96
|
+
`getConfig` only accepts object payloads.
|
|
97
|
+
- **Management `filters` → model:** `distinct_id` / `email` property groups become overrides;
|
|
98
|
+
a property-less group at 100 % is the default (its `variant`, else the largest multivariate
|
|
99
|
+
variant, else `true`); at 0 % or a partial rollout it is `false`; cohort and other property
|
|
100
|
+
groups are ignored (deterministic, no hashing).
|
|
101
|
+
|
|
102
|
+
### Admin (beyond the standard contract)
|
|
103
|
+
|
|
104
|
+
| Route | Effect |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `PUT /__admin/flags/:key` | `{default?: bool \| "variant" \| null, payload?: any, overrides?: [{distinct_id? \| email?, value?, payload?}], active?, name?}`. Omitting `default` makes the flag absent for everyone no override names. Bumps `metadata.version`. |
|
|
107
|
+
| `DELETE /__admin/flags/:key` | Remove the flag (absent). |
|
|
108
|
+
| `PUT /__admin/flags` | Bulk: `{flags: {<key>: spec}, replace?: true}` or `[{key, …spec}]`. |
|
|
109
|
+
| `GET /__admin/flags`, `GET /__admin/flags/:key` | The namespace's flags. |
|
|
110
|
+
| `GET /__admin/flags/evaluate?distinct_id=&email=` | What `/flags` answers for that subject, as `{flags: {key: value}}`. |
|
|
111
|
+
| `POST /__admin/flags/import` | `{from: "state.json", env: "dev" \| "prod", project?: "member-app" \| "emr", replace?}` seeds from the bundled copy of geviti `docs/feature-flags/state.json` (or pass `state: {flags: […]}` inline). `live` → `true` (or the largest variant), `rollout 0` / `targeted` / `ramping` → `false`, `inactive` / `missing` → absent. |
|
|
112
|
+
| `POST /__admin/flags/bump` | Changes nothing server-side; returns a `generation` counter. The documented moment to clear the app's flag caches (backend `getAllFlagsAndPayloads` 60 s per user; EMR frontend server 60 s / 10 s). |
|
|
113
|
+
| `GET /__admin/events?distinct_id=&event=&since=` | Captured events, oldest first (`since`: epoch ms or ISO, mock clock). `$exception` keeps only `$lib`, `$lib_version`, `$exception_level`, `$session_id`; properties named like message/body/text/content/prompt/stack/trace/html/comment/note are dropped from every event (and from `$set`). |
|
|
114
|
+
| `GET /__admin/recordings` | `{count}` of `/s/` posts. |
|
|
115
|
+
| `GET/PUT /__admin/settings` | `{sessionRecording?: bool, queryResults?: [{match?, columns?, results}]}`. |
|
|
116
|
+
|
|
117
|
+
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; `GET /__admin/faults/presets`),
|
|
118
|
+
each on `/flags` and `/decide`: `flags_5xx`, `flags_429` (`retry-after: 1`), `flags_hang`
|
|
119
|
+
(1.5 s, past the backend strict and EMR 1 s races), `errors_while_computing`
|
|
120
|
+
(`errorsWhileComputingFlags: true`, flags still present), `quota_limited`
|
|
121
|
+
(`quotaLimited: ["feature_flags"]`, no flags). Plus `capture_5xx` on the capture endpoints.
|
|
122
|
+
Each preset adds one rule per route, so `count` applies per route.
|
|
123
|
+
|
|
124
|
+
### Namespaces
|
|
125
|
+
|
|
126
|
+
PostHog SDKs cannot add headers. Choose a namespace by:
|
|
127
|
+
|
|
128
|
+
- **host prefix** (primary): `POSTHOG_HOST=http://127.0.0.1:8795/ns/w1`. Every SDK builds
|
|
129
|
+
`${host}/path`, so `/ns/w1/flags/?v=2` selects `w1`.
|
|
130
|
+
- **project token**: `PUT /__admin/credentials {"credentials": {"phc_…": "w1"}}`. The token is
|
|
131
|
+
read from `/array/{token}/…`, `?token=`, the body (`token`, `api_key`, or a batch's first
|
|
132
|
+
event's `properties.token`, after decoding gzip/base64), or a personal key's
|
|
133
|
+
`Authorization: Bearer` on the management API.
|
|
134
|
+
- `x-mockingbird-namespace`, for raw clients.
|
|
135
|
+
|
|
136
|
+
The management API's `next` page URL is built from the request without the `/ns/` prefix; page
|
|
137
|
+
through it with a credential-mapped personal key instead.
|
|
138
|
+
|
|
139
|
+
### Deliberately not modelled
|
|
140
|
+
|
|
141
|
+
- Percentage rollouts, cohorts, group (organisation) targeting and local evaluation
|
|
142
|
+
(`/api/feature_flag/local_evaluation`): flags evaluate deterministically from explicit
|
|
143
|
+
overrides and a default.
|
|
144
|
+
- Session-recording content, heatmaps, surveys and web experiments (the endpoints answer empty).
|
|
145
|
+
- HogQL execution: queries answer canned results.
|
|
146
|
+
- Person profiles: `$identify` / `$set` events are stored for assertions but do not feed
|
|
147
|
+
targeting; send `person_properties` on `/flags` as the SDKs do.
|
|
148
|
+
- Project isolation by project id on the management API: one flag set per namespace.
|
|
149
|
+
- Real remote-config fields beyond what the SDKs read.
|
|
150
|
+
|
|
151
|
+
## API
|
|
152
|
+
|
|
153
|
+
| Export | Kind | Description |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `PostHogAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `evaluate(subject, keys?)`, `events(query?)`, `flagList()`, `state`. Options: `sqlite`, `now`, `namespace`, `flags`, `settings`. |
|
|
156
|
+
| `createRuntime` | function | The mock with the full service contract (health, admin, namespaces by prefix/token/header, presets, journal). Options: `flags`, `settings`, `clock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
|
|
157
|
+
| `POSTHOG_PRESETS` | object | Every named fault preset. |
|
|
158
|
+
| `POSTHOG_NAMESPACE` | string | The service name, `"posthog"`. |
|
|
159
|
+
| `evaluateFlag` | function | Evaluate one flag record for `{distinct_id, person_properties}` (`undefined` = absent). |
|
|
160
|
+
| `parseFlagSpec` | function | Validate an admin flag body into a `FlagSpec`. |
|
|
161
|
+
| `payloadString` | function | A payload as PostHog stores it (JSON string, or `null`). |
|
|
162
|
+
| `specsFromState`, `valueForState` | functions | Map a `state.json` file (or one flag state) to flag specs. |
|
|
163
|
+
| `GEVITI_FLAG_STATE` | object | The bundled, trimmed copy of geviti `docs/feature-flags/state.json`. |
|
|
164
|
+
| `decodePostHogBody`, `tokenFromBody` | functions | Undo PostHog body envelopes (gzip, gzip-js, base64 `data=`); find the project token in a body. |
|
|
165
|
+
| `scrubProperties` | function | The event-property scrubbing applied before storage. |
|
|
166
|
+
| `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
|
|
167
|
+
| `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--import-flags dev\|prod`, `--session-recording`); port 8795. |
|
|
168
|
+
|
|
169
|
+
Part of [mockingbird](https://github.com/crvouga/mockingbird).
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GEVITI_FLAG_STATE,
|
|
3
|
+
createRuntime,
|
|
4
|
+
specsFromState
|
|
5
|
+
} from "./chunk-KO6LMI55.js";
|
|
6
|
+
|
|
7
|
+
// ../../adapters/node/dist/cli.js
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
|
|
11
|
+
// ../../adapters/node/dist/serve.js
|
|
12
|
+
import { createServer } from "node:http";
|
|
13
|
+
var serve = async (api, options = {}) => {
|
|
14
|
+
const server = createServer(async (req, res) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
for await (const chunk of req) {
|
|
17
|
+
chunks.push(Buffer.from(chunk));
|
|
18
|
+
}
|
|
19
|
+
const body = Buffer.concat(chunks);
|
|
20
|
+
const address = server.address();
|
|
21
|
+
const port = typeof address === "object" && address !== null ? address.port : void 0;
|
|
22
|
+
const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
|
|
23
|
+
const raw = req.url ?? "/";
|
|
24
|
+
const url = new URL(raw.replace(/^\/+/, "/"), base);
|
|
25
|
+
const method = req.method ?? "GET";
|
|
26
|
+
const init = { method, headers: req.headers };
|
|
27
|
+
if (method !== "GET" && method !== "HEAD" && body.length > 0) {
|
|
28
|
+
init.body = body;
|
|
29
|
+
}
|
|
30
|
+
const aborted = new AbortController();
|
|
31
|
+
res.once("close", () => {
|
|
32
|
+
if (!res.writableFinished)
|
|
33
|
+
aborted.abort();
|
|
34
|
+
});
|
|
35
|
+
init.signal = aborted.signal;
|
|
36
|
+
const request = new Request(url, init);
|
|
37
|
+
let response;
|
|
38
|
+
try {
|
|
39
|
+
response = await api.fetch(request);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code === "MOCKINGBIRD_DROP") {
|
|
42
|
+
req.socket.destroy();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
46
|
+
res.end(JSON.stringify({
|
|
47
|
+
error: {
|
|
48
|
+
type: "mockingbird_internal",
|
|
49
|
+
message: error instanceof Error ? error.message : String(error)
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const headers = Object.fromEntries(response.headers);
|
|
55
|
+
const cookies = response.headers.getSetCookie();
|
|
56
|
+
if (cookies.length > 0)
|
|
57
|
+
headers["set-cookie"] = cookies;
|
|
58
|
+
if (!response.body) {
|
|
59
|
+
res.writeHead(response.status, headers);
|
|
60
|
+
res.end();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.writeHead(response.status, headers);
|
|
64
|
+
res.flushHeaders();
|
|
65
|
+
const reader = response.body.getReader();
|
|
66
|
+
try {
|
|
67
|
+
for (; ; ) {
|
|
68
|
+
const { done, value } = await reader.read();
|
|
69
|
+
if (done)
|
|
70
|
+
break;
|
|
71
|
+
if (!res.write(value))
|
|
72
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
73
|
+
}
|
|
74
|
+
res.end();
|
|
75
|
+
} catch {
|
|
76
|
+
res.destroy();
|
|
77
|
+
} finally {
|
|
78
|
+
reader.releaseLock();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
await new Promise((resolve, reject) => {
|
|
82
|
+
server.once("error", reject);
|
|
83
|
+
server.listen(options.port ?? 0, options.host, resolve);
|
|
84
|
+
});
|
|
85
|
+
return server;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ../../adapters/node/dist/listen.js
|
|
89
|
+
var listen = async (api, options = {}) => {
|
|
90
|
+
const host = options.host ?? "127.0.0.1";
|
|
91
|
+
const server = await serve(api, { port: options.port ?? 0, host });
|
|
92
|
+
const address = server.address();
|
|
93
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
|
|
94
|
+
const shown = host.includes(":") ? `[${host}]` : host;
|
|
95
|
+
return {
|
|
96
|
+
url: `http://${shown}:${port}`,
|
|
97
|
+
port,
|
|
98
|
+
host,
|
|
99
|
+
server,
|
|
100
|
+
close: () => new Promise((resolve, reject) => {
|
|
101
|
+
server.close((error) => {
|
|
102
|
+
if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
|
|
103
|
+
reject(error);
|
|
104
|
+
else
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
server.closeAllConnections?.();
|
|
108
|
+
})
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// ../../adapters/node/dist/cli.js
|
|
113
|
+
var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
|
|
114
|
+
const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
|
|
115
|
+
const fallback = option.default !== void 0 ? ` (default: ${String(option.default)})` : "";
|
|
116
|
+
return ` ${flag.padEnd(30)} ${option.description}${fallback}`;
|
|
117
|
+
});
|
|
118
|
+
var help = (spec) => [
|
|
119
|
+
`${spec.bin} \u2014 ${spec.description}`,
|
|
120
|
+
"",
|
|
121
|
+
"Usage:",
|
|
122
|
+
` ${spec.bin} <command> [options]`,
|
|
123
|
+
"",
|
|
124
|
+
"Commands:",
|
|
125
|
+
...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),
|
|
126
|
+
"",
|
|
127
|
+
`Run \`${spec.bin} <command> --help\` for a command's options.`
|
|
128
|
+
].join("\n");
|
|
129
|
+
var commandHelp = (spec, name, command) => [
|
|
130
|
+
`${spec.bin} ${name} \u2014 ${command.summary}`,
|
|
131
|
+
"",
|
|
132
|
+
"Usage:",
|
|
133
|
+
` ${command.usage ?? `${spec.bin} ${name} [options]`}`,
|
|
134
|
+
...command.options ? ["", "Options:", ...optionHelp(command.options)] : []
|
|
135
|
+
].join("\n");
|
|
136
|
+
var runCli = async (spec, argv) => {
|
|
137
|
+
const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : void 0;
|
|
138
|
+
const words = pair !== void 0 && spec.commands[pair] ? 2 : 1;
|
|
139
|
+
const name = words === 2 ? pair : argv[0];
|
|
140
|
+
const rest = argv.slice(words);
|
|
141
|
+
if (name === void 0 || name === "--help" || name === "-h" || name === "help") {
|
|
142
|
+
console.log(help(spec));
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
const command = spec.commands[name];
|
|
146
|
+
if (!command) {
|
|
147
|
+
console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}
|
|
148
|
+
|
|
149
|
+
${help(spec)}`);
|
|
150
|
+
return 2;
|
|
151
|
+
}
|
|
152
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
153
|
+
console.log(commandHelp(spec, name, command));
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
let parsed;
|
|
157
|
+
try {
|
|
158
|
+
parsed = parseArgs({
|
|
159
|
+
args: rest,
|
|
160
|
+
allowPositionals: true,
|
|
161
|
+
strict: true,
|
|
162
|
+
options: Object.fromEntries(Object.entries(command.options ?? {}).map(([key, option]) => [
|
|
163
|
+
key,
|
|
164
|
+
{
|
|
165
|
+
type: option.type,
|
|
166
|
+
...option.default !== void 0 ? { default: option.default } : {}
|
|
167
|
+
}
|
|
168
|
+
]))
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(`${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}
|
|
172
|
+
|
|
173
|
+
${commandHelp(spec, name, command)}`);
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
return command.run(parsed.values, parsed.positionals);
|
|
177
|
+
};
|
|
178
|
+
var COMMON_SERVE_OPTIONS = {
|
|
179
|
+
port: { type: "string", value: "<port>", description: "Port to listen on" },
|
|
180
|
+
host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
|
|
181
|
+
"admin-key": {
|
|
182
|
+
type: "string",
|
|
183
|
+
value: "<key>",
|
|
184
|
+
description: "Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)"
|
|
185
|
+
},
|
|
186
|
+
seed: { type: "string", value: "<seed>", description: "Seed for every random choice" },
|
|
187
|
+
log: {
|
|
188
|
+
type: "string",
|
|
189
|
+
value: "<pretty|json|off>",
|
|
190
|
+
description: "Request log format",
|
|
191
|
+
default: "pretty"
|
|
192
|
+
},
|
|
193
|
+
"log-requests": {
|
|
194
|
+
type: "boolean",
|
|
195
|
+
description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
|
|
196
|
+
},
|
|
197
|
+
config: {
|
|
198
|
+
type: "string",
|
|
199
|
+
value: "<file>",
|
|
200
|
+
description: "Serve every service in a mockingbird.json config instead"
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var formatLog = (format) => {
|
|
204
|
+
if (format === "off")
|
|
205
|
+
return void 0;
|
|
206
|
+
if (format === "json")
|
|
207
|
+
return (entry) => console.log(JSON.stringify(entry));
|
|
208
|
+
return (entry) => {
|
|
209
|
+
const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
|
|
210
|
+
const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
|
|
211
|
+
const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
|
|
212
|
+
const adopted = entry.adopted ? " adopted" : "";
|
|
213
|
+
console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}`);
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
217
|
+
var loadTarget = async (name, own) => {
|
|
218
|
+
if (name === own.name)
|
|
219
|
+
return own;
|
|
220
|
+
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
221
|
+
try {
|
|
222
|
+
const mod = await import(specifier);
|
|
223
|
+
if (!mod.serveTarget)
|
|
224
|
+
throw new Error(`${specifier} exports no serveTarget`);
|
|
225
|
+
return mod.serveTarget;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
228
|
+
throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var start = async (target, values, config) => {
|
|
232
|
+
const runtime = await target.create(values, {
|
|
233
|
+
adminKey: config.adminKey,
|
|
234
|
+
seed: config.seed,
|
|
235
|
+
onLog: formatLog(config.log)
|
|
236
|
+
});
|
|
237
|
+
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
238
|
+
console.log(`${target.name} mock listening on ${listening.url}`);
|
|
239
|
+
console.log(`${target.name} health: GET ${listening.url}/health`);
|
|
240
|
+
console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
|
|
241
|
+
for (const line of target.banner?.(runtime) ?? [])
|
|
242
|
+
console.log(`${target.name} ${line}`);
|
|
243
|
+
return listening;
|
|
244
|
+
};
|
|
245
|
+
var untilSignal = async (servers) => new Promise((resolve) => {
|
|
246
|
+
const stop = async () => {
|
|
247
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
248
|
+
resolve(0);
|
|
249
|
+
};
|
|
250
|
+
process.once("SIGINT", stop);
|
|
251
|
+
process.once("SIGTERM", stop);
|
|
252
|
+
});
|
|
253
|
+
var serveCommand = (target) => ({
|
|
254
|
+
summary: `Serve the ${target.name} mock over HTTP`,
|
|
255
|
+
options: { ...COMMON_SERVE_OPTIONS, ...target.options },
|
|
256
|
+
async run(values) {
|
|
257
|
+
const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
|
|
258
|
+
if (!["pretty", "json", "off"].includes(log)) {
|
|
259
|
+
console.error(`--log must be pretty, json or off (got ${log})`);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
const configPath = asString(values.config);
|
|
263
|
+
if (configPath !== void 0) {
|
|
264
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
265
|
+
const servers = [];
|
|
266
|
+
try {
|
|
267
|
+
for (const [name, entry] of Object.entries(config.services ?? {})) {
|
|
268
|
+
const each = await loadTarget(name, target);
|
|
269
|
+
servers.push(await start(each, entry.options ?? {}, {
|
|
270
|
+
port: entry.port ?? each.defaultPort,
|
|
271
|
+
host: entry.host ?? "127.0.0.1",
|
|
272
|
+
...entry.adminKey !== void 0 ? { adminKey: entry.adminKey } : {},
|
|
273
|
+
...entry.seed !== void 0 ? { seed: entry.seed } : {},
|
|
274
|
+
log: config.log ?? log
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
279
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
return untilSignal(servers);
|
|
283
|
+
}
|
|
284
|
+
const port = asString(values.port);
|
|
285
|
+
const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
286
|
+
const seed = asString(values.seed);
|
|
287
|
+
let listening;
|
|
288
|
+
try {
|
|
289
|
+
listening = await start(target, values, {
|
|
290
|
+
port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
|
|
291
|
+
host: asString(values.host) ?? "127.0.0.1",
|
|
292
|
+
...adminKey !== void 0 ? { adminKey } : {},
|
|
293
|
+
...seed !== void 0 ? { seed } : {},
|
|
294
|
+
log
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
return untilSignal([listening]);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// src/server.ts
|
|
305
|
+
var DEFAULT_PORT = 8795;
|
|
306
|
+
var createServer2 = async (options = {}) => {
|
|
307
|
+
const { port, host, ...rest } = options;
|
|
308
|
+
const runtime = createRuntime(rest);
|
|
309
|
+
const listening = await listen(runtime, {
|
|
310
|
+
port: port ?? 0,
|
|
311
|
+
...host !== void 0 ? { host } : {}
|
|
312
|
+
});
|
|
313
|
+
return { ...listening, runtime };
|
|
314
|
+
};
|
|
315
|
+
var text = (value) => typeof value === "string" ? value : void 0;
|
|
316
|
+
var serveTarget = {
|
|
317
|
+
name: "posthog",
|
|
318
|
+
defaultPort: DEFAULT_PORT,
|
|
319
|
+
options: {
|
|
320
|
+
"import-flags": {
|
|
321
|
+
type: "string",
|
|
322
|
+
value: "<dev|prod>",
|
|
323
|
+
description: "Seed every namespace from the bundled geviti docs/feature-flags/state.json (member-app project)"
|
|
324
|
+
},
|
|
325
|
+
"session-recording": {
|
|
326
|
+
type: "boolean",
|
|
327
|
+
description: "Advertise session recording ({endpoint: /s/}) in remote config"
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
create: (values, common) => {
|
|
331
|
+
const env = text(values["import-flags"]);
|
|
332
|
+
if (env !== void 0 && env !== "dev" && env !== "prod") {
|
|
333
|
+
throw new Error('--import-flags must be "dev" or "prod"');
|
|
334
|
+
}
|
|
335
|
+
return createRuntime({
|
|
336
|
+
...env ? { flags: specsFromState(GEVITI_FLAG_STATE, { env }) } : {},
|
|
337
|
+
...values["session-recording"] === true ? { settings: { sessionRecording: true } } : {},
|
|
338
|
+
...common.adminKey !== void 0 ? { adminKey: common.adminKey } : {},
|
|
339
|
+
...common.seed !== void 0 ? { seed: common.seed } : {},
|
|
340
|
+
...common.onLog ? { onLog: common.onLog } : {}
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
banner: () => [
|
|
344
|
+
"point POSTHOG_HOST / NEXT_PUBLIC_POSTHOG_HOST here; set flags with PUT /__admin/flags/<key>",
|
|
345
|
+
"namespaces: /ns/<name> host prefix, x-mockingbird-namespace, or PUT /__admin/credentials {<phc_token>: <ns>}"
|
|
346
|
+
]
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
export {
|
|
350
|
+
runCli,
|
|
351
|
+
serveCommand,
|
|
352
|
+
DEFAULT_PORT,
|
|
353
|
+
createServer2 as createServer,
|
|
354
|
+
serveTarget
|
|
355
|
+
};
|
|
356
|
+
//# sourceMappingURL=chunk-A34UTVQR.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts", "../src/server.ts"],
|
|
4
|
+
"sourcesContent": ["import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n", "/// <reference types=\"node\" />\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { GEVITI_FLAG_STATE } from \"./flag-state-fixture.js\"\nimport { specsFromState } from \"./import.js\"\nimport { createRuntime, type PostHogRuntime, type PostHogRuntimeOptions } from \"./runtime.js\"\n\n/** Port `mockingbird-posthog serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8795\n\nexport type PostHogServerOptions = PostHogRuntimeOptions & {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`. */\n host?: string\n}\n\nexport type PostHogServer = Listening & { runtime: PostHogRuntime }\n\n/** Serve the PostHog mock over `node:http`. */\nexport const createServer = async (options: PostHogServerOptions = {}): Promise<PostHogServer> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime(rest)\n const listening = await listen(runtime, {\n port: port ?? 0,\n ...(host !== undefined ? { host } : {}),\n })\n return { ...listening, runtime }\n}\n\nconst text = (value: string | boolean | undefined) =>\n typeof value === \"string\" ? value : undefined\n\n/** How `serve` (and `serve --config`) builds the PostHog mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"posthog\",\n defaultPort: DEFAULT_PORT,\n options: {\n \"import-flags\": {\n type: \"string\",\n value: \"<dev|prod>\",\n description:\n \"Seed every namespace from the bundled geviti docs/feature-flags/state.json (member-app project)\",\n },\n \"session-recording\": {\n type: \"boolean\",\n description: \"Advertise session recording ({endpoint: /s/}) in remote config\",\n },\n },\n create: (values, common) => {\n const env = text(values[\"import-flags\"])\n if (env !== undefined && env !== \"dev\" && env !== \"prod\") {\n throw new Error('--import-flags must be \"dev\" or \"prod\"')\n }\n return createRuntime({\n ...(env ? { flags: specsFromState(GEVITI_FLAG_STATE, { env }) } : {}),\n ...(values[\"session-recording\"] === true ? { settings: { sessionRecording: true } } : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.onLog ? { onLog: common.onLog } : {}),\n })\n },\n banner: () => [\n \"point POSTHOG_HOST / NEXT_PUBLIC_POSTHOG_HOST here; set flags with PUT /__admin/flags/<key>\",\n \"namespaces: /ns/<name> host prefix, x-mockingbird-namespace, or PUT /__admin/credentials {<phc_token>: <ns>}\",\n ],\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createServer"]
|
|
7
|
+
}
|