@crvouga/mockingbird-service-google-calendar 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 +165 -0
- package/dist/chunk-S2OLMZ66.js +360 -0
- package/dist/chunk-S2OLMZ66.js.map +7 -0
- package/dist/chunk-UAEYK3SE.js +3567 -0
- package/dist/chunk-UAEYK3SE.js.map +7 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +986 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +7 -0
- package/dist/server.d.ts +1304 -0
- package/dist/server.js +12 -0
- package/dist/server.js.map +7 -0
- package/package.json +90 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-google-calendar
|
|
2
|
+
|
|
3
|
+
Stateful mock of **Google Calendar v3** and **Google OAuth 2.0** for test suites, covering what
|
|
4
|
+
our EMR backend calls: `calendarList.list`, `calendars.insert`, `events.list` (time windows,
|
|
5
|
+
`q`, `orderBy`, paging, incremental sync with 410 `fullSyncRequired`), `events.get/insert/
|
|
6
|
+
update/delete`, `events.watch` with real push notifications to the channel's address, and
|
|
7
|
+
`channels.stop`; plus the token endpoint (authorization code and refresh grants), revoke, and
|
|
8
|
+
OpenID userinfo. Practitioners connect, sync EMR appointments and receive calendar pushes
|
|
9
|
+
without a Google account.
|
|
10
|
+
|
|
11
|
+
- Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/google-calendar/SUPPORT.md)
|
|
12
|
+
- The contract (`openapi.yaml`) is trimmed from Google's Calendar v3 discovery document and
|
|
13
|
+
OAuth docs to what `google-calendar-service.ts`, `authorize.business.ts` and the webhook
|
|
14
|
+
controller use.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -D @crvouga/mockingbird-service-google-calendar
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
|
|
23
|
+
`npx mockingbird-google-calendar serve`, `createServer` from `./server` (Node), or
|
|
24
|
+
`createRuntime` with any Fetch server.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Seams (the app hardcodes Google today): give the calendar client a `rootUrl`
|
|
29
|
+
(`google.calendar({version: "v3", auth, rootUrl: "<mock>/"})`), give `OAuth2Client`
|
|
30
|
+
`endpoints: {oauth2TokenUrl: "<mock>/token", oauth2RevokeUrl: "<mock>/revoke"}`, and point the
|
|
31
|
+
userinfo fetch at `<mock>/oauth2/v3/userinfo`. `GOOGLE_WEBHOOK_URL` can stay as it is: the mock
|
|
32
|
+
posts pushes to whatever `address` the app registers (http is allowed unless
|
|
33
|
+
`--require-https-webhooks`).
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx mockingbird-google-calendar serve --port 8820 --client "$GOOGLE_CLIENT_ID:$GOOGLE_CLIENT_SECRET"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { auth, calendar } from "@googleapis/calendar"
|
|
41
|
+
import { OAuth2Client } from "google-auth-library"
|
|
42
|
+
import { createServer } from "@crvouga/mockingbird-service-google-calendar/server"
|
|
43
|
+
|
|
44
|
+
const google = await createServer()
|
|
45
|
+
const oauth = new OAuth2Client({
|
|
46
|
+
clientId: "c",
|
|
47
|
+
clientSecret: "s",
|
|
48
|
+
redirectUri: "postmessage",
|
|
49
|
+
endpoints: { oauth2TokenUrl: `${google.url}/token`, oauth2RevokeUrl: `${google.url}/revoke` },
|
|
50
|
+
})
|
|
51
|
+
// Authorization code "4/mock-<name>" signs in <name>@example.com ("4/mock-<email>" for others).
|
|
52
|
+
const { tokens } = await oauth.getToken("4/mock-dr.house")
|
|
53
|
+
const client = new auth.OAuth2()
|
|
54
|
+
client.setCredentials({ access_token: tokens.access_token ?? null })
|
|
55
|
+
const cal = calendar({ version: "v3", auth: client, rootUrl: `${google.url}/` })
|
|
56
|
+
await cal.events.insert({
|
|
57
|
+
calendarId: "primary",
|
|
58
|
+
requestBody: { summary: "Rounds", start: { dateTime: "2026-10-01T15:00:00Z" }, end: { dateTime: "2026-10-01T16:00:00Z" } },
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
An event created "in Google's UI" (admin route) is what the app then lists and syncs:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { createServer } from "@crvouga/mockingbird-service-google-calendar/server"
|
|
66
|
+
|
|
67
|
+
const google = await createServer()
|
|
68
|
+
await fetch(`${google.url}/__admin/events`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "content-type": "application/json" },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
email: "dr.house@example.com",
|
|
73
|
+
event: { summary: "Rounds", start: { dateTime: "2026-10-01T15:00:00Z" }, end: { dateTime: "2026-10-01T16:00:00Z" } },
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
const stored = await (await fetch(`${google.url}/__admin/events?email=dr.house@example.com`)).json()
|
|
77
|
+
await google.close()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Routes
|
|
81
|
+
|
|
82
|
+
| Route | Behaviour |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `POST /token` | Form-encoded (`client_secret_post` or Basic). `authorization_code`: `4/mock-<name or email>` → `{access_token, expires_in: 3599, refresh_token, scope, token_type, id_token}`; codes are single-use; anything else is 400 `invalid_grant`. `refresh_token` → a new access token (+ `id_token`); revoked or foreign tokens are `invalid_grant`. Unknown client (with `clients` set) 401 `invalid_client`. |
|
|
85
|
+
| `POST /revoke?token=` | Revokes an access or refresh token; unknown / already revoked 400 `invalid_token`. |
|
|
86
|
+
| `GET /oauth2/v3/userinfo` | `{sub, email, email_verified, name, given_name, family_name, picture}` (derived from the email, or set with `PUT /__admin/users/:email`). |
|
|
87
|
+
| `GET /calendar/v3/users/me/calendarList` | The account's primary calendar (id = email) and its secondary calendars. |
|
|
88
|
+
| `POST /calendar/v3/calendars` | Create a secondary calendar (`…@group.calendar.google.com`). |
|
|
89
|
+
| `GET /calendar/v3/calendars/{id}/events` | `timeMin`/`timeMax` (overlap), `q`, `showDeleted`, `singleEvents`, `orderBy` (`startTime` needs `singleEvents`), `updatedMin`, `maxResults` (≤ 2500) with `nextPageToken`; the last page carries `nextSyncToken` (not with `q`/`orderBy`/`updatedMin`). `syncToken` returns only changes since, deletions as `{id, status: "cancelled"}`; combined with a window or `q` it is 400; invalid or invalidated it is 410 `fullSyncRequired`. |
|
|
90
|
+
| `POST …/events`, `GET/PUT/DELETE …/events/{eventId}` | Base32hex ids (client ids allowed; duplicates 409). Missing start/end 400, an empty range 400 `timeRangeEmpty`. `PUT` replaces (sequence +1 when the time moves). `DELETE` 204, again 410 `deleted`. |
|
|
91
|
+
| `POST …/events/watch` | `{id, type: "web_hook", address, token?, expiration?}` → `{kind: "api#channel", id, resourceId, resourceUri, expiration}` (capped at 30 days). A `sync` push follows immediately. Duplicate live channel ids are 400. |
|
|
92
|
+
| `POST /calendar/v3/channels/stop` | `{id, resourceId}` → 204; unknown 404. |
|
|
93
|
+
|
|
94
|
+
Errors use Google's envelope `{error: {errors: [{domain, reason, message}], code, message, status?}}`.
|
|
95
|
+
Invalid or expired access tokens are 401 `authError` (userinfo: `invalid_request`).
|
|
96
|
+
|
|
97
|
+
### Push notifications
|
|
98
|
+
|
|
99
|
+
Every change to a calendar's events (through the API or the admin plane) posts to each live
|
|
100
|
+
channel on that calendar: an empty body with `X-Goog-Channel-ID`, `X-Goog-Channel-Token` (when
|
|
101
|
+
set), `X-Goog-Channel-Expiration` (RFC 1123), `X-Goog-Message-Number` (1 for `sync`, then +1),
|
|
102
|
+
`X-Goog-Resource-ID`, `X-Goog-Resource-URI` and `X-Goog-Resource-State` (`sync`, then
|
|
103
|
+
`exists`). Stopped or expired (mock clock) channels get nothing. Non-2xx answers are retried
|
|
104
|
+
(immediately, 1 s, 10 s, 1 min, 10 min). `GET /__admin/webhooks` lists deliveries, and
|
|
105
|
+
`/__admin/webhooks/events`, `…/replay`, `…/flush` work as usual.
|
|
106
|
+
|
|
107
|
+
### Admin (beyond the standard contract)
|
|
108
|
+
|
|
109
|
+
| Route | Effect |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `POST /__admin/events` | `{email, calendarId?, event}`: create an event as the account would in Google's UI (pushes). |
|
|
112
|
+
| `PUT /__admin/events/:id` | `{email, calendarId?, event}`: replace it (pushes). |
|
|
113
|
+
| `DELETE /__admin/events/:id?email=&calendarId=` | Delete it (pushes). |
|
|
114
|
+
| `GET /__admin/events?email=` | Stored events (with change sequences). |
|
|
115
|
+
| `PUT /__admin/users/:email` | Set the userinfo profile. |
|
|
116
|
+
| `GET /__admin/channels` | Every channel with its message number, expiry and stopped flag. |
|
|
117
|
+
| `POST /__admin/sync-tokens/invalidate` | Every sync token issued so far answers 410. |
|
|
118
|
+
| `GET/PUT /__admin/settings` | `{clients?, tokenTtlSeconds?, requireHttpsWebhooks?, maxChannelTtlSeconds?, scope?}`. |
|
|
119
|
+
|
|
120
|
+
Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`): `invalid_grant`,
|
|
121
|
+
`token_expired`, `rate_limited` (403 rateLimitExceeded), `too_many_requests` (429),
|
|
122
|
+
`calendar_api_disabled` (403 accessNotConfigured), `insufficient_scopes`, `sync_token_gone`,
|
|
123
|
+
`backend_error` (503), `connection_drop`, `push_duplicate`, `push_drop`. gaxios retries 429 and
|
|
124
|
+
5xx itself, so pass a `count` above its budget (3 retries) to surface them.
|
|
125
|
+
|
|
126
|
+
### Namespaces
|
|
127
|
+
|
|
128
|
+
By account: access tokens carry their email, so
|
|
129
|
+
`PUT /__admin/credentials {"credentials": {"dr.house@example.com": "<namespace>"}}` routes every
|
|
130
|
+
calendar and userinfo call. `x-mockingbird-namespace` works as usual. A `/ns/<name>` prefix
|
|
131
|
+
works for the token, revoke and userinfo URLs (they are full URLs) but not for the calendar
|
|
132
|
+
client: googleapis resolves paths against `rootUrl`'s origin and drops a path prefix.
|
|
133
|
+
|
|
134
|
+
### SDK
|
|
135
|
+
|
|
136
|
+
The tests drive the real client stack of `googleapis@149`: `@googleapis/calendar@9.8.0` (the same
|
|
137
|
+
generated calendar module over googleapis-common 7 / gaxios 6; the full `googleapis` package is
|
|
138
|
+
~125 MB unpacked, so only the module is installed) and `google-auth-library@9.15.0`.
|
|
139
|
+
|
|
140
|
+
### Deliberately not modelled
|
|
141
|
+
|
|
142
|
+
- Recurring events (`recurrence` is stored, never expanded), attendees' responses and email
|
|
143
|
+
invitations, Meet links, free/busy, ACLs, colors and settings.
|
|
144
|
+
- Real Google identity: `id_token`s are JWT-shaped but not signed with Google's keys.
|
|
145
|
+
- `X-Goog-Changed` (Google omits it for calendar event channels) and `not_exists` states.
|
|
146
|
+
|
|
147
|
+
## API
|
|
148
|
+
|
|
149
|
+
| Export | Kind | Description |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `GoogleCalendarAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `user(email)`, `ensurePrimary(email)`, `resolveCalendar(email, id)`, `createEvent`, `updateEvent`, `deleteEvent`, `findEvent`, `notify(calendarId)`, `channels()`, `events(email?)`, `invalidateSyncTokens()`. Options: `sqlite`, `now`, `namespace`, `settings`, `onPush`, `onChannels`. |
|
|
152
|
+
| `createRuntime` | function | The mock with the full service contract and push delivery. Options: `settings`, `push: {retryDelaysMs?, fetch?}`, `clock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
|
|
153
|
+
| `GOOGLE_CALENDAR_PRESETS` | object | Every named fault preset. |
|
|
154
|
+
| `GOOGLE_CALENDAR_NAMESPACE` | string | The service name, `"google-calendar"`. |
|
|
155
|
+
| `DEFAULT_SCOPE` | string | The scope the token endpoint reports. |
|
|
156
|
+
| `pushHeaders` | function | The `X-Goog-*` headers of a push notification. |
|
|
157
|
+
| `issueAccessToken` | function | Mint an access token the mock accepts for an email. |
|
|
158
|
+
| `parseToken` | function | Decode a mock access or refresh token. |
|
|
159
|
+
| `accessTokenCredential` | function | The account an access token belongs to (how credentials map to namespaces). |
|
|
160
|
+
| `emailFromCode` | function | The account a `4/mock-…` authorization code signs in. |
|
|
161
|
+
| `googleError` | function | Build Google's error envelope. |
|
|
162
|
+
| `document`, `operationIds`, `supportedOperationIds` | values | The OpenAPI contract and its operation ids. |
|
|
163
|
+
| `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target (`--client`, `--require-https-webhooks`); port 8820. |
|
|
164
|
+
|
|
165
|
+
Part of [mockingbird](https://github.com/crvouga/mockingbird).
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRuntime
|
|
3
|
+
} from "./chunk-UAEYK3SE.js";
|
|
4
|
+
|
|
5
|
+
// ../../adapters/node/dist/cli.js
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
|
|
9
|
+
// ../../adapters/node/dist/serve.js
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
var serve = async (api, options = {}) => {
|
|
12
|
+
const server = createServer(async (req, res) => {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of req) {
|
|
15
|
+
chunks.push(Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
const body = Buffer.concat(chunks);
|
|
18
|
+
const address = server.address();
|
|
19
|
+
const port = typeof address === "object" && address !== null ? address.port : void 0;
|
|
20
|
+
const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`;
|
|
21
|
+
const raw = req.url ?? "/";
|
|
22
|
+
const url = new URL(raw.replace(/^\/+/, "/"), base);
|
|
23
|
+
const method = req.method ?? "GET";
|
|
24
|
+
const init = { method, headers: req.headers };
|
|
25
|
+
if (method !== "GET" && method !== "HEAD" && body.length > 0) {
|
|
26
|
+
init.body = body;
|
|
27
|
+
}
|
|
28
|
+
const aborted = new AbortController();
|
|
29
|
+
res.once("close", () => {
|
|
30
|
+
if (!res.writableFinished)
|
|
31
|
+
aborted.abort();
|
|
32
|
+
});
|
|
33
|
+
init.signal = aborted.signal;
|
|
34
|
+
const request = new Request(url, init);
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await api.fetch(request);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code === "MOCKINGBIRD_DROP") {
|
|
40
|
+
req.socket.destroy();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
44
|
+
res.end(JSON.stringify({
|
|
45
|
+
error: {
|
|
46
|
+
type: "mockingbird_internal",
|
|
47
|
+
message: error instanceof Error ? error.message : String(error)
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const headers = Object.fromEntries(response.headers);
|
|
53
|
+
const cookies = response.headers.getSetCookie();
|
|
54
|
+
if (cookies.length > 0)
|
|
55
|
+
headers["set-cookie"] = cookies;
|
|
56
|
+
if (!response.body) {
|
|
57
|
+
res.writeHead(response.status, headers);
|
|
58
|
+
res.end();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
res.writeHead(response.status, headers);
|
|
62
|
+
res.flushHeaders();
|
|
63
|
+
const reader = response.body.getReader();
|
|
64
|
+
try {
|
|
65
|
+
for (; ; ) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done)
|
|
68
|
+
break;
|
|
69
|
+
if (!res.write(value))
|
|
70
|
+
await new Promise((resolve) => res.once("drain", resolve));
|
|
71
|
+
}
|
|
72
|
+
res.end();
|
|
73
|
+
} catch {
|
|
74
|
+
res.destroy();
|
|
75
|
+
} finally {
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
await new Promise((resolve, reject) => {
|
|
80
|
+
server.once("error", reject);
|
|
81
|
+
server.listen(options.port ?? 0, options.host, resolve);
|
|
82
|
+
});
|
|
83
|
+
return server;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ../../adapters/node/dist/listen.js
|
|
87
|
+
var listen = async (api, options = {}) => {
|
|
88
|
+
const host = options.host ?? "127.0.0.1";
|
|
89
|
+
const server = await serve(api, { port: options.port ?? 0, host });
|
|
90
|
+
const address = server.address();
|
|
91
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
|
|
92
|
+
const shown = host.includes(":") ? `[${host}]` : host;
|
|
93
|
+
return {
|
|
94
|
+
url: `http://${shown}:${port}`,
|
|
95
|
+
port,
|
|
96
|
+
host,
|
|
97
|
+
server,
|
|
98
|
+
close: () => new Promise((resolve, reject) => {
|
|
99
|
+
server.close((error) => {
|
|
100
|
+
if (error && error.code !== "ERR_SERVER_NOT_RUNNING")
|
|
101
|
+
reject(error);
|
|
102
|
+
else
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
server.closeAllConnections?.();
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ../../adapters/node/dist/cli.js
|
|
111
|
+
var optionHelp = (options) => Object.entries(options).map(([name, option]) => {
|
|
112
|
+
const flag = `--${name}${option.type === "string" ? ` ${option.value ?? "<value>"}` : ""}`;
|
|
113
|
+
const fallback = option.default !== void 0 ? ` (default: ${String(option.default)})` : "";
|
|
114
|
+
return ` ${flag.padEnd(30)} ${option.description}${fallback}`;
|
|
115
|
+
});
|
|
116
|
+
var help = (spec) => [
|
|
117
|
+
`${spec.bin} \u2014 ${spec.description}`,
|
|
118
|
+
"",
|
|
119
|
+
"Usage:",
|
|
120
|
+
` ${spec.bin} <command> [options]`,
|
|
121
|
+
"",
|
|
122
|
+
"Commands:",
|
|
123
|
+
...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),
|
|
124
|
+
"",
|
|
125
|
+
`Run \`${spec.bin} <command> --help\` for a command's options.`
|
|
126
|
+
].join("\n");
|
|
127
|
+
var commandHelp = (spec, name, command) => [
|
|
128
|
+
`${spec.bin} ${name} \u2014 ${command.summary}`,
|
|
129
|
+
"",
|
|
130
|
+
"Usage:",
|
|
131
|
+
` ${command.usage ?? `${spec.bin} ${name} [options]`}`,
|
|
132
|
+
...command.options ? ["", "Options:", ...optionHelp(command.options)] : []
|
|
133
|
+
].join("\n");
|
|
134
|
+
var runCli = async (spec, argv) => {
|
|
135
|
+
const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : void 0;
|
|
136
|
+
const words = pair !== void 0 && spec.commands[pair] ? 2 : 1;
|
|
137
|
+
const name = words === 2 ? pair : argv[0];
|
|
138
|
+
const rest = argv.slice(words);
|
|
139
|
+
if (name === void 0 || name === "--help" || name === "-h" || name === "help") {
|
|
140
|
+
console.log(help(spec));
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
const command = spec.commands[name];
|
|
144
|
+
if (!command) {
|
|
145
|
+
console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}
|
|
146
|
+
|
|
147
|
+
${help(spec)}`);
|
|
148
|
+
return 2;
|
|
149
|
+
}
|
|
150
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
151
|
+
console.log(commandHelp(spec, name, command));
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = parseArgs({
|
|
157
|
+
args: rest,
|
|
158
|
+
allowPositionals: true,
|
|
159
|
+
strict: true,
|
|
160
|
+
options: Object.fromEntries(Object.entries(command.options ?? {}).map(([key, option]) => [
|
|
161
|
+
key,
|
|
162
|
+
{
|
|
163
|
+
type: option.type,
|
|
164
|
+
...option.default !== void 0 ? { default: option.default } : {}
|
|
165
|
+
}
|
|
166
|
+
]))
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}
|
|
170
|
+
|
|
171
|
+
${commandHelp(spec, name, command)}`);
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
return command.run(parsed.values, parsed.positionals);
|
|
175
|
+
};
|
|
176
|
+
var COMMON_SERVE_OPTIONS = {
|
|
177
|
+
port: { type: "string", value: "<port>", description: "Port to listen on" },
|
|
178
|
+
host: { type: "string", value: "<host>", description: "Interface to bind", default: "127.0.0.1" },
|
|
179
|
+
"admin-key": {
|
|
180
|
+
type: "string",
|
|
181
|
+
value: "<key>",
|
|
182
|
+
description: "Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)"
|
|
183
|
+
},
|
|
184
|
+
seed: { type: "string", value: "<seed>", description: "Seed for every random choice" },
|
|
185
|
+
log: {
|
|
186
|
+
type: "string",
|
|
187
|
+
value: "<pretty|json|off>",
|
|
188
|
+
description: "Request log format",
|
|
189
|
+
default: "pretty"
|
|
190
|
+
},
|
|
191
|
+
"log-requests": {
|
|
192
|
+
type: "boolean",
|
|
193
|
+
description: "One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json"
|
|
194
|
+
},
|
|
195
|
+
config: {
|
|
196
|
+
type: "string",
|
|
197
|
+
value: "<file>",
|
|
198
|
+
description: "Serve every service in a mockingbird.json config instead"
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var formatLog = (format) => {
|
|
202
|
+
if (format === "off")
|
|
203
|
+
return void 0;
|
|
204
|
+
if (format === "json")
|
|
205
|
+
return (entry) => console.log(JSON.stringify(entry));
|
|
206
|
+
return (entry) => {
|
|
207
|
+
const op = entry.operationId ?? (entry.unmatched ? "UNMATCHED" : "-");
|
|
208
|
+
const ns = entry.namespace === "default" ? "" : ` [${entry.namespace}]`;
|
|
209
|
+
const fault = entry.faultId ? ` fault=${entry.faultId}` : "";
|
|
210
|
+
const adopted = entry.adopted ? " adopted" : "";
|
|
211
|
+
console.log(`${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}`);
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
var asString = (value) => typeof value === "string" ? value : void 0;
|
|
215
|
+
var loadTarget = async (name, own) => {
|
|
216
|
+
if (name === own.name)
|
|
217
|
+
return own;
|
|
218
|
+
const specifier = `@crvouga/mockingbird-service-${name}/server`;
|
|
219
|
+
try {
|
|
220
|
+
const mod = await import(specifier);
|
|
221
|
+
if (!mod.serveTarget)
|
|
222
|
+
throw new Error(`${specifier} exports no serveTarget`);
|
|
223
|
+
return mod.serveTarget;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
226
|
+
throw new Error(`cannot load service "${name}": ${reason}. Install @crvouga/mockingbird-service-${name}.`);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var start = async (target, values, config) => {
|
|
230
|
+
const runtime = await target.create(values, {
|
|
231
|
+
adminKey: config.adminKey,
|
|
232
|
+
seed: config.seed,
|
|
233
|
+
onLog: formatLog(config.log)
|
|
234
|
+
});
|
|
235
|
+
const listening = await listen(runtime, { port: config.port, host: config.host });
|
|
236
|
+
console.log(`${target.name} mock listening on ${listening.url}`);
|
|
237
|
+
console.log(`${target.name} health: GET ${listening.url}/health`);
|
|
238
|
+
console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
|
|
239
|
+
for (const line of target.banner?.(runtime) ?? [])
|
|
240
|
+
console.log(`${target.name} ${line}`);
|
|
241
|
+
return listening;
|
|
242
|
+
};
|
|
243
|
+
var untilSignal = async (servers) => new Promise((resolve) => {
|
|
244
|
+
const stop = async () => {
|
|
245
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
246
|
+
resolve(0);
|
|
247
|
+
};
|
|
248
|
+
process.once("SIGINT", stop);
|
|
249
|
+
process.once("SIGTERM", stop);
|
|
250
|
+
});
|
|
251
|
+
var serveCommand = (target) => ({
|
|
252
|
+
summary: `Serve the ${target.name} mock over HTTP`,
|
|
253
|
+
options: { ...COMMON_SERVE_OPTIONS, ...target.options },
|
|
254
|
+
async run(values) {
|
|
255
|
+
const log = values["log-requests"] === true ? "json" : asString(values.log) ?? "pretty";
|
|
256
|
+
if (!["pretty", "json", "off"].includes(log)) {
|
|
257
|
+
console.error(`--log must be pretty, json or off (got ${log})`);
|
|
258
|
+
return 2;
|
|
259
|
+
}
|
|
260
|
+
const configPath = asString(values.config);
|
|
261
|
+
if (configPath !== void 0) {
|
|
262
|
+
const config = JSON.parse(await readFile(configPath, "utf8"));
|
|
263
|
+
const servers = [];
|
|
264
|
+
try {
|
|
265
|
+
for (const [name, entry] of Object.entries(config.services ?? {})) {
|
|
266
|
+
const each = await loadTarget(name, target);
|
|
267
|
+
servers.push(await start(each, entry.options ?? {}, {
|
|
268
|
+
port: entry.port ?? each.defaultPort,
|
|
269
|
+
host: entry.host ?? "127.0.0.1",
|
|
270
|
+
...entry.adminKey !== void 0 ? { adminKey: entry.adminKey } : {},
|
|
271
|
+
...entry.seed !== void 0 ? { seed: entry.seed } : {},
|
|
272
|
+
log: config.log ?? log
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
await Promise.allSettled(servers.map((s) => s.close()));
|
|
277
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
return untilSignal(servers);
|
|
281
|
+
}
|
|
282
|
+
const port = asString(values.port);
|
|
283
|
+
const adminKey = asString(values["admin-key"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY;
|
|
284
|
+
const seed = asString(values.seed);
|
|
285
|
+
let listening;
|
|
286
|
+
try {
|
|
287
|
+
listening = await start(target, values, {
|
|
288
|
+
port: port === void 0 ? target.defaultPort : Number.parseInt(port, 10),
|
|
289
|
+
host: asString(values.host) ?? "127.0.0.1",
|
|
290
|
+
...adminKey !== void 0 ? { adminKey } : {},
|
|
291
|
+
...seed !== void 0 ? { seed } : {},
|
|
292
|
+
log
|
|
293
|
+
});
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
return untilSignal([listening]);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// src/server.ts
|
|
303
|
+
var DEFAULT_PORT = 8820;
|
|
304
|
+
var createServer2 = async (options = {}) => {
|
|
305
|
+
const { port, host, ...rest } = options;
|
|
306
|
+
const runtime = createRuntime(rest);
|
|
307
|
+
const listening = await listen(runtime, {
|
|
308
|
+
port: port ?? 0,
|
|
309
|
+
...host !== void 0 ? { host } : {}
|
|
310
|
+
});
|
|
311
|
+
return { ...listening, runtime };
|
|
312
|
+
};
|
|
313
|
+
var text = (value) => typeof value === "string" ? value : void 0;
|
|
314
|
+
var serveTarget = {
|
|
315
|
+
name: "google-calendar",
|
|
316
|
+
defaultPort: DEFAULT_PORT,
|
|
317
|
+
options: {
|
|
318
|
+
client: {
|
|
319
|
+
type: "string",
|
|
320
|
+
value: "<client_id:client_secret>",
|
|
321
|
+
description: "Accept only this OAuth client (GOOGLE_CLIENT_ID:GOOGLE_CLIENT_SECRET); default: any"
|
|
322
|
+
},
|
|
323
|
+
"require-https-webhooks": {
|
|
324
|
+
type: "boolean",
|
|
325
|
+
description: "Reject non-https watch addresses, as Google does (default: allow http for local receivers)"
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
create: (values, common) => {
|
|
329
|
+
const client = text(values.client);
|
|
330
|
+
const colon = client?.indexOf(":") ?? -1;
|
|
331
|
+
if (client && colon < 1) throw new Error('--client must look like "client_id:client_secret"');
|
|
332
|
+
return createRuntime({
|
|
333
|
+
settings: {
|
|
334
|
+
...client ? {
|
|
335
|
+
clients: [
|
|
336
|
+
{ clientId: client.slice(0, colon), clientSecret: client.slice(colon + 1) }
|
|
337
|
+
]
|
|
338
|
+
} : {},
|
|
339
|
+
...values["require-https-webhooks"] === true ? { requireHttpsWebhooks: true } : {}
|
|
340
|
+
},
|
|
341
|
+
...common.adminKey !== void 0 ? { adminKey: common.adminKey } : {},
|
|
342
|
+
...common.seed !== void 0 ? { seed: common.seed } : {},
|
|
343
|
+
...common.onLog ? { onLog: common.onLog } : {}
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
banner: () => [
|
|
347
|
+
"rootUrl: <this url>/ for googleapis; OAuth2Client endpoints: <this url>/token, <this url>/revoke",
|
|
348
|
+
"sign in with authorization code 4/mock-<name> (\u2192 <name>@example.com) or 4/mock-<email>",
|
|
349
|
+
"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<email>: <ns>}"
|
|
350
|
+
]
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export {
|
|
354
|
+
runCli,
|
|
355
|
+
serveCommand,
|
|
356
|
+
DEFAULT_PORT,
|
|
357
|
+
createServer2 as createServer,
|
|
358
|
+
serveTarget
|
|
359
|
+
};
|
|
360
|
+
//# sourceMappingURL=chunk-S2OLMZ66.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 {\n createRuntime,\n type GoogleCalendarRuntime,\n type GoogleCalendarRuntimeOptions,\n} from \"./runtime.js\"\n\n/** Port `mockingbird-google-calendar serve` listens on when none is given. */\nexport const DEFAULT_PORT = 8820\n\nexport type GoogleCalendarServerOptions = GoogleCalendarRuntimeOptions & {\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 GoogleCalendarServer = Listening & { runtime: GoogleCalendarRuntime }\n\n/** Serve the Google Calendar mock over `node:http`. */\nexport const createServer = async (\n options: GoogleCalendarServerOptions = {},\n): Promise<GoogleCalendarServer> => {\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 GoogleCalendar mock from flags. */\nexport const serveTarget: ServeTarget = {\n name: \"google-calendar\",\n defaultPort: DEFAULT_PORT,\n options: {\n client: {\n type: \"string\",\n value: \"<client_id:client_secret>\",\n description:\n \"Accept only this OAuth client (GOOGLE_CLIENT_ID:GOOGLE_CLIENT_SECRET); default: any\",\n },\n \"require-https-webhooks\": {\n type: \"boolean\",\n description:\n \"Reject non-https watch addresses, as Google does (default: allow http for local receivers)\",\n },\n },\n create: (values, common) => {\n const client = text(values.client)\n const colon = client?.indexOf(\":\") ?? -1\n if (client && colon < 1) throw new Error('--client must look like \"client_id:client_secret\"')\n return createRuntime({\n settings: {\n ...(client\n ? {\n clients: [\n { clientId: client.slice(0, colon), clientSecret: client.slice(colon + 1) },\n ],\n }\n : {}),\n ...(values[\"require-https-webhooks\"] === true ? { requireHttpsWebhooks: true } : {}),\n },\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 \"rootUrl: <this url>/ for googleapis; OAuth2Client endpoints: <this url>/token, <this url>/revoke\",\n \"sign in with authorization code 4/mock-<name> (\u2192 <name>@example.com) or 4/mock-<email>\",\n \"namespaces: x-mockingbird-namespace, /ns/<name>/\u2026, or PUT /__admin/credentials {<email>: <ns>}\",\n ],\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["createServer"]
|
|
7
|
+
}
|