@enricai/barnacle 1.4.1 → 1.5.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/dist/scraper/flow-runner.d.ts +12 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +37 -0
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts +41 -0
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +49 -2
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts +19 -4
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +21 -6
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +108 -7
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +315 -41
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/site-plugin.d.ts +5 -3
- package/dist/site-plugin.d.ts.map +1 -1
- package/package.json +48 -24
|
@@ -20,20 +20,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.resolveStepPayloadField = resolveStepPayloadField;
|
|
21
21
|
exports.inferZodSchemaFromSamples = inferZodSchemaFromSamples;
|
|
22
22
|
exports.selectPayloadAction = selectPayloadAction;
|
|
23
|
+
exports.extractActionSequence = extractActionSequence;
|
|
24
|
+
exports.indexStateValues = indexStateValues;
|
|
25
|
+
exports.compileActionSteps = compileActionSteps;
|
|
23
26
|
exports.loadQuestionPromptKeywords = loadQuestionPromptKeywords;
|
|
24
27
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
25
28
|
exports.emitContractTs = emitContractTs;
|
|
26
29
|
exports.emitConfigManifest = emitConfigManifest;
|
|
27
30
|
exports.emitBrowserFlowTs = emitBrowserFlowTs;
|
|
31
|
+
exports.emitIndexTs = emitIndexTs;
|
|
28
32
|
const node_fs_1 = require("node:fs");
|
|
29
33
|
const node_path_1 = require("node:path");
|
|
30
34
|
const errors_1 = require("../lib/errors");
|
|
31
35
|
const logging_1 = require("../lib/logging");
|
|
36
|
+
const plugin_api_version_1 = require("../plugins/plugin-api-version");
|
|
32
37
|
const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
|
|
33
38
|
const load_vocabulary_1 = require("../recon/load-vocabulary");
|
|
34
39
|
const vocabulary_1 = require("../recon/vocabulary");
|
|
35
40
|
const recon_shared_1 = require("../scripts/recon-shared");
|
|
36
41
|
const logger = (0, logging_1.getScriptLogger)("recon-generate");
|
|
42
|
+
/**
|
|
43
|
+
* Engine imports in GENERATED code must be package subpaths, never the `@/`
|
|
44
|
+
* alias, and the reason is not visible from the source: `tsc-alias` rewrites by
|
|
45
|
+
* text, so it cannot tell an import this module *uses* from one it *emits as a
|
|
46
|
+
* string*. Written as `@/scraper/session`, the build silently rewrote the
|
|
47
|
+
* template literal itself — shipping `dist/` emitters that generated
|
|
48
|
+
* `../scraper/session` and left every out-of-tree consumer with TS2307.
|
|
49
|
+
* (`@/sites/...` survived only because `src/sites/` is empty, so it resolved to
|
|
50
|
+
* no file.) A bare specifier has nothing to resolve against, so the build leaves
|
|
51
|
+
* it alone. `out-of-tree-e2e.test.ts` asserts this against the BUILT dist —
|
|
52
|
+
* asserting it against the source would pass while the shipped artifact is broken.
|
|
53
|
+
*/
|
|
54
|
+
const ENGINE_PKG = "@enricai/barnacle";
|
|
37
55
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
38
56
|
function toPascalCase(siteId) {
|
|
39
57
|
return siteId
|
|
@@ -406,9 +424,29 @@ const TELEMETRY_URL_PATTERNS = [
|
|
|
406
424
|
.map((p) => p.trim())
|
|
407
425
|
.filter(Boolean),
|
|
408
426
|
];
|
|
427
|
+
/**
|
|
428
|
+
* A POST to a path whose own segment is `error`/`errors` is a client-side
|
|
429
|
+
* reporting sink, never a call a caller wants replayed.
|
|
430
|
+
*
|
|
431
|
+
* Emitting one is worse than noise. A browser's error reports are frozen at
|
|
432
|
+
* recon time, so the generated plugin re-POSTs a crash that never happened —
|
|
433
|
+
* a stack trace and timestamp from the recon run, sent to the site on every
|
|
434
|
+
* invocation, describing a failure in a page the plugin never loaded.
|
|
435
|
+
*
|
|
436
|
+
* Matched on a whole path segment rather than by substring so `/error-codes`
|
|
437
|
+
* and `/terrorism-screening` stay data endpoints, and kept out of
|
|
438
|
+
* TELEMETRY_URL_PATTERNS because that list is literal substrings — a site's own
|
|
439
|
+
* sink is structural, not an ad-tech domain the operator must enumerate.
|
|
440
|
+
*/
|
|
441
|
+
const ERROR_SINK_PATH_SEGMENT = /(^|\/)errors?(\/|$)/i;
|
|
409
442
|
/**
|
|
410
443
|
* Extracts the ordered sequence of meaningful POSTs that represent the
|
|
411
|
-
* transactional flow
|
|
444
|
+
* transactional flow: same-host 2xx POSTs, minus telemetry and error-reporting
|
|
445
|
+
* sinks. Assets need no filter of their own — they arrive as GETs.
|
|
446
|
+
*
|
|
447
|
+
* Exported for tests: this predicate decides what a generated plugin will POST
|
|
448
|
+
* at a live site, and it is the only gate between a browser's incidental
|
|
449
|
+
* chatter and the emitted hot path.
|
|
412
450
|
*/
|
|
413
451
|
function extractActionSequence(captures, baseUrl) {
|
|
414
452
|
let host;
|
|
@@ -436,6 +474,13 @@ function extractActionSequence(captures, baseUrl) {
|
|
|
436
474
|
return false;
|
|
437
475
|
if (TELEMETRY_URL_PATTERNS.some((p) => capture.url.includes(p)))
|
|
438
476
|
return false;
|
|
477
|
+
try {
|
|
478
|
+
if (ERROR_SINK_PATH_SEGMENT.test(new URL(capture.url).pathname))
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
439
484
|
return true;
|
|
440
485
|
});
|
|
441
486
|
}
|
|
@@ -663,13 +708,16 @@ function detectFormSchemaFieldNames(captures) {
|
|
|
663
708
|
return { fieldNameMap, fieldOptionsMap, allSchemaUuids };
|
|
664
709
|
}
|
|
665
710
|
/**
|
|
666
|
-
* Walks a response body collecting
|
|
667
|
-
*
|
|
668
|
-
* `Id` of an object that has `OptionSourceCode`/`StringKey`/`FieldOptions`
|
|
669
|
-
* sibling — i.e. an OptionId in the form schema. These UUIDs are stable
|
|
711
|
+
* Walks a response body collecting UUID-shaped strings under a `FieldId` key, or
|
|
712
|
+
* under the `Id` of an entry in a sibling `FieldOptions` array. These are stable
|
|
670
713
|
* schema anchors that must be shielded from state-threading even when
|
|
671
|
-
* detectFormSchemaFieldNames
|
|
672
|
-
*
|
|
714
|
+
* detectFormSchemaFieldNames emits no payload-mappable name for the field (e.g.
|
|
715
|
+
* when the field's FieldName is too long for our naming heuristic).
|
|
716
|
+
*
|
|
717
|
+
* The key names are exact by design, not an oversight: a differing wire format
|
|
718
|
+
* (a lowercase `fieldId`, another vendor's option key) is the consumer's to
|
|
719
|
+
* declare, not the engine's to guess — see issue #57. Matching case variants
|
|
720
|
+
* here would re-broaden the very fingerprint that issue exists to narrow.
|
|
673
721
|
*/
|
|
674
722
|
function walkForSchemaUuids(value, out) {
|
|
675
723
|
if (value === null || typeof value !== "object")
|
|
@@ -1057,12 +1105,35 @@ const MAX_STATE_VALUE_LENGTH = 256;
|
|
|
1057
1105
|
* does not exist". Closed set, literal-string match — never expand to
|
|
1058
1106
|
* pattern-based detection (would trip the no-regex-on-open-sets rule). */
|
|
1059
1107
|
const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"]);
|
|
1108
|
+
/**
|
|
1109
|
+
* Splits a raw `Set-Cookie` response-header string into `name`/`value` pairs.
|
|
1110
|
+
* Captures store `responseHeaders` as a flat `Record<string, string>`
|
|
1111
|
+
* (see recon-shared.ts's `Capture`), so multiple `Set-Cookie` headers from the
|
|
1112
|
+
* same response — if the recon browser's CDP session folds them together —
|
|
1113
|
+
* would already have lost their individual boundaries before reaching here;
|
|
1114
|
+
* this only recovers name/value pairs from whatever single string survives.
|
|
1115
|
+
*/
|
|
1116
|
+
function* walkSetCookiePairs(rawSetCookie) {
|
|
1117
|
+
const pair = rawSetCookie.split(";", 1)[0] ?? "";
|
|
1118
|
+
const eq = pair.indexOf("=");
|
|
1119
|
+
if (eq === -1)
|
|
1120
|
+
return;
|
|
1121
|
+
const name = pair.slice(0, eq).trim();
|
|
1122
|
+
const value = pair.slice(eq + 1).trim();
|
|
1123
|
+
if (name && value)
|
|
1124
|
+
yield { name, value };
|
|
1125
|
+
}
|
|
1060
1126
|
/**
|
|
1061
1127
|
* Walks every capture's response (including GETs — formHistoryId-style values
|
|
1062
1128
|
* may originate in a state-load GET, not a POST). Indexes every string leaf
|
|
1063
1129
|
* whose length is in [MIN, MAX], recording the EARLIEST capture index that
|
|
1064
1130
|
* produced it. Later occurrences of the same value reuse the earliest origin.
|
|
1065
1131
|
*
|
|
1132
|
+
* Also indexes response-header/cookie-origin values (e.g. a `Set-Cookie`
|
|
1133
|
+
* auth token) the same way, tagged with `headerOrigin` instead of a body
|
|
1134
|
+
* `path` — this is what lets a stateful API's token-mint response feed a
|
|
1135
|
+
* later call's `Cookie` header via `compileActionSteps`.
|
|
1136
|
+
*
|
|
1066
1137
|
* The index is intentionally permissive — it doesn't try to shape-match
|
|
1067
1138
|
* "what looks like a token" because token shapes are an open set across the
|
|
1068
1139
|
* web. Authoritative filtering happens downstream in `compileActionSteps`,
|
|
@@ -1073,6 +1144,8 @@ const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"
|
|
|
1073
1144
|
* the LATER non-placeholder occurrence at the same JSON path becomes the
|
|
1074
1145
|
* canonical binding instead.
|
|
1075
1146
|
*/
|
|
1147
|
+
/** Exported for unit testing — lets tests exercise the produces[] walk (body
|
|
1148
|
+
* AND header/cookie origins) directly against synthetic Capture sequences. */
|
|
1076
1149
|
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set()) {
|
|
1077
1150
|
const index = new Map();
|
|
1078
1151
|
// First pass: identify the earliest origin among ACTION captures for each
|
|
@@ -1085,10 +1158,32 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1085
1158
|
const haveActionFilter = actionCaptureIndices.size > 0;
|
|
1086
1159
|
for (let i = 0; i < captures.length; i++) {
|
|
1087
1160
|
const c = captures[i];
|
|
1088
|
-
if (c.responseBody === undefined || c.responseBody === null)
|
|
1089
|
-
continue;
|
|
1090
1161
|
if (haveActionFilter && !actionCaptureIndices.has(i))
|
|
1091
1162
|
continue;
|
|
1163
|
+
// Headers/cookies are indexed regardless of responseBody presence — a
|
|
1164
|
+
// token-mint call like disneycruise's `authz/private` returns `{}` and
|
|
1165
|
+
// carries its whole payload in `Set-Cookie`.
|
|
1166
|
+
const rawSetCookie = Object.entries(c.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
|
|
1167
|
+
if (rawSetCookie !== undefined) {
|
|
1168
|
+
for (const { name, value } of walkSetCookiePairs(rawSetCookie)) {
|
|
1169
|
+
if (value.length < MIN_STATE_VALUE_LENGTH)
|
|
1170
|
+
continue;
|
|
1171
|
+
if (value.length > MAX_STATE_VALUE_LENGTH)
|
|
1172
|
+
continue;
|
|
1173
|
+
if (PLACEHOLDER_STATE_VALUES.has(value))
|
|
1174
|
+
continue;
|
|
1175
|
+
if (!index.has(value)) {
|
|
1176
|
+
index.set(value, {
|
|
1177
|
+
value,
|
|
1178
|
+
originIndex: i,
|
|
1179
|
+
path: [],
|
|
1180
|
+
headerOrigin: { sourceHeader: "set-cookie", cookieName: name },
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (c.responseBody === undefined || c.responseBody === null)
|
|
1186
|
+
continue;
|
|
1092
1187
|
// For GET captures, only index UUID-shaped strings. GET captures (today,
|
|
1093
1188
|
// only the form-schema fetch inserted as an action step) surface stable
|
|
1094
1189
|
// structural identifiers — UUIDs that downstream POSTs need to thread.
|
|
@@ -1125,9 +1220,18 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1125
1220
|
function isValidJsIdentifier(s) {
|
|
1126
1221
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
1127
1222
|
}
|
|
1128
|
-
/**
|
|
1223
|
+
/**
|
|
1224
|
+
* Converts a path like ["Auth","Token"] to a JS access expression ".Auth.Token".
|
|
1225
|
+
* Bracket segments (numeric array indices, non-identifier keys) get a trailing
|
|
1226
|
+
* `!` — under `noUncheckedIndexedAccess` an array/index-signature access types
|
|
1227
|
+
* as `T | undefined`, and this accessor is only ever used against a real Zod-
|
|
1228
|
+
* inferred array/object type (payload fields, captured response bodies), never
|
|
1229
|
+
* against the object-literal assertion types `pathToAssertionType` builds (those
|
|
1230
|
+
* use known string-literal keys, which `noUncheckedIndexedAccess` does not
|
|
1231
|
+
* widen). Dot segments stay bare since object property access isn't affected.
|
|
1232
|
+
*/
|
|
1129
1233
|
function pathToAccessor(path) {
|
|
1130
|
-
return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]
|
|
1234
|
+
return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]!`)).join("");
|
|
1131
1235
|
}
|
|
1132
1236
|
/**
|
|
1133
1237
|
* Builds a nested TypeScript assertion type matching a JSON path. e.g.
|
|
@@ -1161,26 +1265,75 @@ function pathToVarName(path) {
|
|
|
1161
1265
|
* var name, the state values its response produces (used by downstream steps),
|
|
1162
1266
|
* and a multipart flag (request body bytes not captured).
|
|
1163
1267
|
*/
|
|
1268
|
+
/** Exported for unit testing — see `indexStateValues`. */
|
|
1164
1269
|
function compileActionSteps(actions, stateIndex) {
|
|
1165
1270
|
const usedValues = new Set();
|
|
1271
|
+
// Maps a used state value to the request-header NAME that carries it, for
|
|
1272
|
+
// values whose consuming reference is a request header (not the URL/body).
|
|
1273
|
+
// A header-origin produce needs this as its `targetHeader` — the header the
|
|
1274
|
+
// *next* httpClient call must send the bound value back on. Only the first
|
|
1275
|
+
// consuming header name observed wins; a value used in more than one distinct
|
|
1276
|
+
// header downstream isn't a shape this models (see http-client.ts's `bind`,
|
|
1277
|
+
// which is single-target per binding).
|
|
1278
|
+
const usedValueTargetHeader = new Map();
|
|
1166
1279
|
// Pre-scan: collect all state values referenced by ANY action's URL/headers/body
|
|
1167
1280
|
// so we only "produce" the values that are actually consumed downstream.
|
|
1168
1281
|
for (const { capture } of actions) {
|
|
1169
1282
|
const haystacks = [capture.url];
|
|
1170
|
-
for (const v of Object.values(capture.requestHeaders))
|
|
1171
|
-
haystacks.push(v);
|
|
1172
1283
|
if (capture.requestPostData)
|
|
1173
1284
|
haystacks.push(capture.requestPostData);
|
|
1174
1285
|
for (const sv of stateIndex.values()) {
|
|
1175
1286
|
if (haystacks.some((h) => h.includes(sv.value)))
|
|
1176
1287
|
usedValues.add(sv.value);
|
|
1177
1288
|
}
|
|
1289
|
+
for (const [headerName, headerValue] of Object.entries(capture.requestHeaders)) {
|
|
1290
|
+
for (const sv of stateIndex.values()) {
|
|
1291
|
+
if (!headerValue.includes(sv.value))
|
|
1292
|
+
continue;
|
|
1293
|
+
usedValues.add(sv.value);
|
|
1294
|
+
if (!usedValueTargetHeader.has(sv.value)) {
|
|
1295
|
+
usedValueTargetHeader.set(sv.value, headerName);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1178
1299
|
}
|
|
1179
1300
|
let lastHost = null;
|
|
1180
1301
|
return actions.map(({ capture, index }, i) => {
|
|
1181
1302
|
const varName = `r${i}`;
|
|
1182
1303
|
const produces = [];
|
|
1183
1304
|
const seenNames = new Set();
|
|
1305
|
+
// Header/cookie-origin produces — walked first so a value that appears in
|
|
1306
|
+
// BOTH a Set-Cookie and the JSON body (unlikely, but not ruled out) prefers
|
|
1307
|
+
// the header binding, which is what the runtime actually threads.
|
|
1308
|
+
const rawSetCookie = Object.entries(capture.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
|
|
1309
|
+
if (rawSetCookie !== undefined) {
|
|
1310
|
+
for (const { name: cookieName, value } of walkSetCookiePairs(rawSetCookie)) {
|
|
1311
|
+
if (!usedValues.has(value))
|
|
1312
|
+
continue;
|
|
1313
|
+
const sv = stateIndex.get(value);
|
|
1314
|
+
if (!sv || sv.originIndex !== index || !sv.headerOrigin)
|
|
1315
|
+
continue;
|
|
1316
|
+
const targetHeader = usedValueTargetHeader.get(value);
|
|
1317
|
+
if (!targetHeader)
|
|
1318
|
+
continue;
|
|
1319
|
+
let name = `${cookieName.replace(/[^A-Za-z0-9]/g, "")}Cookie`;
|
|
1320
|
+
if (!/^[A-Za-z_$]/.test(name))
|
|
1321
|
+
name = `_${name}`;
|
|
1322
|
+
let suffix = 1;
|
|
1323
|
+
while (seenNames.has(name)) {
|
|
1324
|
+
suffix++;
|
|
1325
|
+
name = `${cookieName.replace(/[^A-Za-z0-9]/g, "")}Cookie${suffix}`;
|
|
1326
|
+
}
|
|
1327
|
+
seenNames.add(name);
|
|
1328
|
+
produces.push({
|
|
1329
|
+
kind: "header",
|
|
1330
|
+
name,
|
|
1331
|
+
sourceHeader: sv.headerOrigin.sourceHeader,
|
|
1332
|
+
cookieName: sv.headerOrigin.cookieName,
|
|
1333
|
+
targetHeader,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1184
1337
|
if (capture.responseBody !== undefined && capture.responseBody !== null) {
|
|
1185
1338
|
for (const { value, path } of walkStringLeaves(capture.responseBody)) {
|
|
1186
1339
|
if (!usedValues.has(value))
|
|
@@ -1196,7 +1349,7 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
1196
1349
|
name = `${pathToVarName(path)}${suffix}`;
|
|
1197
1350
|
}
|
|
1198
1351
|
seenNames.add(name);
|
|
1199
|
-
produces.push({ name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
|
|
1352
|
+
produces.push({ kind: "body", name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
|
|
1200
1353
|
}
|
|
1201
1354
|
}
|
|
1202
1355
|
const ct = Object.entries(capture.requestHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
@@ -1213,6 +1366,27 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
1213
1366
|
return { capture, varName, produces, isMultipart, isCrossDomain };
|
|
1214
1367
|
});
|
|
1215
1368
|
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Collects every header/cookie-origin produce across an action sequence, in
|
|
1371
|
+
* step order — this is what `emitContractTs` renders as `createHttpClient`'s
|
|
1372
|
+
* `bind` option so the generated `executeHttp` actually forwards a value like
|
|
1373
|
+
* disneycruise's `Set-Cookie: __pa=<jwt>` mint to the stateful call that 401s
|
|
1374
|
+
* without it. Deduped by `targetHeader`: `HttpResponseBinding` (http-client.ts)
|
|
1375
|
+
* is one binding per target header, so if two steps somehow produced the same
|
|
1376
|
+
* target the earliest wins.
|
|
1377
|
+
*/
|
|
1378
|
+
function collectHeaderBindings(actionSteps) {
|
|
1379
|
+
const byTarget = new Map();
|
|
1380
|
+
for (const step of actionSteps) {
|
|
1381
|
+
for (const p of step.produces) {
|
|
1382
|
+
if (p.kind !== "header")
|
|
1383
|
+
continue;
|
|
1384
|
+
if (!byTarget.has(p.targetHeader))
|
|
1385
|
+
byTarget.set(p.targetHeader, p);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return [...byTarget.values()];
|
|
1389
|
+
}
|
|
1216
1390
|
/**
|
|
1217
1391
|
* Replaces occurrences of state values in `template` with `${varName}`
|
|
1218
1392
|
* interpolations. Returns a JS template-literal string fragment (no backticks).
|
|
@@ -1227,6 +1401,12 @@ function interpolateStateValues(template, priorSteps, payloadAccessorByValue = n
|
|
|
1227
1401
|
const varNameByValue = new Map();
|
|
1228
1402
|
for (const step of priorSteps) {
|
|
1229
1403
|
for (const p of step.produces) {
|
|
1404
|
+
// Header/cookie-origin produces have no body path — their value never
|
|
1405
|
+
// appears as a literal in a URL/body template (http-client's `bind`
|
|
1406
|
+
// forwards it directly as a request header), so there's nothing to
|
|
1407
|
+
// interpolate here.
|
|
1408
|
+
if (p.kind === "header")
|
|
1409
|
+
continue;
|
|
1230
1410
|
let cursor = step.capture.responseBody;
|
|
1231
1411
|
for (const segment of p.path) {
|
|
1232
1412
|
if (cursor !== null &&
|
|
@@ -1820,6 +2000,11 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
1820
2000
|
}
|
|
1821
2001
|
}
|
|
1822
2002
|
for (const p of step.produces) {
|
|
2003
|
+
// Header/cookie-origin produces never surface as a JS accessor —
|
|
2004
|
+
// createHttpClient's `bind` option (rendered once, above the steps)
|
|
2005
|
+
// captures and forwards the value internally.
|
|
2006
|
+
if (p.kind === "header")
|
|
2007
|
+
continue;
|
|
1823
2008
|
if (declaredNames.has(p.name))
|
|
1824
2009
|
continue;
|
|
1825
2010
|
if (!referencedNames.has(p.name))
|
|
@@ -1845,14 +2030,40 @@ function summariseResponseShape(value) {
|
|
|
1845
2030
|
}
|
|
1846
2031
|
return obj;
|
|
1847
2032
|
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Renders `headerBindings` as a trailing `, bind: [...]` fragment for
|
|
2035
|
+
* `createHttpClient`'s options object literal — empty string when there are
|
|
2036
|
+
* none, so a plugin with no header/cookie-origin state keeps the exact output
|
|
2037
|
+
* this emitter already produced. Structurally matches `HttpResponseBinding`
|
|
2038
|
+
* (http-client.ts) without importing the type: the object literal typechecks
|
|
2039
|
+
* against `HttpClientOptions.bind` on its own shape.
|
|
2040
|
+
*/
|
|
2041
|
+
function bindOptionLiteral(headerBindings) {
|
|
2042
|
+
if (headerBindings.length === 0)
|
|
2043
|
+
return "";
|
|
2044
|
+
const entries = headerBindings
|
|
2045
|
+
.map((b) => {
|
|
2046
|
+
const cookieNameField = b.cookieName !== undefined ? ` cookieName: ${JSON.stringify(b.cookieName)},` : "";
|
|
2047
|
+
return `{ sourceHeader: ${JSON.stringify(b.sourceHeader)},${cookieNameField} targetHeader: ${JSON.stringify(b.targetHeader)} }`;
|
|
2048
|
+
})
|
|
2049
|
+
.join(", ");
|
|
2050
|
+
return `, bind: [${entries}]`;
|
|
2051
|
+
}
|
|
1848
2052
|
// ── code emitters ─────────────────────────────────────────────────────────────
|
|
1849
2053
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
1850
2054
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
1851
2055
|
function emitContractTs(opts) {
|
|
1852
|
-
const { siteId, pascal, baseUrl, baseHeaders, minTime, safeRps, responseBody, gql, gqlQuery, endpointPath, auxFiles, multiStepBody, inputBody, hasMultipartStep = false, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, payloadFieldNames, base64ContentHelper = "", } = opts;
|
|
2056
|
+
const { siteId, pascal, baseUrl, baseHeaders, minTime, safeRps, responseBody, gql, gqlQuery, endpointPath, auxFiles, multiStepBody, inputBody, hasMultipartStep = false, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, payloadFieldNames, base64ContentHelper = "", headerBindings = [], } = opts;
|
|
1853
2057
|
// Multi-step plugins thread responses through many different shapes that a
|
|
1854
2058
|
// single Zod schema can't cover — use z.unknown() so each per-step access
|
|
1855
2059
|
// compiles cleanly. Single-endpoint plugins keep the inferred schema.
|
|
2060
|
+
//
|
|
2061
|
+
// This is deliberate, not an unfinished schema: a submission flow's terminal
|
|
2062
|
+
// shape is the plugin's OWN contract with its caller (e.g. { verified: boolean }),
|
|
2063
|
+
// a field that appears in zero captured responses. Inferring a schema from the
|
|
2064
|
+
// captures would emit the wrong shape with false confidence. z.unknown() plus
|
|
2065
|
+
// the generated `[ ] Narrow ResponseSchema` checklist item is the intended
|
|
2066
|
+
// hand-off to the plugin author, who alone knows that contract.
|
|
1856
2067
|
const responseSchemaExpr = multiStepBody ? `z.unknown()` : inferZodSchema(responseBody);
|
|
1857
2068
|
// Multi-step flows that include a multipart upload need the binary asset
|
|
1858
2069
|
// on the payload. Add Resume/ResumeContentType/ResumeFilename as required
|
|
@@ -1965,24 +2176,24 @@ function emitContractTs(opts) {
|
|
|
1965
2176
|
? `${basePayloadSchemaExpr}.extend({\n Resume: z.instanceof(Buffer),\n ResumeContentType: z.string(),\n ResumeFilename: z.string(),\n})${formFieldsExtension}${splicedFieldsExtension}${optionSchemaExtension}${rawOptionSchemaExtension}${additionalBodyKeysExtension}${answersExtension}`
|
|
1966
2177
|
: `${basePayloadSchemaExpr}${formFieldsExtension}${splicedFieldsExtension}${optionSchemaExtension}${rawOptionSchemaExtension}${additionalBodyKeysExtension}${answersExtension}`;
|
|
1967
2178
|
// When the payload schema uses multipartBoolean(), import the shared helper
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
2179
|
+
// so the generated file resolves the reference and doesn't re-inline the
|
|
2180
|
+
// preprocess expression per boolean field.
|
|
1970
2181
|
const multipartBoolImport = hasMultipartStep
|
|
1971
|
-
? `import { multipartBoolean } from "
|
|
2182
|
+
? `import { multipartBoolean } from "${ENGINE_PKG}/lib/zod-multipart";\n`
|
|
1972
2183
|
: "";
|
|
1973
2184
|
// Content-Type must be absent from multipart fetch calls so FormData can inject the boundary.
|
|
1974
2185
|
const caseInsensitiveHeadersImport = hasMultipartStep
|
|
1975
|
-
? `import { omitHeaderCaseInsensitive } from "
|
|
2186
|
+
? `import { omitHeaderCaseInsensitive } from "${ENGINE_PKG}/lib/case-insensitive-headers";\n`
|
|
1976
2187
|
: "";
|
|
1977
2188
|
// Emit identifier-shaped keys unquoted so Biome's formatter doesn't rewrite
|
|
1978
2189
|
// the generated file on first lint:fix.
|
|
1979
2190
|
const headersLiteral = Object.entries(baseHeaders)
|
|
1980
2191
|
.map(([k, v]) => ` ${isValidJsIdentifier(k) ? k : JSON.stringify(k)}: ${JSON.stringify(v)}`)
|
|
1981
2192
|
.join(",\n");
|
|
1982
|
-
const fixtureImport = auxFiles.length > 0 ? `// import { loadFixture } from "
|
|
2193
|
+
const fixtureImport = auxFiles.length > 0 ? `// import { loadFixture } from "${ENGINE_PKG}/scraper/fixtures";\n` : "";
|
|
1983
2194
|
const clientImport = gql
|
|
1984
|
-
? `import { createGraphqlClient } from "
|
|
1985
|
-
: `import { createHttpClient } from "
|
|
2195
|
+
? `import { createGraphqlClient } from "${ENGINE_PKG}/scraper/graphql-client";`
|
|
2196
|
+
: `import { createHttpClient } from "${ENGINE_PKG}/scraper/http-client";`;
|
|
1986
2197
|
const queryConst = gql && gqlQuery
|
|
1987
2198
|
? `\n// Lifted verbatim from recon capture — trim UI-only fields before shipping.\nconst ${pascal.toUpperCase()}_QUERY = \`${gqlQuery.trim()}\`;\n`
|
|
1988
2199
|
: "";
|
|
@@ -2007,7 +2218,7 @@ function getGql(baseUrl: string): GqlFn {
|
|
|
2007
2218
|
}
|
|
2008
2219
|
`
|
|
2009
2220
|
: `
|
|
2010
|
-
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS });
|
|
2221
|
+
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
|
|
2011
2222
|
`;
|
|
2012
2223
|
const executeHttpBody = multiStepBody
|
|
2013
2224
|
? multiStepBody
|
|
@@ -2029,6 +2240,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
2029
2240
|
const queryChecklistLine = gql
|
|
2030
2241
|
? `\n * [ ] Trim UI-only fields from ${pascal.toUpperCase()}_QUERY (keep only fields you need)`
|
|
2031
2242
|
: "";
|
|
2243
|
+
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2032
2244
|
return `/**
|
|
2033
2245
|
* Generated by recon-generate.ts — review before shipping.
|
|
2034
2246
|
*
|
|
@@ -2036,14 +2248,17 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
2036
2248
|
* [ ] Narrow ${pascal}ResponseSchema to match the real response shape
|
|
2037
2249
|
* [ ] Adjust ${pascal}PayloadSchema to your actual request parameters
|
|
2038
2250
|
* [ ] Verify BASE_HEADERS — remove any that aren't load-bearing
|
|
2251
|
+
* [ ] Out-of-tree: \`pnpm add bottleneck zod\` — this file imports both
|
|
2252
|
+
* directly, and a strict node_modules layout (pnpm) won't resolve
|
|
2253
|
+
* them as transitive deps of @enricai/barnacle alone
|
|
2039
2254
|
*/
|
|
2040
2255
|
|
|
2041
2256
|
import Bottleneck from "bottleneck";
|
|
2042
2257
|
import { z } from "zod/v4";
|
|
2043
2258
|
|
|
2044
2259
|
${fixtureImport}${caseInsensitiveHeadersImport}${multipartBoolImport}${clientImport}
|
|
2045
|
-
import type { BrowserSession } from "
|
|
2046
|
-
import type { SitePlugin, SitePluginContext, SitePluginResult } from "
|
|
2260
|
+
import type { BrowserSession } from "${ENGINE_PKG}/scraper/session";
|
|
2261
|
+
import type { SitePlugin, SitePluginContext, SitePluginResult } from "${ENGINE_PKG}/site-plugin";
|
|
2047
2262
|
import { run${pascal}BrowserFlow } from "@/sites/${siteId}/flows/browser-flow";
|
|
2048
2263
|
|
|
2049
2264
|
const BASE_HEADERS: Record<string, string> = {
|
|
@@ -2067,13 +2282,14 @@ ${queryConst}${gqlCacheBlock}${fixtureComments}
|
|
|
2067
2282
|
* Plugin for ${siteId}. Tries the direct-HTTP hot path first; falls back to
|
|
2068
2283
|
* Stagehand automatically on schema drift or bot challenge.
|
|
2069
2284
|
*/
|
|
2070
|
-
export const ${
|
|
2285
|
+
export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
2071
2286
|
meta: {
|
|
2072
2287
|
siteId: ${JSON.stringify(siteId)},
|
|
2073
2288
|
displayName: ${JSON.stringify(pascal.replace(/([A-Z])/g, " $1").trim())},
|
|
2074
2289
|
bodySchema: ${pascal}PayloadSchema,
|
|
2075
2290
|
responseSchema: ${pascal}ResponseSchema,
|
|
2076
|
-
defaultBaseUrl: ${JSON.stringify(baseUrl)}
|
|
2291
|
+
defaultBaseUrl: ${JSON.stringify(baseUrl)},
|
|
2292
|
+
apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${hasMultipartStep ? "\n multipart: true," : ""}
|
|
2077
2293
|
},
|
|
2078
2294
|
|
|
2079
2295
|
/** Hot path: direct HTTP — no browser, no LLM tokens. */
|
|
@@ -2094,6 +2310,11 @@ ${executeHttpBody}
|
|
|
2094
2310
|
return { data: raw as ${pascal}Response };
|
|
2095
2311
|
},
|
|
2096
2312
|
};
|
|
2313
|
+
|
|
2314
|
+
// Out-of-tree loader resolves \`m.plugin ?? m.default ?? m\` — this named
|
|
2315
|
+
// alias is what BARNACLE_PLUGINS finds; without it the loader would fall
|
|
2316
|
+
// through to \`m.default\` (the response schema above) and 404 at runtime.
|
|
2317
|
+
export { ${camel}Plugin as plugin };
|
|
2097
2318
|
`;
|
|
2098
2319
|
}
|
|
2099
2320
|
/**
|
|
@@ -2163,16 +2384,43 @@ function buildManifestInstruction(instruction, field) {
|
|
|
2163
2384
|
`{{ .request.${field} }}` +
|
|
2164
2385
|
instruction.slice(m.index + m[0].length));
|
|
2165
2386
|
}
|
|
2387
|
+
/**
|
|
2388
|
+
* The JSON Schema `type` keyword for a sample value. Just the keyword, not a
|
|
2389
|
+
* full schema: the manifest is a scaffold a human narrows, so it needs the real
|
|
2390
|
+
* type a caller must send (`page` is a number, `filters` an array) without
|
|
2391
|
+
* duplicating {@link inferZodSchema}'s recursive shape inference. `null` and
|
|
2392
|
+
* `undefined` fall back to `string`, the safe default for a field a caller fills.
|
|
2393
|
+
*/
|
|
2394
|
+
function jsonSchemaTypeOf(value) {
|
|
2395
|
+
if (Array.isArray(value))
|
|
2396
|
+
return "array";
|
|
2397
|
+
if (value === null || value === undefined)
|
|
2398
|
+
return "string";
|
|
2399
|
+
const t = typeof value;
|
|
2400
|
+
if (t === "number")
|
|
2401
|
+
return "number";
|
|
2402
|
+
if (t === "boolean")
|
|
2403
|
+
return "boolean";
|
|
2404
|
+
if (t === "object")
|
|
2405
|
+
return "object";
|
|
2406
|
+
return "string";
|
|
2407
|
+
}
|
|
2166
2408
|
/**
|
|
2167
2409
|
* Emits a config-only plugin manifest (`<siteId>.plugin.json`) from the recon
|
|
2168
2410
|
* flow, as an alternative to the `.ts` trio for browser-only sites. Reuses the
|
|
2169
2411
|
* SAME `resolveStepPayloadField` splice logic as the browser-flow emitter, so
|
|
2170
2412
|
* every `{{ .request.<field> }}` reference also lands in the manifest's request
|
|
2171
|
-
* schema — the two cannot drift.
|
|
2172
|
-
*
|
|
2413
|
+
* schema — the two cannot drift.
|
|
2414
|
+
*
|
|
2415
|
+
* `recovered` carries the request contract the `.ts` path infers from real
|
|
2416
|
+
* captures — the first POST body's fields plus form-schema discoveries — so
|
|
2417
|
+
* `--emit config` no longer throws that away and emit a request schema built
|
|
2418
|
+
* only from the handful of flow-step splice hints. The direct-HTTP hot path is
|
|
2419
|
+
* still omitted; a site that needs it keeps the `.ts` path or wires
|
|
2420
|
+
* `spec.httpModule` by hand.
|
|
2173
2421
|
*/
|
|
2174
2422
|
function emitConfigManifest(opts) {
|
|
2175
|
-
const { siteId, displayName, baseUrl, flowSteps, vocabulary } = opts;
|
|
2423
|
+
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields } = opts;
|
|
2176
2424
|
const payloadFieldNames = new Set();
|
|
2177
2425
|
const steps = flowSteps.map((step) => {
|
|
2178
2426
|
const isObj = typeof step !== "string";
|
|
@@ -2188,14 +2436,30 @@ function emitConfigManifest(opts) {
|
|
|
2188
2436
|
return rewritten;
|
|
2189
2437
|
return { step: rewritten, optional, upload, submitStep };
|
|
2190
2438
|
});
|
|
2191
|
-
|
|
2439
|
+
// The request surface, widest wins: a flow splice, a recovered form field, or
|
|
2440
|
+
// a key from the first POST body all name something a caller controls. Splices
|
|
2441
|
+
// and recovered fields are strings (the browser flow fills them as text); a
|
|
2442
|
+
// body key keeps its captured type so a caller sends `page: 1`, not `"1"`.
|
|
2443
|
+
const requestProperties = {};
|
|
2444
|
+
for (const name of payloadFieldNames)
|
|
2445
|
+
requestProperties[name] = { type: "string" };
|
|
2446
|
+
for (const name of recoveredFields ?? [])
|
|
2447
|
+
requestProperties[name] = { type: "string" };
|
|
2448
|
+
if (inputBody !== null && typeof inputBody === "object" && !Array.isArray(inputBody)) {
|
|
2449
|
+
for (const [name, value] of Object.entries(inputBody)) {
|
|
2450
|
+
requestProperties[name] = { type: jsonSchemaTypeOf(value) };
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
const sortedRequestProperties = Object.fromEntries(Object.keys(requestProperties)
|
|
2454
|
+
.sort()
|
|
2455
|
+
.map((name) => [name, requestProperties[name]]));
|
|
2192
2456
|
const manifest = {
|
|
2193
2457
|
apiVersion: plugin_manifest_envelope_1.CONFIG_PLUGIN_API_VERSION,
|
|
2194
2458
|
kind: plugin_manifest_envelope_1.CONFIG_PLUGIN_KIND,
|
|
2195
2459
|
metadata: { siteId, displayName },
|
|
2196
2460
|
spec: {
|
|
2197
2461
|
defaultBaseUrl: baseUrl,
|
|
2198
|
-
request: { type: "object", properties:
|
|
2462
|
+
request: { type: "object", properties: sortedRequestProperties },
|
|
2199
2463
|
response: {
|
|
2200
2464
|
type: "object",
|
|
2201
2465
|
description: "TODO: declare the fields this site returns (recon leaves this empty).",
|
|
@@ -2270,10 +2534,10 @@ function emitBrowserFlowTs(opts) {
|
|
|
2270
2534
|
import type { Stagehand } from "@browserbasehq/stagehand";
|
|
2271
2535
|
import { z } from "zod/v4";
|
|
2272
2536
|
|
|
2273
|
-
import { buildAnthropicClient } from "
|
|
2274
|
-
import { getLogger } from "
|
|
2275
|
-
import { type HealingFlowStep, runHealingFlow, waitForSpaReady } from "
|
|
2276
|
-
import { guardedExtract } from "
|
|
2537
|
+
import { buildAnthropicClient } from "${ENGINE_PKG}/lib/llm/anthropic-client";
|
|
2538
|
+
import { getLogger } from "${ENGINE_PKG}/lib/logging";
|
|
2539
|
+
import { type HealingFlowStep, runHealingFlow, waitForSpaReady } from "${ENGINE_PKG}/scraper/flow-runner";
|
|
2540
|
+
import { guardedExtract } from "${ENGINE_PKG}/scraper/stagehand-guard";
|
|
2277
2541
|
import type { ${pascal}Payload, ${pascal}Response } from "@/sites/${siteId}/contract";
|
|
2278
2542
|
|
|
2279
2543
|
const logger = getLogger({ name: "${siteId}-browser-flow" });
|
|
@@ -2332,19 +2596,23 @@ ${flowStepsBlock}
|
|
|
2332
2596
|
`;
|
|
2333
2597
|
return { code, payloadFieldNames };
|
|
2334
2598
|
}
|
|
2599
|
+
/** Generates the site's index.ts barrel — exported so the out-of-tree e2e
|
|
2600
|
+
* test can drive the emitter directly without spawning the CLI. */
|
|
2335
2601
|
function emitIndexTs(opts) {
|
|
2336
2602
|
const { siteId } = opts;
|
|
2337
2603
|
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2338
2604
|
return `/**
|
|
2339
2605
|
* Generated by recon-generate.ts.
|
|
2340
|
-
*
|
|
2341
|
-
*
|
|
2606
|
+
* Build this package, then point BARNACLE_PLUGINS at the compiled module —
|
|
2607
|
+
* no core edits required:
|
|
2608
|
+
*
|
|
2609
|
+
* BARNACLE_PLUGINS=./dist/sites/${siteId}/index.js pnpm start
|
|
2342
2610
|
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2611
|
+
* The loader resolves \`m.plugin ?? m.default ?? m\` — the \`plugin\` alias
|
|
2612
|
+
* below is what it finds.
|
|
2345
2613
|
*/
|
|
2346
2614
|
|
|
2347
|
-
export { ${camel}Plugin } from "@/sites/${siteId}/contract";
|
|
2615
|
+
export { ${camel}Plugin, ${camel}Plugin as plugin } from "@/sites/${siteId}/contract";
|
|
2348
2616
|
`;
|
|
2349
2617
|
}
|
|
2350
2618
|
// ── main ──────────────────────────────────────────────────────────────────────
|
|
@@ -2613,6 +2881,7 @@ async function main() {
|
|
|
2613
2881
|
s.capture.url.includes("recruitingCEJobApplicationDrafts"));
|
|
2614
2882
|
if (draftPostStep && !draftPostStep.produces.some((p) => p.name === "draftId")) {
|
|
2615
2883
|
draftPostStep.produces.push({
|
|
2884
|
+
kind: "body",
|
|
2616
2885
|
name: "draftId",
|
|
2617
2886
|
pathExpr: `${draftPostStep.varName}.APPDraftId`,
|
|
2618
2887
|
path: ["APPDraftId"],
|
|
@@ -2621,6 +2890,7 @@ async function main() {
|
|
|
2621
2890
|
const attachPostStep = actionSteps.find((s) => s.capture.method === "POST" && s.capture.url.includes("/attachments"));
|
|
2622
2891
|
if (attachPostStep && !attachPostStep.produces.some((p) => p.name === "attachmentId")) {
|
|
2623
2892
|
attachPostStep.produces.push({
|
|
2893
|
+
kind: "body",
|
|
2624
2894
|
name: "attachmentId",
|
|
2625
2895
|
pathExpr: `${attachPostStep.varName}.Id`,
|
|
2626
2896
|
path: ["Id"],
|
|
@@ -2669,6 +2939,7 @@ async function main() {
|
|
|
2669
2939
|
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride)
|
|
2670
2940
|
: multiStepBody;
|
|
2671
2941
|
const hasMultipartStep = actionSteps.some((s) => s.isMultipart);
|
|
2942
|
+
const headerBindings = collectHeaderBindings(actionSteps);
|
|
2672
2943
|
// For submission flows the final action's response body is the most useful
|
|
2673
2944
|
// shape inference target (it's the terminal success signal). Fall back to
|
|
2674
2945
|
// the replay body for single-endpoint sites.
|
|
@@ -2684,6 +2955,8 @@ async function main() {
|
|
|
2684
2955
|
baseUrl,
|
|
2685
2956
|
flowSteps,
|
|
2686
2957
|
vocabulary,
|
|
2958
|
+
inputBody,
|
|
2959
|
+
recoveredFields: [...discoveredFormFields, ...discoveredOptionFields],
|
|
2687
2960
|
}));
|
|
2688
2961
|
logger.info(`wrote ${manifestPath}`);
|
|
2689
2962
|
logger.info(`done — review ${manifestPath}, fill in response/extract schemas, then load via BARNACLE_PLUGINS or BARNACLE_PLUGINS_CONFIG_DIR (no compile step)`);
|
|
@@ -2726,6 +2999,7 @@ async function main() {
|
|
|
2726
2999
|
discoveredRawOptionFields,
|
|
2727
3000
|
discoveredAdditionalBodyKeys,
|
|
2728
3001
|
payloadFieldNames: browserFlow.payloadFieldNames,
|
|
3002
|
+
headerBindings,
|
|
2729
3003
|
}));
|
|
2730
3004
|
logger.info(`wrote ${outDir}/contract.ts`);
|
|
2731
3005
|
(0, node_fs_1.writeFileSync)(`${outDir}/flows/browser-flow.ts`, browserFlow.code);
|