@venn-lang/http 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/LICENSE +21 -0
- package/README.md +162 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +577 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.mts +63 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +146 -0
- package/dist/node.mjs.map +1 -0
- package/package.json +61 -0
- package/src/actions/build-request.ts +110 -0
- package/src/actions/http-action.ts +36 -0
- package/src/actions/index.ts +18 -0
- package/src/actions/request-params.ts +32 -0
- package/src/actions/request.types.ts +28 -0
- package/src/clients/fake-client.ts +37 -0
- package/src/clients/fetch-client.ts +59 -0
- package/src/clients/index.ts +2 -0
- package/src/index.ts +11 -0
- package/src/matchers/header.ts +29 -0
- package/src/matchers/index.ts +4 -0
- package/src/node.ts +6 -0
- package/src/plugin.ts +22 -0
- package/src/port/http-client.port.ts +15 -0
- package/src/port/http-client.types.ts +29 -0
- package/src/port/index.ts +2 -0
- package/src/server/http-server.errors.ts +36 -0
- package/src/server/http-server.port.ts +15 -0
- package/src/server/http-server.types.ts +39 -0
- package/src/server/index.ts +12 -0
- package/src/server/memory-server.ts +78 -0
- package/src/server/node-server.ts +122 -0
- package/src/server/on-action.ts +42 -0
- package/src/server/serve-action.ts +99 -0
- package/src/types.ts +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { arg, defineAction, defineMatcher, definePlugin, optionalArg, z } from "@venn-lang/sdk";
|
|
2
|
+
import { t } from "@venn-lang/types";
|
|
3
|
+
import { VennError } from "@venn-lang/contracts";
|
|
4
|
+
//#region src/clients/fake-client.ts
|
|
5
|
+
/**
|
|
6
|
+
* A 200 response with a small JSON body, for tests that only care about a field
|
|
7
|
+
* or two.
|
|
8
|
+
*
|
|
9
|
+
* @param overrides Fields to replace on the default response.
|
|
10
|
+
* @returns A complete {@link HttpResponse}.
|
|
11
|
+
*/
|
|
12
|
+
function okResponse(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
status: 200,
|
|
15
|
+
ok: true,
|
|
16
|
+
headers: {},
|
|
17
|
+
body: "{\"ok\":true}",
|
|
18
|
+
json: { ok: true },
|
|
19
|
+
time: 0,
|
|
20
|
+
...overrides
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The double: answers from a table keyed by the request's full URL.
|
|
25
|
+
*
|
|
26
|
+
* A URL with no entry gets {@link okResponse}, so a test only has to name the
|
|
27
|
+
* responses it cares about. Never touches the network.
|
|
28
|
+
*
|
|
29
|
+
* @param args.responses Canned responses, keyed by the URL the flow requests.
|
|
30
|
+
*/
|
|
31
|
+
function createFakeClient(args = {}) {
|
|
32
|
+
const responses = args.responses ?? {};
|
|
33
|
+
return { request: async (req) => responses[req.url] ?? okResponse() };
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/clients/fetch-client.ts
|
|
37
|
+
/**
|
|
38
|
+
* The real client, over the global `fetch`. Requires the `net` capability.
|
|
39
|
+
*
|
|
40
|
+
* The body is always read as text and parsed afterwards, so a reply that claims
|
|
41
|
+
* JSON but is not still arrives whole in `body` instead of throwing.
|
|
42
|
+
*/
|
|
43
|
+
function createFetchClient() {
|
|
44
|
+
return { async request(req) {
|
|
45
|
+
const response = await fetch(req.url, {
|
|
46
|
+
method: req.method,
|
|
47
|
+
headers: req.headers,
|
|
48
|
+
body: req.body,
|
|
49
|
+
signal: req.signal
|
|
50
|
+
});
|
|
51
|
+
const body = await response.text();
|
|
52
|
+
return mapResponse({
|
|
53
|
+
status: response.status,
|
|
54
|
+
ok: response.ok,
|
|
55
|
+
headers: response.headers,
|
|
56
|
+
body
|
|
57
|
+
});
|
|
58
|
+
} };
|
|
59
|
+
}
|
|
60
|
+
function mapResponse(args) {
|
|
61
|
+
return {
|
|
62
|
+
status: args.status,
|
|
63
|
+
ok: args.ok,
|
|
64
|
+
headers: toObject(args.headers),
|
|
65
|
+
body: args.body,
|
|
66
|
+
json: tryJson(args.body),
|
|
67
|
+
time: 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function toObject(headers) {
|
|
71
|
+
const out = {};
|
|
72
|
+
headers.forEach((value, key) => {
|
|
73
|
+
out[key] = value;
|
|
74
|
+
});
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function tryJson(body) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(body);
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/server/on-action.ts
|
|
86
|
+
/**
|
|
87
|
+
* `http.on server handler`: say what the server answers with.
|
|
88
|
+
*
|
|
89
|
+
* The handler is an ordinary `fn`, so everything the language already does
|
|
90
|
+
* applies inside it: it can call any verb, and it waits for what it reaches for
|
|
91
|
+
* without saying so. Whatever it returns becomes the reply. Calling `http.on`
|
|
92
|
+
* again replaces the handler.
|
|
93
|
+
*/
|
|
94
|
+
function onAction() {
|
|
95
|
+
return defineAction({
|
|
96
|
+
name: "on",
|
|
97
|
+
doc: "Answer this server's requests with a function. `fn (req) => { … }` is the reply.",
|
|
98
|
+
args: [arg("server", t.ref("http.Server"), "The handle `http.serve` gave back."), arg("handler", t.callback([t.ref("http.Request")], t.dynamic, 1), "Called for every request. What it returns is the reply.")],
|
|
99
|
+
result: t.void,
|
|
100
|
+
run: (ctx, input) => {
|
|
101
|
+
const server = asServer(input.args[0]);
|
|
102
|
+
const handler = input.args[1];
|
|
103
|
+
if (!server) throw new Error("`http.on` needs a server, as `http.serve` returns.");
|
|
104
|
+
if (handler === void 0) throw new Error("`http.on` needs a function to answer with.");
|
|
105
|
+
server.onRequest((request) => ctx.invoke(handler, [request]));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function asServer(value) {
|
|
110
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
111
|
+
return value.kind === "http-server" ? value : void 0;
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/server/http-server.port.ts
|
|
115
|
+
/**
|
|
116
|
+
* The port descriptor `http.serve` resolves through `ctx.port(...)`.
|
|
117
|
+
*
|
|
118
|
+
* Declares the `net` capability, so a host that cannot bind a socket refuses the
|
|
119
|
+
* binding at load time rather than mid-flow.
|
|
120
|
+
*/
|
|
121
|
+
const HttpServerPort = {
|
|
122
|
+
id: "venn.port.http-server",
|
|
123
|
+
version: 1,
|
|
124
|
+
requires: ["net"],
|
|
125
|
+
methods: ["listen"]
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/server/serve-action.ts
|
|
129
|
+
const serveParams = z.object({
|
|
130
|
+
port: z.number().optional().describe("Which port to bind. 0 asks for any free one."),
|
|
131
|
+
host: z.string().optional().describe("Which interface to bind. Defaults to 127.0.0.1.")
|
|
132
|
+
});
|
|
133
|
+
/**
|
|
134
|
+
* `let api = http.serve { port: 8080 }`: start listening.
|
|
135
|
+
*
|
|
136
|
+
* The handler starts as a 404 and `http.on` replaces it, so a request arriving
|
|
137
|
+
* before the flow has said what to do with it gets an answer instead of hanging.
|
|
138
|
+
*/
|
|
139
|
+
function serveAction() {
|
|
140
|
+
return defineAction({
|
|
141
|
+
name: "serve",
|
|
142
|
+
doc: "Start an HTTP server and hand back a handle. `http.on` says what to answer.",
|
|
143
|
+
params: serveParams.optional(),
|
|
144
|
+
result: t.ref("http.Server"),
|
|
145
|
+
run: async (ctx, input) => {
|
|
146
|
+
const params = input.params ?? {};
|
|
147
|
+
const state = { handle: notFound };
|
|
148
|
+
return handle(await ctx.port(HttpServerPort).listen({
|
|
149
|
+
port: params.port ?? 0,
|
|
150
|
+
host: params.host,
|
|
151
|
+
handle: (request) => state.handle(request)
|
|
152
|
+
}), state);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function handle(running, state) {
|
|
157
|
+
return {
|
|
158
|
+
kind: "http-server",
|
|
159
|
+
port: running.port,
|
|
160
|
+
close: () => running.close(),
|
|
161
|
+
deliver: async (request) => state.handle(fill$1(request)),
|
|
162
|
+
onRequest: (given) => {
|
|
163
|
+
state.handle = async (request) => asReply(await given(request));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Whatever the flow answered, as a reply. A map with a `status` is taken at its
|
|
169
|
+
* word; anything else is the body, so `=> { ok: true }` does the obvious thing.
|
|
170
|
+
*/
|
|
171
|
+
function asReply(value) {
|
|
172
|
+
if (value === void 0 || value === null) return { status: 204 };
|
|
173
|
+
if (isReplyShape(value)) return value;
|
|
174
|
+
return {
|
|
175
|
+
status: 200,
|
|
176
|
+
body: value
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function isReplyShape(value) {
|
|
180
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
181
|
+
const shape = value;
|
|
182
|
+
return "status" in shape || "headers" in shape || "body" in shape;
|
|
183
|
+
}
|
|
184
|
+
function notFound() {
|
|
185
|
+
return {
|
|
186
|
+
status: 404,
|
|
187
|
+
body: { error: "This server has no handler yet." }
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function fill$1(request) {
|
|
191
|
+
return {
|
|
192
|
+
method: request.method ?? "GET",
|
|
193
|
+
url: request.url ?? "/",
|
|
194
|
+
headers: request.headers ?? {},
|
|
195
|
+
body: request.body ?? ""
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/port/http-client.port.ts
|
|
200
|
+
/**
|
|
201
|
+
* The port descriptor an http verb resolves through `ctx.port(...)`.
|
|
202
|
+
*
|
|
203
|
+
* Declares the `net` capability, so a host that cannot open sockets refuses the
|
|
204
|
+
* binding at load time with a readable diagnostic.
|
|
205
|
+
*/
|
|
206
|
+
const HttpClientPort = {
|
|
207
|
+
id: "venn.port.http-client",
|
|
208
|
+
version: 1,
|
|
209
|
+
requires: ["net"],
|
|
210
|
+
methods: ["request"]
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/actions/build-request.ts
|
|
214
|
+
/** Turn one action call into the request the HttpClient port sends. */
|
|
215
|
+
function buildRequest(args) {
|
|
216
|
+
const payload = bodyOf(args.params);
|
|
217
|
+
return {
|
|
218
|
+
method: args.method,
|
|
219
|
+
url: withQuery(absoluteUrl(String(args.url ?? ""), args.baseUrl), args.params.query),
|
|
220
|
+
headers: headersOf(args.params, payload.contentType),
|
|
221
|
+
body: payload.body,
|
|
222
|
+
signal: args.signal
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const BOUNDARY = "----vennFormBoundary";
|
|
226
|
+
/** A map means JSON and a string means raw, unless `encode` says otherwise. */
|
|
227
|
+
function bodyOf(params) {
|
|
228
|
+
if (params.body === void 0) return {};
|
|
229
|
+
switch (params.encode ?? defaultEncoding(params.body)) {
|
|
230
|
+
case "raw": return { body: String(params.body) };
|
|
231
|
+
case "form": return {
|
|
232
|
+
body: encode(asMap(params.body)),
|
|
233
|
+
contentType: FORM_TYPE
|
|
234
|
+
};
|
|
235
|
+
case "multipart": return multipart(asMap(params.body));
|
|
236
|
+
default: return {
|
|
237
|
+
body: JSON.stringify(params.body),
|
|
238
|
+
contentType: "application/json"
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function defaultEncoding(body) {
|
|
243
|
+
return typeof body === "string" ? "raw" : "json";
|
|
244
|
+
}
|
|
245
|
+
const FORM_TYPE = "application/x-www-form-urlencoded";
|
|
246
|
+
function multipart(values) {
|
|
247
|
+
return {
|
|
248
|
+
body: `${Object.entries(values).map(([key, value]) => `--${BOUNDARY}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`).join("")}--${BOUNDARY}--\r\n`,
|
|
249
|
+
contentType: `multipart/form-data; boundary=${BOUNDARY}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function asMap(body) {
|
|
253
|
+
return body ?? {};
|
|
254
|
+
}
|
|
255
|
+
function headersOf(params, contentType) {
|
|
256
|
+
const headers = {};
|
|
257
|
+
for (const [key, value] of Object.entries(params.headers ?? {})) headers[key] = String(value);
|
|
258
|
+
if (contentType && !hasHeader$1(headers, "content-type")) headers["Content-Type"] = contentType;
|
|
259
|
+
const auth = authorization(params);
|
|
260
|
+
if (auth && !hasHeader$1(headers, "authorization")) headers.Authorization = auth;
|
|
261
|
+
return headers;
|
|
262
|
+
}
|
|
263
|
+
function authorization(params) {
|
|
264
|
+
if (params.bearer) return `Bearer ${params.bearer}`;
|
|
265
|
+
if (!params.basic) return void 0;
|
|
266
|
+
return `Basic ${base64(`${params.basic.user}:${params.basic.pass}`)}`;
|
|
267
|
+
}
|
|
268
|
+
function withQuery(url, query) {
|
|
269
|
+
const encoded = query ? encode(query) : "";
|
|
270
|
+
if (!encoded) return url;
|
|
271
|
+
return `${url}${url.includes("?") ? "&" : "?"}${encoded}`;
|
|
272
|
+
}
|
|
273
|
+
function encode(values) {
|
|
274
|
+
const search = new URLSearchParams();
|
|
275
|
+
for (const [key, value] of Object.entries(values)) search.append(key, String(value));
|
|
276
|
+
return search.toString();
|
|
277
|
+
}
|
|
278
|
+
function hasHeader$1(headers, name) {
|
|
279
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name);
|
|
280
|
+
}
|
|
281
|
+
function base64(text) {
|
|
282
|
+
return btoa(text);
|
|
283
|
+
}
|
|
284
|
+
/** Resolve a relative path against `config.baseUrl`; absolute URLs pass through. */
|
|
285
|
+
function absoluteUrl(url, baseUrl) {
|
|
286
|
+
if (typeof baseUrl !== "string" || baseUrl === "" || hasScheme(url)) return url;
|
|
287
|
+
return `${baseUrl.replace(/\/+$/, "")}/${url.replace(/^\/+/, "")}`;
|
|
288
|
+
}
|
|
289
|
+
function hasScheme(url) {
|
|
290
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(url);
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/actions/request-params.ts
|
|
294
|
+
const scalar = z.union([
|
|
295
|
+
z.string(),
|
|
296
|
+
z.number(),
|
|
297
|
+
z.boolean()
|
|
298
|
+
]);
|
|
299
|
+
const scalarMap = z.record(z.string(), scalar);
|
|
300
|
+
/**
|
|
301
|
+
* Validation for {@link RequestParams}, the options map every http verb accepts.
|
|
302
|
+
*
|
|
303
|
+
* Each key carries its own `.describe()` because the editor reads this schema to
|
|
304
|
+
* offer the keys, document them and reject the ones that do not exist. Keeping
|
|
305
|
+
* the wording here keeps it beside the rule it describes.
|
|
306
|
+
*/
|
|
307
|
+
const requestParams = z.object({
|
|
308
|
+
headers: scalarMap.optional().describe("Extra headers. Anything you set here wins over what Venn would infer."),
|
|
309
|
+
query: scalarMap.optional().describe("Appended to the URL as a query string, encoded for you."),
|
|
310
|
+
body: z.unknown().optional().describe("What to send. A map becomes JSON; a string is sent as written."),
|
|
311
|
+
encode: z.enum([
|
|
312
|
+
"json",
|
|
313
|
+
"form",
|
|
314
|
+
"multipart",
|
|
315
|
+
"raw"
|
|
316
|
+
]).optional().describe("How to serialise `body`. Defaults to `json` for a map, `raw` for a string."),
|
|
317
|
+
bearer: z.string().optional().describe("Shorthand for `Authorization: Bearer …`."),
|
|
318
|
+
basic: z.object({
|
|
319
|
+
user: z.string(),
|
|
320
|
+
pass: z.string()
|
|
321
|
+
}).optional().describe("Shorthand for HTTP basic auth.")
|
|
322
|
+
});
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/actions/http-action.ts
|
|
325
|
+
/**
|
|
326
|
+
* Build the action for one HTTP verb.
|
|
327
|
+
*
|
|
328
|
+
* Every verb has the same shape: the URL is the single positional argument and
|
|
329
|
+
* everything else rides in the options map, so `http.get` and `http.post` read
|
|
330
|
+
* alike at the call site.
|
|
331
|
+
*
|
|
332
|
+
* @param config.name The verb as a flow writes it, such as `get`.
|
|
333
|
+
* @param config.method The method put on the wire, such as `GET`.
|
|
334
|
+
*/
|
|
335
|
+
function httpAction(config) {
|
|
336
|
+
return defineAction({
|
|
337
|
+
name: config.name,
|
|
338
|
+
doc: `HTTP ${config.method} request.`,
|
|
339
|
+
params: requestParams.optional(),
|
|
340
|
+
args: [arg("url", t.string, "Where to send it. Relative paths join the configured base URL.")],
|
|
341
|
+
result: t.ref("http.Response"),
|
|
342
|
+
run: (ctx, input) => ctx.port(HttpClientPort).request(buildRequest({
|
|
343
|
+
method: config.method,
|
|
344
|
+
url: input.args[0],
|
|
345
|
+
params: input.params ?? {},
|
|
346
|
+
baseUrl: ctx.config.baseUrl,
|
|
347
|
+
signal: ctx.signal
|
|
348
|
+
}))
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/actions/index.ts
|
|
353
|
+
/** The http namespace's verbs. Adding one is a single line here. */
|
|
354
|
+
const httpActions = [
|
|
355
|
+
httpAction({
|
|
356
|
+
name: "get",
|
|
357
|
+
method: "GET"
|
|
358
|
+
}),
|
|
359
|
+
httpAction({
|
|
360
|
+
name: "post",
|
|
361
|
+
method: "POST"
|
|
362
|
+
}),
|
|
363
|
+
httpAction({
|
|
364
|
+
name: "put",
|
|
365
|
+
method: "PUT"
|
|
366
|
+
}),
|
|
367
|
+
httpAction({
|
|
368
|
+
name: "patch",
|
|
369
|
+
method: "PATCH"
|
|
370
|
+
}),
|
|
371
|
+
httpAction({
|
|
372
|
+
name: "delete",
|
|
373
|
+
method: "DELETE"
|
|
374
|
+
}),
|
|
375
|
+
httpAction({
|
|
376
|
+
name: "head",
|
|
377
|
+
method: "HEAD"
|
|
378
|
+
}),
|
|
379
|
+
httpAction({
|
|
380
|
+
name: "options",
|
|
381
|
+
method: "OPTIONS"
|
|
382
|
+
}),
|
|
383
|
+
serveAction(),
|
|
384
|
+
onAction()
|
|
385
|
+
];
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/matchers/header.ts
|
|
388
|
+
/**
|
|
389
|
+
* `expect res header "content-type"`: passes if the response carries the header.
|
|
390
|
+
*
|
|
391
|
+
* The lookup is case-insensitive, because header casing is whatever the far end
|
|
392
|
+
* happened to send.
|
|
393
|
+
*/
|
|
394
|
+
const header = defineMatcher({
|
|
395
|
+
name: "header",
|
|
396
|
+
args: [arg("name", t.string, "Which header to read."), optionalArg("value", t.string, "What it should say. Omit it to check the header is merely present.")],
|
|
397
|
+
appliesTo: "Response",
|
|
398
|
+
test: ({ subject, args }) => hasHeader(subject, String(args[0])),
|
|
399
|
+
message: ({ args }) => `expected the response to carry header "${String(args[0])}"`
|
|
400
|
+
});
|
|
401
|
+
function hasHeader(subject, name) {
|
|
402
|
+
const headers = subject.headers ?? {};
|
|
403
|
+
const target = name.toLowerCase();
|
|
404
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === target);
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/plugin.ts
|
|
408
|
+
/**
|
|
409
|
+
* The `http` namespace: the request verbs, `http.serve`/`http.on`, the `header`
|
|
410
|
+
* matcher and the types they trade in.
|
|
411
|
+
*
|
|
412
|
+
* Requires the `net` capability, so a host without it refuses the plugin at load
|
|
413
|
+
* time rather than failing mid-flow. The compiler treats it exactly as it treats
|
|
414
|
+
* a third-party plugin.
|
|
415
|
+
*/
|
|
416
|
+
const httpPlugin = definePlugin({
|
|
417
|
+
name: "venn/http",
|
|
418
|
+
version: "0.0.0",
|
|
419
|
+
namespace: "http",
|
|
420
|
+
requires: ["net"],
|
|
421
|
+
actions: httpActions,
|
|
422
|
+
matchers: [header],
|
|
423
|
+
typeDefs: {
|
|
424
|
+
/** One request, as the handler receives it. */
|
|
425
|
+
Request: t.record({
|
|
426
|
+
method: t.string,
|
|
427
|
+
url: t.string,
|
|
428
|
+
headers: t.map(t.string),
|
|
429
|
+
body: t.string
|
|
430
|
+
}),
|
|
431
|
+
/** What a handler may answer with. Anything else is taken as the body. */
|
|
432
|
+
Reply: t.record({
|
|
433
|
+
status: t.number,
|
|
434
|
+
headers: t.map(t.string),
|
|
435
|
+
body: t.dynamic
|
|
436
|
+
}, {
|
|
437
|
+
optional: [
|
|
438
|
+
"status",
|
|
439
|
+
"headers",
|
|
440
|
+
"body"
|
|
441
|
+
],
|
|
442
|
+
open: true
|
|
443
|
+
}),
|
|
444
|
+
/**
|
|
445
|
+
* A bound socket: the port it got, and how to give it back.
|
|
446
|
+
*
|
|
447
|
+
* Attaching a handler is `http.on`, so the handle does not offer that itself.
|
|
448
|
+
* The type names only what a program may do with the value, which is why it is
|
|
449
|
+
* opaque with two visible members rather than fully opaque.
|
|
450
|
+
*/
|
|
451
|
+
Server: t.opaque("http.Server", {
|
|
452
|
+
port: t.number,
|
|
453
|
+
close: t.fn([], t.void)
|
|
454
|
+
}),
|
|
455
|
+
/**
|
|
456
|
+
* One response, as the client verbs give it back.
|
|
457
|
+
*
|
|
458
|
+
* `body` is the raw text, always a string whatever came over the wire. `json`
|
|
459
|
+
* is that text parsed, and is the one field nothing can know the shape of: it
|
|
460
|
+
* is whatever the far end chose to send. Name a shape for it, as in
|
|
461
|
+
* `const price: Price = res.json`, and everything after reads as that shape.
|
|
462
|
+
*/
|
|
463
|
+
Response: t.record({
|
|
464
|
+
status: t.number,
|
|
465
|
+
ok: t.bool,
|
|
466
|
+
headers: t.map(t.string),
|
|
467
|
+
body: t.string,
|
|
468
|
+
json: t.dynamic,
|
|
469
|
+
time: t.number
|
|
470
|
+
}, { open: true })
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/server/http-server.errors.ts
|
|
475
|
+
/** VN7020: the address the flow asked for is already taken by something else. */
|
|
476
|
+
function portInUse(args) {
|
|
477
|
+
return new VennError({
|
|
478
|
+
code: "VN7020",
|
|
479
|
+
message: `Port ${args.port} is already in use — stop whatever is listening on ${args.host}:${args.port}, or ask for "port: 0" to take any free one.`,
|
|
480
|
+
detail: {
|
|
481
|
+
port: args.port,
|
|
482
|
+
host: args.host,
|
|
483
|
+
cause: "EADDRINUSE"
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/** VN7021: the socket refused to bind for any other reason. */
|
|
488
|
+
function listenFailed(args) {
|
|
489
|
+
return new VennError({
|
|
490
|
+
code: "VN7021",
|
|
491
|
+
message: `Could not listen on ${args.host}:${args.port} — ${args.cause}.`,
|
|
492
|
+
detail: {
|
|
493
|
+
port: args.port,
|
|
494
|
+
host: args.host,
|
|
495
|
+
cause: args.cause
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Whatever the socket threw, as a Venn error: VN7020 for `EADDRINUSE`, VN7021
|
|
501
|
+
* for anything else.
|
|
502
|
+
*
|
|
503
|
+
* The translation lives at the producer so no caller has to read a `node:net`
|
|
504
|
+
* errno to know what went wrong.
|
|
505
|
+
*/
|
|
506
|
+
function asListenError(args) {
|
|
507
|
+
const error = args.error;
|
|
508
|
+
if (error?.code === "EADDRINUSE") return portInUse({
|
|
509
|
+
port: args.port,
|
|
510
|
+
host: args.host
|
|
511
|
+
});
|
|
512
|
+
return listenFailed({
|
|
513
|
+
port: args.port,
|
|
514
|
+
host: args.host,
|
|
515
|
+
cause: error?.message ?? String(args.error)
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
519
|
+
//#region src/server/memory-server.ts
|
|
520
|
+
/** Where the double starts handing out ports when a flow asks for any free one. */
|
|
521
|
+
const EPHEMERAL_START = 49152;
|
|
522
|
+
/**
|
|
523
|
+
* The double: no socket, no network, no waiting.
|
|
524
|
+
*
|
|
525
|
+
* A test starts the flow that serves, hands it a request and reads the reply,
|
|
526
|
+
* running the same handler the real server would call. Ports are book-kept here
|
|
527
|
+
* rather than by the operating system, so tests beside each other never collide.
|
|
528
|
+
*/
|
|
529
|
+
function createMemoryServer() {
|
|
530
|
+
const started = [];
|
|
531
|
+
const bound = /* @__PURE__ */ new Set();
|
|
532
|
+
let next = EPHEMERAL_START;
|
|
533
|
+
return {
|
|
534
|
+
started,
|
|
535
|
+
listen: async ({ port, host, handle }) => {
|
|
536
|
+
const chosen = port === 0 ? next++ : port;
|
|
537
|
+
if (bound.has(chosen)) throw portInUse({
|
|
538
|
+
port: chosen,
|
|
539
|
+
host: host ?? "127.0.0.1"
|
|
540
|
+
});
|
|
541
|
+
bound.add(chosen);
|
|
542
|
+
const server = memoryServer({
|
|
543
|
+
port: chosen,
|
|
544
|
+
handle,
|
|
545
|
+
release: () => bound.delete(chosen)
|
|
546
|
+
});
|
|
547
|
+
started.push(server);
|
|
548
|
+
return server;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function memoryServer(args) {
|
|
553
|
+
const state = { closed: false };
|
|
554
|
+
return {
|
|
555
|
+
port: args.port,
|
|
556
|
+
get closed() {
|
|
557
|
+
return state.closed;
|
|
558
|
+
},
|
|
559
|
+
close: async () => {
|
|
560
|
+
state.closed = true;
|
|
561
|
+
args.release();
|
|
562
|
+
},
|
|
563
|
+
deliver: async (request) => await args.handle(fill(request)) ?? { status: 204 }
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function fill(request) {
|
|
567
|
+
return {
|
|
568
|
+
method: request.method ?? "GET",
|
|
569
|
+
url: request.url ?? "/",
|
|
570
|
+
headers: request.headers ?? {},
|
|
571
|
+
body: request.body ?? ""
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
//#endregion
|
|
575
|
+
export { HttpClientPort, HttpServerPort, asListenError, createFakeClient, createFetchClient, createMemoryServer, httpPlugin as default, httpPlugin, listenFailed, okResponse, onAction, portInUse, serveAction };
|
|
576
|
+
|
|
577
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["fill","hasHeader"],"sources":["../src/clients/fake-client.ts","../src/clients/fetch-client.ts","../src/server/on-action.ts","../src/server/http-server.port.ts","../src/server/serve-action.ts","../src/port/http-client.port.ts","../src/actions/build-request.ts","../src/actions/request-params.ts","../src/actions/http-action.ts","../src/actions/index.ts","../src/matchers/header.ts","../src/matchers/index.ts","../src/types.ts","../src/plugin.ts","../src/server/http-server.errors.ts","../src/server/memory-server.ts"],"sourcesContent":["import type { HttpClient, HttpResponse } from \"../port/index.js\";\n\n/**\n * A 200 response with a small JSON body, for tests that only care about a field\n * or two.\n *\n * @param overrides Fields to replace on the default response.\n * @returns A complete {@link HttpResponse}.\n */\nexport function okResponse(overrides: Partial<HttpResponse> = {}): HttpResponse {\n return {\n status: 200,\n ok: true,\n headers: {},\n body: '{\"ok\":true}',\n json: { ok: true },\n time: 0,\n ...overrides,\n };\n}\n\n/**\n * The double: answers from a table keyed by the request's full URL.\n *\n * A URL with no entry gets {@link okResponse}, so a test only has to name the\n * responses it cares about. Never touches the network.\n *\n * @param args.responses Canned responses, keyed by the URL the flow requests.\n */\nexport function createFakeClient(\n args: { responses?: Record<string, HttpResponse> } = {},\n): HttpClient {\n const responses = args.responses ?? {};\n return {\n request: async (req) => responses[req.url] ?? okResponse(),\n };\n}\n","import type { HttpClient, HttpResponse } from \"../port/index.js\";\n\n/**\n * The real client, over the global `fetch`. Requires the `net` capability.\n *\n * The body is always read as text and parsed afterwards, so a reply that claims\n * JSON but is not still arrives whole in `body` instead of throwing.\n */\nexport function createFetchClient(): HttpClient {\n return {\n async request(req): Promise<HttpResponse> {\n const response = await fetch(req.url, {\n method: req.method,\n headers: req.headers,\n body: req.body,\n signal: req.signal,\n });\n const body = await response.text();\n return mapResponse({\n status: response.status,\n ok: response.ok,\n headers: response.headers,\n body,\n });\n },\n };\n}\n\nfunction mapResponse(args: {\n status: number;\n ok: boolean;\n headers: Headers;\n body: string;\n}): HttpResponse {\n return {\n status: args.status,\n ok: args.ok,\n headers: toObject(args.headers),\n body: args.body,\n json: tryJson(args.body),\n time: 0,\n };\n}\n\nfunction toObject(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n headers.forEach((value, key) => {\n out[key] = value;\n });\n return out;\n}\n\nfunction tryJson(body: string): unknown {\n try {\n return JSON.parse(body);\n } catch {\n return undefined;\n }\n}\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport type { ServeHandle } from \"./serve-action.js\";\n\n/**\n * `http.on server handler`: say what the server answers with.\n *\n * The handler is an ordinary `fn`, so everything the language already does\n * applies inside it: it can call any verb, and it waits for what it reaches for\n * without saying so. Whatever it returns becomes the reply. Calling `http.on`\n * again replaces the handler.\n */\nexport function onAction(): ActionDefinition {\n return defineAction({\n name: \"on\",\n doc: \"Answer this server's requests with a function. `fn (req) => { … }` is the reply.\",\n // The callback type is what gives `req` its shape, so `http.on api req => …`\n // needs no annotation in the flow.\n args: [\n arg(\"server\", t.ref(\"http.Server\"), \"The handle `http.serve` gave back.\"),\n arg(\n \"handler\",\n t.callback([t.ref(\"http.Request\")], t.dynamic, 1),\n \"Called for every request. What it returns is the reply.\",\n ),\n ],\n result: t.void,\n run: (ctx, input) => {\n const server = asServer(input.args[0]);\n const handler = input.args[1];\n if (!server) throw new Error(\"`http.on` needs a server, as `http.serve` returns.\");\n if (handler === undefined) throw new Error(\"`http.on` needs a function to answer with.\");\n server.onRequest((request) => ctx.invoke(handler, [request]));\n return undefined;\n },\n });\n}\n\nfunction asServer(value: unknown): ServeHandle | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n return (value as ServeHandle).kind === \"http-server\" ? (value as ServeHandle) : undefined;\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { HttpServer } from \"./http-server.types.js\";\n\n/**\n * The port descriptor `http.serve` resolves through `ctx.port(...)`.\n *\n * Declares the `net` capability, so a host that cannot bind a socket refuses the\n * binding at load time rather than mid-flow.\n */\nexport const HttpServerPort: Port<HttpServer> = {\n id: \"venn.port.http-server\",\n version: 1,\n requires: [\"net\"],\n methods: [\"listen\"],\n};\n","import { type ActionDefinition, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { HttpServerPort } from \"./http-server.port.js\";\nimport type { ServerReply, ServerRequest } from \"./http-server.types.js\";\n\nconst serveParams = z.object({\n port: z.number().optional().describe(\"Which port to bind. 0 asks for any free one.\"),\n host: z.string().optional().describe(\"Which interface to bind. Defaults to 127.0.0.1.\"),\n});\n\n/**\n * The value `http.serve` hands back, seen by flows as `http.Server`.\n *\n * A server is not a request-response verb: it stays, and the requests arrive\n * afterwards. So the verb returns something the program holds on to: the port it\n * got, and what it may do with it.\n */\nexport interface ServeHandle {\n kind: \"http-server\";\n port: number;\n /** Deliver one request to whatever `handle` was last given. Used by tests. */\n deliver: (request: Partial<ServerRequest>) => Promise<ServerReply>;\n close: () => Promise<void>;\n /** Replace the handler. `http.on` is how a flow reaches this. */\n onRequest: (handle: (request: ServerRequest) => unknown) => void;\n}\n\n/**\n * `let api = http.serve { port: 8080 }`: start listening.\n *\n * The handler starts as a 404 and `http.on` replaces it, so a request arriving\n * before the flow has said what to do with it gets an answer instead of hanging.\n */\nexport function serveAction(): ActionDefinition {\n return defineAction({\n name: \"serve\",\n doc: \"Start an HTTP server and hand back a handle. `http.on` says what to answer.\",\n params: serveParams.optional(),\n result: t.ref(\"http.Server\"),\n run: async (ctx, input) => {\n const params = (input.params ?? {}) as { port?: number; host?: string };\n const state: HandlerState = { handle: notFound };\n const running = await ctx.port(HttpServerPort).listen({\n port: params.port ?? 0,\n host: params.host,\n handle: (request: ServerRequest) => state.handle(request),\n });\n return handle(running, state);\n },\n });\n}\n\ninterface HandlerState {\n handle: (request: ServerRequest) => ServerReply | Promise<ServerReply>;\n}\n\nfunction handle(\n running: { port: number; close: () => Promise<void> },\n state: HandlerState,\n): ServeHandle {\n return {\n kind: \"http-server\",\n port: running.port,\n close: () => running.close(),\n deliver: async (request) => state.handle(fill(request)),\n onRequest: (given) => {\n state.handle = async (request) => asReply(await given(request));\n },\n };\n}\n\n/**\n * Whatever the flow answered, as a reply. A map with a `status` is taken at its\n * word; anything else is the body, so `=> { ok: true }` does the obvious thing.\n */\nfunction asReply(value: unknown): ServerReply {\n if (value === undefined || value === null) return { status: 204 };\n if (isReplyShape(value)) return value as ServerReply;\n return { status: 200, body: value };\n}\n\nfunction isReplyShape(value: unknown): boolean {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const shape = value as Record<string, unknown>;\n return \"status\" in shape || \"headers\" in shape || \"body\" in shape;\n}\n\nfunction notFound(): ServerReply {\n return { status: 404, body: { error: \"This server has no handler yet.\" } };\n}\n\nfunction fill(request: Partial<ServerRequest>): ServerRequest {\n return {\n method: request.method ?? \"GET\",\n url: request.url ?? \"/\",\n headers: request.headers ?? {},\n body: request.body ?? \"\",\n };\n}\n","import type { Port } from \"@venn-lang/contracts\";\nimport type { HttpClient } from \"./http-client.types.js\";\n\n/**\n * The port descriptor an http verb resolves through `ctx.port(...)`.\n *\n * Declares the `net` capability, so a host that cannot open sockets refuses the\n * binding at load time with a readable diagnostic.\n */\nexport const HttpClientPort: Port<HttpClient> = {\n id: \"venn.port.http-client\",\n version: 1,\n requires: [\"net\"],\n methods: [\"request\"],\n};\n","import type { HttpRequest } from \"../port/index.js\";\nimport type { RequestParams } from \"./request.types.js\";\n\ninterface Payload {\n body?: string;\n contentType?: string;\n}\n\nexport interface BuildArgs {\n method: string;\n url: unknown;\n params: RequestParams;\n baseUrl: unknown;\n signal?: AbortSignal;\n}\n\n/** Turn one action call into the request the HttpClient port sends. */\nexport function buildRequest(args: BuildArgs): HttpRequest {\n const payload = bodyOf(args.params);\n return {\n method: args.method,\n url: withQuery(absoluteUrl(String(args.url ?? \"\"), args.baseUrl), args.params.query),\n headers: headersOf(args.params, payload.contentType),\n body: payload.body,\n signal: args.signal,\n };\n}\n\nconst BOUNDARY = \"----vennFormBoundary\";\n\n/** A map means JSON and a string means raw, unless `encode` says otherwise. */\nfunction bodyOf(params: RequestParams): Payload {\n if (params.body === undefined) return {};\n switch (params.encode ?? defaultEncoding(params.body)) {\n case \"raw\":\n return { body: String(params.body) };\n case \"form\":\n return { body: encode(asMap(params.body)), contentType: FORM_TYPE };\n case \"multipart\":\n return multipart(asMap(params.body));\n default:\n return { body: JSON.stringify(params.body), contentType: \"application/json\" };\n }\n}\n\nfunction defaultEncoding(body: unknown): string {\n return typeof body === \"string\" ? \"raw\" : \"json\";\n}\n\nconst FORM_TYPE = \"application/x-www-form-urlencoded\";\n\nfunction multipart(values: Record<string, string | number | boolean>): Payload {\n const parts = Object.entries(values).map(\n ([key, value]) =>\n `--${BOUNDARY}\\r\\nContent-Disposition: form-data; name=\"${key}\"\\r\\n\\r\\n${value}\\r\\n`,\n );\n return {\n body: `${parts.join(\"\")}--${BOUNDARY}--\\r\\n`,\n contentType: `multipart/form-data; boundary=${BOUNDARY}`,\n };\n}\n\nfunction asMap(body: unknown): Record<string, string | number | boolean> {\n return (body ?? {}) as Record<string, string | number | boolean>;\n}\n\nfunction headersOf(params: RequestParams, contentType: string | undefined): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(params.headers ?? {})) headers[key] = String(value);\n if (contentType && !hasHeader(headers, \"content-type\")) headers[\"Content-Type\"] = contentType;\n const auth = authorization(params);\n if (auth && !hasHeader(headers, \"authorization\")) headers.Authorization = auth;\n return headers;\n}\n\nfunction authorization(params: RequestParams): string | undefined {\n if (params.bearer) return `Bearer ${params.bearer}`;\n if (!params.basic) return undefined;\n return `Basic ${base64(`${params.basic.user}:${params.basic.pass}`)}`;\n}\n\nfunction withQuery(url: string, query: RequestParams[\"query\"]): string {\n const encoded = query ? encode(query) : \"\";\n if (!encoded) return url;\n return `${url}${url.includes(\"?\") ? \"&\" : \"?\"}${encoded}`;\n}\n\nfunction encode(values: Record<string, string | number | boolean>): string {\n const search = new URLSearchParams();\n for (const [key, value] of Object.entries(values)) search.append(key, String(value));\n return search.toString();\n}\n\nfunction hasHeader(headers: Record<string, string>, name: string): boolean {\n return Object.keys(headers).some((key) => key.toLowerCase() === name);\n}\n\nfunction base64(text: string): string {\n return btoa(text);\n}\n\n/** Resolve a relative path against `config.baseUrl`; absolute URLs pass through. */\nfunction absoluteUrl(url: string, baseUrl: unknown): string {\n if (typeof baseUrl !== \"string\" || baseUrl === \"\" || hasScheme(url)) return url;\n return `${baseUrl.replace(/\\/+$/, \"\")}/${url.replace(/^\\/+/, \"\")}`;\n}\n\nfunction hasScheme(url: string): boolean {\n return /^[a-z][a-z0-9+.-]*:\\/\\//i.test(url);\n}\n","import { type ZodType, z } from \"@venn-lang/sdk\";\nimport type { RequestParams } from \"./request.types.js\";\n\nconst scalar = z.union([z.string(), z.number(), z.boolean()]);\nconst scalarMap = z.record(z.string(), scalar);\n\n/**\n * Validation for {@link RequestParams}, the options map every http verb accepts.\n *\n * Each key carries its own `.describe()` because the editor reads this schema to\n * offer the keys, document them and reject the ones that do not exist. Keeping\n * the wording here keeps it beside the rule it describes.\n */\nexport const requestParams: ZodType<RequestParams> = z.object({\n headers: scalarMap\n .optional()\n .describe(\"Extra headers. Anything you set here wins over what Venn would infer.\"),\n query: scalarMap.optional().describe(\"Appended to the URL as a query string, encoded for you.\"),\n body: z\n .unknown()\n .optional()\n .describe(\"What to send. A map becomes JSON; a string is sent as written.\"),\n encode: z\n .enum([\"json\", \"form\", \"multipart\", \"raw\"])\n .optional()\n .describe(\"How to serialise `body`. Defaults to `json` for a map, `raw` for a string.\"),\n bearer: z.string().optional().describe(\"Shorthand for `Authorization: Bearer …`.\"),\n basic: z\n .object({ user: z.string(), pass: z.string() })\n .optional()\n .describe(\"Shorthand for HTTP basic auth.\"),\n});\n","import { type ActionDefinition, arg, defineAction } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { HttpClientPort } from \"../port/index.js\";\nimport { buildRequest } from \"./build-request.js\";\nimport type { RequestParams } from \"./request.types.js\";\nimport { requestParams } from \"./request-params.js\";\n\n/**\n * Build the action for one HTTP verb.\n *\n * Every verb has the same shape: the URL is the single positional argument and\n * everything else rides in the options map, so `http.get` and `http.post` read\n * alike at the call site.\n *\n * @param config.name The verb as a flow writes it, such as `get`.\n * @param config.method The method put on the wire, such as `GET`.\n */\nexport function httpAction(config: { name: string; method: string }): ActionDefinition {\n return defineAction({\n name: config.name,\n doc: `HTTP ${config.method} request.`,\n params: requestParams.optional(),\n args: [arg(\"url\", t.string, \"Where to send it. Relative paths join the configured base URL.\")],\n result: t.ref(\"http.Response\"),\n run: (ctx, input) =>\n ctx.port(HttpClientPort).request(\n buildRequest({\n method: config.method,\n url: input.args[0],\n params: (input.params ?? {}) as RequestParams,\n baseUrl: ctx.config.baseUrl,\n signal: ctx.signal,\n }),\n ),\n });\n}\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { onAction } from \"../server/on-action.js\";\nimport { serveAction } from \"../server/serve-action.js\";\nimport { httpAction } from \"./http-action.js\";\n\n/** The http namespace's verbs. Adding one is a single line here. */\nexport const httpActions: ActionDefinition[] = [\n httpAction({ name: \"get\", method: \"GET\" }),\n httpAction({ name: \"post\", method: \"POST\" }),\n httpAction({ name: \"put\", method: \"PUT\" }),\n httpAction({ name: \"patch\", method: \"PATCH\" }),\n httpAction({ name: \"delete\", method: \"DELETE\" }),\n httpAction({ name: \"head\", method: \"HEAD\" }),\n httpAction({ name: \"options\", method: \"OPTIONS\" }),\n // Serving, not requesting: a server stays, and the requests arrive later.\n serveAction(),\n onAction(),\n];\n","import { arg, defineMatcher, type MatcherDefinition, optionalArg } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\n\n/**\n * `expect res header \"content-type\"`: passes if the response carries the header.\n *\n * The lookup is case-insensitive, because header casing is whatever the far end\n * happened to send.\n */\nexport const header: MatcherDefinition = defineMatcher({\n name: \"header\",\n args: [\n arg(\"name\", t.string, \"Which header to read.\"),\n optionalArg(\n \"value\",\n t.string,\n \"What it should say. Omit it to check the header is merely present.\",\n ),\n ],\n appliesTo: \"Response\",\n test: ({ subject, args }) => hasHeader(subject, String(args[0])),\n message: ({ args }) => `expected the response to carry header \"${String(args[0])}\"`,\n});\n\nfunction hasHeader(subject: unknown, name: string): boolean {\n const headers = (subject as { headers?: Record<string, string> }).headers ?? {};\n const target = name.toLowerCase();\n return Object.keys(headers).some((key) => key.toLowerCase() === target);\n}\n","import type { MatcherDefinition } from \"@venn-lang/sdk\";\nimport { header } from \"./header.js\";\n\nexport const httpMatchers: MatcherDefinition[] = [header];\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types the plugin publishes to flows: `http.Request`, `http.Reply`,\n * `http.Server` and `http.Response`.\n *\n * They are data, not TypeScript, because the compiler and the editor read them\n * without loading the plugin's implementation. Keep them in step by hand with\n * `server/http-server.types.ts` and `port/http-client.types.ts`.\n */\nexport const httpTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /** One request, as the handler receives it. */\n Request: t.record({\n method: t.string,\n url: t.string,\n headers: t.map(t.string),\n body: t.string,\n }),\n /** What a handler may answer with. Anything else is taken as the body. */\n Reply: t.record(\n { status: t.number, headers: t.map(t.string), body: t.dynamic },\n { optional: [\"status\", \"headers\", \"body\"], open: true },\n ),\n /**\n * A bound socket: the port it got, and how to give it back.\n *\n * Attaching a handler is `http.on`, so the handle does not offer that itself.\n * The type names only what a program may do with the value, which is why it is\n * opaque with two visible members rather than fully opaque.\n */\n Server: t.opaque(\"http.Server\", { port: t.number, close: t.fn([], t.void) }),\n /**\n * One response, as the client verbs give it back.\n *\n * `body` is the raw text, always a string whatever came over the wire. `json`\n * is that text parsed, and is the one field nothing can know the shape of: it\n * is whatever the far end chose to send. Name a shape for it, as in\n * `const price: Price = res.json`, and everything after reads as that shape.\n */\n Response: t.record(\n {\n status: t.number,\n ok: t.bool,\n headers: t.map(t.string),\n body: t.string,\n json: t.dynamic,\n time: t.number,\n },\n { open: true },\n ),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { httpActions } from \"./actions/index.js\";\nimport { httpMatchers } from \"./matchers/index.js\";\nimport { httpTypeDefs } from \"./types.js\";\n\n/**\n * The `http` namespace: the request verbs, `http.serve`/`http.on`, the `header`\n * matcher and the types they trade in.\n *\n * Requires the `net` capability, so a host without it refuses the plugin at load\n * time rather than failing mid-flow. The compiler treats it exactly as it treats\n * a third-party plugin.\n */\nexport const httpPlugin: PluginDefinition = definePlugin({\n name: \"venn/http\",\n version: \"0.0.0\",\n namespace: \"http\",\n requires: [\"net\"],\n actions: httpActions,\n matchers: httpMatchers,\n typeDefs: httpTypeDefs,\n});\n","import { VennError } from \"@venn-lang/contracts\";\n\n/** VN7020: the address the flow asked for is already taken by something else. */\nexport function portInUse(args: { port: number; host: string }): VennError {\n return new VennError({\n code: \"VN7020\",\n message: `Port ${args.port} is already in use — stop whatever is listening on ${args.host}:${args.port}, or ask for \"port: 0\" to take any free one.`,\n detail: { port: args.port, host: args.host, cause: \"EADDRINUSE\" },\n });\n}\n\n/** VN7021: the socket refused to bind for any other reason. */\nexport function listenFailed(args: { port: number; host: string; cause: string }): VennError {\n return new VennError({\n code: \"VN7021\",\n message: `Could not listen on ${args.host}:${args.port} — ${args.cause}.`,\n detail: { port: args.port, host: args.host, cause: args.cause },\n });\n}\n\n/**\n * Whatever the socket threw, as a Venn error: VN7020 for `EADDRINUSE`, VN7021\n * for anything else.\n *\n * The translation lives at the producer so no caller has to read a `node:net`\n * errno to know what went wrong.\n */\nexport function asListenError(args: { port: number; host: string; error: unknown }): VennError {\n const error = args.error as { code?: string; message?: string } | undefined;\n if (error?.code === \"EADDRINUSE\") return portInUse({ port: args.port, host: args.host });\n return listenFailed({\n port: args.port,\n host: args.host,\n cause: error?.message ?? String(args.error),\n });\n}\n","import { portInUse } from \"./http-server.errors.js\";\nimport type {\n HttpServer,\n RequestHandler,\n RunningServer,\n ServerReply,\n ServerRequest,\n} from \"./http-server.types.js\";\n\n/** Where the double starts handing out ports when a flow asks for any free one. */\nconst EPHEMERAL_START = 49152;\n\n/** A server that is listening in memory, plus the way to knock on its door. */\nexport interface MemoryServer extends RunningServer {\n /** Deliver a request as though it had arrived over the network. */\n deliver(request: Partial<ServerRequest>): Promise<ServerReply>;\n readonly closed: boolean;\n}\n\n/** The double factory, which keeps every server it started so a test can find it. */\nexport interface MemoryHttpServer extends HttpServer {\n /** Every server started through this, newest last. */\n readonly started: readonly MemoryServer[];\n}\n\n/**\n * The double: no socket, no network, no waiting.\n *\n * A test starts the flow that serves, hands it a request and reads the reply,\n * running the same handler the real server would call. Ports are book-kept here\n * rather than by the operating system, so tests beside each other never collide.\n */\nexport function createMemoryServer(): MemoryHttpServer {\n const started: MemoryServer[] = [];\n const bound = new Set<number>();\n let next = EPHEMERAL_START;\n return {\n started,\n // Binding the same port twice must fail here exactly as it would against a\n // real socket, so the double tracks what it has handed out.\n listen: async ({ port, host, handle }) => {\n const chosen = port === 0 ? next++ : port;\n if (bound.has(chosen)) throw portInUse({ port: chosen, host: host ?? \"127.0.0.1\" });\n bound.add(chosen);\n const server = memoryServer({ port: chosen, handle, release: () => bound.delete(chosen) });\n started.push(server);\n return server;\n },\n };\n}\n\nfunction memoryServer(args: {\n port: number;\n handle: RequestHandler;\n release: () => void;\n}): MemoryServer {\n const state = { closed: false };\n return {\n port: args.port,\n get closed() {\n return state.closed;\n },\n close: async () => {\n state.closed = true;\n args.release();\n },\n deliver: async (request) => (await args.handle(fill(request))) ?? { status: 204 },\n };\n}\n\nfunction fill(request: Partial<ServerRequest>): ServerRequest {\n return {\n method: request.method ?? \"GET\",\n url: request.url ?? \"/\",\n headers: request.headers ?? {},\n body: request.body ?? \"\",\n };\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,WAAW,YAAmC,CAAC,GAAiB;CAC9E,OAAO;EACL,QAAQ;EACR,IAAI;EACJ,SAAS,CAAC;EACV,MAAM;EACN,MAAM,EAAE,IAAI,KAAK;EACjB,MAAM;EACN,GAAG;CACL;AACF;;;;;;;;;AAUA,SAAgB,iBACd,OAAqD,CAAC,GAC1C;CACZ,MAAM,YAAY,KAAK,aAAa,CAAC;CACrC,OAAO,EACL,SAAS,OAAO,QAAQ,UAAU,IAAI,QAAQ,WAAW,EAC3D;AACF;;;;;;;;;AC5BA,SAAgB,oBAAgC;CAC9C,OAAO,EACL,MAAM,QAAQ,KAA4B;EACxC,MAAM,WAAW,MAAM,MAAM,IAAI,KAAK;GACpC,QAAQ,IAAI;GACZ,SAAS,IAAI;GACb,MAAM,IAAI;GACV,QAAQ,IAAI;EACd,CAAC;EACD,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,OAAO,YAAY;GACjB,QAAQ,SAAS;GACjB,IAAI,SAAS;GACb,SAAS,SAAS;GAClB;EACF,CAAC;CACH,EACF;AACF;AAEA,SAAS,YAAY,MAKJ;CACf,OAAO;EACL,QAAQ,KAAK;EACb,IAAI,KAAK;EACT,SAAS,SAAS,KAAK,OAAO;EAC9B,MAAM,KAAK;EACX,MAAM,QAAQ,KAAK,IAAI;EACvB,MAAM;CACR;AACF;AAEA,SAAS,SAAS,SAA0C;CAC1D,MAAM,MAA8B,CAAC;CACrC,QAAQ,SAAS,OAAO,QAAQ;EAC9B,IAAI,OAAO;CACb,CAAC;CACD,OAAO;AACT;AAEA,SAAS,QAAQ,MAAuB;CACtC,IAAI;EACF,OAAO,KAAK,MAAM,IAAI;CACxB,QAAQ;EACN;CACF;AACF;;;;;;;;;;;AC9CA,SAAgB,WAA6B;CAC3C,OAAO,aAAa;EAClB,MAAM;EACN,KAAK;EAGL,MAAM,CACJ,IAAI,UAAU,EAAE,IAAI,aAAa,GAAG,oCAAoC,GACxE,IACE,WACA,EAAE,SAAS,CAAC,EAAE,IAAI,cAAc,CAAC,GAAG,EAAE,SAAS,CAAC,GAChD,yDACF,CACF;EACA,QAAQ,EAAE;EACV,MAAM,KAAK,UAAU;GACnB,MAAM,SAAS,SAAS,MAAM,KAAK,EAAE;GACrC,MAAM,UAAU,MAAM,KAAK;GAC3B,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,oDAAoD;GACjF,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,MAAM,4CAA4C;GACvF,OAAO,WAAW,YAAY,IAAI,OAAO,SAAS,CAAC,OAAO,CAAC,CAAC;EAE9D;CACF,CAAC;AACH;AAEA,SAAS,SAAS,OAAyC;CACzD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,KAAA;CACxD,OAAQ,MAAsB,SAAS,gBAAiB,QAAwB,KAAA;AAClF;;;;;;;;;AChCA,MAAa,iBAAmC;CAC9C,IAAI;CACJ,SAAS;CACT,UAAU,CAAC,KAAK;CAChB,SAAS,CAAC,QAAQ;AACpB;;;ACTA,MAAM,cAAc,EAAE,OAAO;CAC3B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,8CAA8C;CACnF,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,iDAAiD;AACxF,CAAC;;;;;;;AAyBD,SAAgB,cAAgC;CAC9C,OAAO,aAAa;EAClB,MAAM;EACN,KAAK;EACL,QAAQ,YAAY,SAAS;EAC7B,QAAQ,EAAE,IAAI,aAAa;EAC3B,KAAK,OAAO,KAAK,UAAU;GACzB,MAAM,SAAU,MAAM,UAAU,CAAC;GACjC,MAAM,QAAsB,EAAE,QAAQ,SAAS;GAM/C,OAAO,OAAO,MALQ,IAAI,KAAK,cAAc,CAAC,CAAC,OAAO;IACpD,MAAM,OAAO,QAAQ;IACrB,MAAM,OAAO;IACb,SAAS,YAA2B,MAAM,OAAO,OAAO;GAC1D,CAAC,GACsB,KAAK;EAC9B;CACF,CAAC;AACH;AAMA,SAAS,OACP,SACA,OACa;CACb,OAAO;EACL,MAAM;EACN,MAAM,QAAQ;EACd,aAAa,QAAQ,MAAM;EAC3B,SAAS,OAAO,YAAY,MAAM,OAAOA,OAAK,OAAO,CAAC;EACtD,YAAY,UAAU;GACpB,MAAM,SAAS,OAAO,YAAY,QAAQ,MAAM,MAAM,OAAO,CAAC;EAChE;CACF;AACF;;;;;AAMA,SAAS,QAAQ,OAA6B;CAC5C,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO,EAAE,QAAQ,IAAI;CAChE,IAAI,aAAa,KAAK,GAAG,OAAO;CAChC,OAAO;EAAE,QAAQ;EAAK,MAAM;CAAM;AACpC;AAEA,SAAS,aAAa,OAAyB;CAC7C,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG,OAAO;CAChF,MAAM,QAAQ;CACd,OAAO,YAAY,SAAS,aAAa,SAAS,UAAU;AAC9D;AAEA,SAAS,WAAwB;CAC/B,OAAO;EAAE,QAAQ;EAAK,MAAM,EAAE,OAAO,kCAAkC;CAAE;AAC3E;AAEA,SAASA,OAAK,SAAgD;CAC5D,OAAO;EACL,QAAQ,QAAQ,UAAU;EAC1B,KAAK,QAAQ,OAAO;EACpB,SAAS,QAAQ,WAAW,CAAC;EAC7B,MAAM,QAAQ,QAAQ;CACxB;AACF;;;;;;;;;ACzFA,MAAa,iBAAmC;CAC9C,IAAI;CACJ,SAAS;CACT,UAAU,CAAC,KAAK;CAChB,SAAS,CAAC,SAAS;AACrB;;;;ACGA,SAAgB,aAAa,MAA8B;CACzD,MAAM,UAAU,OAAO,KAAK,MAAM;CAClC,OAAO;EACL,QAAQ,KAAK;EACb,KAAK,UAAU,YAAY,OAAO,KAAK,OAAO,EAAE,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO,KAAK;EACnF,SAAS,UAAU,KAAK,QAAQ,QAAQ,WAAW;EACnD,MAAM,QAAQ;EACd,QAAQ,KAAK;CACf;AACF;AAEA,MAAM,WAAW;;AAGjB,SAAS,OAAO,QAAgC;CAC9C,IAAI,OAAO,SAAS,KAAA,GAAW,OAAO,CAAC;CACvC,QAAQ,OAAO,UAAU,gBAAgB,OAAO,IAAI,GAApD;EACE,KAAK,OACH,OAAO,EAAE,MAAM,OAAO,OAAO,IAAI,EAAE;EACrC,KAAK,QACH,OAAO;GAAE,MAAM,OAAO,MAAM,OAAO,IAAI,CAAC;GAAG,aAAa;EAAU;EACpE,KAAK,aACH,OAAO,UAAU,MAAM,OAAO,IAAI,CAAC;EACrC,SACE,OAAO;GAAE,MAAM,KAAK,UAAU,OAAO,IAAI;GAAG,aAAa;EAAmB;CAChF;AACF;AAEA,SAAS,gBAAgB,MAAuB;CAC9C,OAAO,OAAO,SAAS,WAAW,QAAQ;AAC5C;AAEA,MAAM,YAAY;AAElB,SAAS,UAAU,QAA4D;CAK7E,OAAO;EACL,MAAM,GALM,OAAO,QAAQ,MAAM,CAAC,CAAC,KAClC,CAAC,KAAK,WACL,KAAK,SAAS,4CAA4C,IAAI,WAAW,MAAM,KAGpE,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,SAAS;EACrC,aAAa,iCAAiC;CAChD;AACF;AAEA,SAAS,MAAM,MAA0D;CACvE,OAAQ,QAAQ,CAAC;AACnB;AAEA,SAAS,UAAU,QAAuB,aAAyD;CACjG,MAAM,UAAkC,CAAC;CACzC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,WAAW,CAAC,CAAC,GAAG,QAAQ,OAAO,OAAO,KAAK;CAC5F,IAAI,eAAe,CAACC,YAAU,SAAS,cAAc,GAAG,QAAQ,kBAAkB;CAClF,MAAM,OAAO,cAAc,MAAM;CACjC,IAAI,QAAQ,CAACA,YAAU,SAAS,eAAe,GAAG,QAAQ,gBAAgB;CAC1E,OAAO;AACT;AAEA,SAAS,cAAc,QAA2C;CAChE,IAAI,OAAO,QAAQ,OAAO,UAAU,OAAO;CAC3C,IAAI,CAAC,OAAO,OAAO,OAAO,KAAA;CAC1B,OAAO,SAAS,OAAO,GAAG,OAAO,MAAM,KAAK,GAAG,OAAO,MAAM,MAAM;AACpE;AAEA,SAAS,UAAU,KAAa,OAAuC;CACrE,MAAM,UAAU,QAAQ,OAAO,KAAK,IAAI;CACxC,IAAI,CAAC,SAAS,OAAO;CACrB,OAAO,GAAG,MAAM,IAAI,SAAS,GAAG,IAAI,MAAM,MAAM;AAClD;AAEA,SAAS,OAAO,QAA2D;CACzE,MAAM,SAAS,IAAI,gBAAgB;CACnC,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG,OAAO,OAAO,KAAK,OAAO,KAAK,CAAC;CACnF,OAAO,OAAO,SAAS;AACzB;AAEA,SAASA,YAAU,SAAiC,MAAuB;CACzE,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,QAAQ,IAAI,YAAY,MAAM,IAAI;AACtE;AAEA,SAAS,OAAO,MAAsB;CACpC,OAAO,KAAK,IAAI;AAClB;;AAGA,SAAS,YAAY,KAAa,SAA0B;CAC1D,IAAI,OAAO,YAAY,YAAY,YAAY,MAAM,UAAU,GAAG,GAAG,OAAO;CAC5E,OAAO,GAAG,QAAQ,QAAQ,QAAQ,EAAE,EAAE,GAAG,IAAI,QAAQ,QAAQ,EAAE;AACjE;AAEA,SAAS,UAAU,KAAsB;CACvC,OAAO,2BAA2B,KAAK,GAAG;AAC5C;;;AC1GA,MAAM,SAAS,EAAE,MAAM;CAAC,EAAE,OAAO;CAAG,EAAE,OAAO;CAAG,EAAE,QAAQ;AAAC,CAAC;AAC5D,MAAM,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM;;;;;;;;AAS7C,MAAa,gBAAwC,EAAE,OAAO;CAC5D,SAAS,UACN,SAAS,CAAC,CACV,SAAS,uEAAuE;CACnF,OAAO,UAAU,SAAS,CAAC,CAAC,SAAS,yDAAyD;CAC9F,MAAM,EACH,QAAQ,CAAC,CACT,SAAS,CAAC,CACV,SAAS,gEAAgE;CAC5E,QAAQ,EACL,KAAK;EAAC;EAAQ;EAAQ;EAAa;CAAK,CAAC,CAAC,CAC1C,SAAS,CAAC,CACV,SAAS,4EAA4E;CACxF,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,0CAA0C;CACjF,OAAO,EACJ,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,MAAM,EAAE,OAAO;CAAE,CAAC,CAAC,CAC9C,SAAS,CAAC,CACV,SAAS,gCAAgC;AAC9C,CAAC;;;;;;;;;;;;;ACdD,SAAgB,WAAW,QAA4D;CACrF,OAAO,aAAa;EAClB,MAAM,OAAO;EACb,KAAK,QAAQ,OAAO,OAAO;EAC3B,QAAQ,cAAc,SAAS;EAC/B,MAAM,CAAC,IAAI,OAAO,EAAE,QAAQ,gEAAgE,CAAC;EAC7F,QAAQ,EAAE,IAAI,eAAe;EAC7B,MAAM,KAAK,UACT,IAAI,KAAK,cAAc,CAAC,CAAC,QACvB,aAAa;GACX,QAAQ,OAAO;GACf,KAAK,MAAM,KAAK;GAChB,QAAS,MAAM,UAAU,CAAC;GAC1B,SAAS,IAAI,OAAO;GACpB,QAAQ,IAAI;EACd,CAAC,CACH;CACJ,CAAC;AACH;;;;AC7BA,MAAa,cAAkC;CAC7C,WAAW;EAAE,MAAM;EAAO,QAAQ;CAAM,CAAC;CACzC,WAAW;EAAE,MAAM;EAAQ,QAAQ;CAAO,CAAC;CAC3C,WAAW;EAAE,MAAM;EAAO,QAAQ;CAAM,CAAC;CACzC,WAAW;EAAE,MAAM;EAAS,QAAQ;CAAQ,CAAC;CAC7C,WAAW;EAAE,MAAM;EAAU,QAAQ;CAAS,CAAC;CAC/C,WAAW;EAAE,MAAM;EAAQ,QAAQ;CAAO,CAAC;CAC3C,WAAW;EAAE,MAAM;EAAW,QAAQ;CAAU,CAAC;CAEjD,YAAY;CACZ,SAAS;AACX;;;;;;;;;ACRA,MAAa,SAA4B,cAAc;CACrD,MAAM;CACN,MAAM,CACJ,IAAI,QAAQ,EAAE,QAAQ,uBAAuB,GAC7C,YACE,SACA,EAAE,QACF,oEACF,CACF;CACA,WAAW;CACX,OAAO,EAAE,SAAS,WAAW,UAAU,SAAS,OAAO,KAAK,EAAE,CAAC;CAC/D,UAAU,EAAE,WAAW,0CAA0C,OAAO,KAAK,EAAE,EAAE;AACnF,CAAC;AAED,SAAS,UAAU,SAAkB,MAAuB;CAC1D,MAAM,UAAW,QAAiD,WAAW,CAAC;CAC9E,MAAM,SAAS,KAAK,YAAY;CAChC,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,MAAM,QAAQ,IAAI,YAAY,MAAM,MAAM;AACxE;;;;;;;;;;;AGfA,MAAa,aAA+B,aAAa;CACvD,MAAM;CACN,SAAS;CACT,WAAW;CACX,UAAU,CAAC,KAAK;CAChB,SAAS;CACT,UAAU,CFhBsC,MEgBtC;CACV,UAAU;;EDRV,SAAS,EAAE,OAAO;GAChB,QAAQ,EAAE;GACV,KAAK,EAAE;GACP,SAAS,EAAE,IAAI,EAAE,MAAM;GACvB,MAAM,EAAE;EACV,CAAC;;EAED,OAAO,EAAE,OACP;GAAE,QAAQ,EAAE;GAAQ,SAAS,EAAE,IAAI,EAAE,MAAM;GAAG,MAAM,EAAE;EAAQ,GAC9D;GAAE,UAAU;IAAC;IAAU;IAAW;GAAM;GAAG,MAAM;EAAK,CACxD;;;;;;;;EAQA,QAAQ,EAAE,OAAO,eAAe;GAAE,MAAM,EAAE;GAAQ,OAAO,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI;EAAE,CAAC;;;;;;;;;EAS3E,UAAU,EAAE,OACV;GACE,QAAQ,EAAE;GACV,IAAI,EAAE;GACN,SAAS,EAAE,IAAI,EAAE,MAAM;GACvB,MAAM,EAAE;GACR,MAAM,EAAE;GACR,MAAM,EAAE;EACV,GACA,EAAE,MAAM,KAAK,CACf;CC7BU;AACZ,CAAC;;;;AClBD,SAAgB,UAAU,MAAiD;CACzE,OAAO,IAAI,UAAU;EACnB,MAAM;EACN,SAAS,QAAQ,KAAK,KAAK,qDAAqD,KAAK,KAAK,GAAG,KAAK,KAAK;EACvG,QAAQ;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,OAAO;EAAa;CAClE,CAAC;AACH;;AAGA,SAAgB,aAAa,MAAgE;CAC3F,OAAO,IAAI,UAAU;EACnB,MAAM;EACN,SAAS,uBAAuB,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,KAAK,MAAM;EACvE,QAAQ;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,OAAO,KAAK;EAAM;CAChE,CAAC;AACH;;;;;;;;AASA,SAAgB,cAAc,MAAiE;CAC7F,MAAM,QAAQ,KAAK;CACnB,IAAI,OAAO,SAAS,cAAc,OAAO,UAAU;EAAE,MAAM,KAAK;EAAM,MAAM,KAAK;CAAK,CAAC;CACvF,OAAO,aAAa;EAClB,MAAM,KAAK;EACX,MAAM,KAAK;EACX,OAAO,OAAO,WAAW,OAAO,KAAK,KAAK;CAC5C,CAAC;AACH;;;;ACzBA,MAAM,kBAAkB;;;;;;;;AAsBxB,SAAgB,qBAAuC;CACrD,MAAM,UAA0B,CAAC;CACjC,MAAM,wBAAQ,IAAI,IAAY;CAC9B,IAAI,OAAO;CACX,OAAO;EACL;EAGA,QAAQ,OAAO,EAAE,MAAM,MAAM,aAAa;GACxC,MAAM,SAAS,SAAS,IAAI,SAAS;GACrC,IAAI,MAAM,IAAI,MAAM,GAAG,MAAM,UAAU;IAAE,MAAM;IAAQ,MAAM,QAAQ;GAAY,CAAC;GAClF,MAAM,IAAI,MAAM;GAChB,MAAM,SAAS,aAAa;IAAE,MAAM;IAAQ;IAAQ,eAAe,MAAM,OAAO,MAAM;GAAE,CAAC;GACzF,QAAQ,KAAK,MAAM;GACnB,OAAO;EACT;CACF;AACF;AAEA,SAAS,aAAa,MAIL;CACf,MAAM,QAAQ,EAAE,QAAQ,MAAM;CAC9B,OAAO;EACL,MAAM,KAAK;EACX,IAAI,SAAS;GACX,OAAO,MAAM;EACf;EACA,OAAO,YAAY;GACjB,MAAM,SAAS;GACf,KAAK,QAAQ;EACf;EACA,SAAS,OAAO,YAAa,MAAM,KAAK,OAAO,KAAK,OAAO,CAAC,KAAM,EAAE,QAAQ,IAAI;CAClF;AACF;AAEA,SAAS,KAAK,SAAgD;CAC5D,OAAO;EACL,QAAQ,QAAQ,UAAU;EAC1B,KAAK,QAAQ,OAAO;EACpB,SAAS,QAAQ,WAAW,CAAC;EAC7B,MAAM,QAAQ,QAAQ;CACxB;AACF"}
|