@enricai/barnacle 1.4.1 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +86 -5
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +194 -16
- 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 +44 -24
|
@@ -20,15 +20,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
20
20
|
exports.resolveStepPayloadField = resolveStepPayloadField;
|
|
21
21
|
exports.inferZodSchemaFromSamples = inferZodSchemaFromSamples;
|
|
22
22
|
exports.selectPayloadAction = selectPayloadAction;
|
|
23
|
+
exports.indexStateValues = indexStateValues;
|
|
24
|
+
exports.compileActionSteps = compileActionSteps;
|
|
23
25
|
exports.loadQuestionPromptKeywords = loadQuestionPromptKeywords;
|
|
24
26
|
exports.emitMultiStepExecuteHttp = emitMultiStepExecuteHttp;
|
|
25
27
|
exports.emitContractTs = emitContractTs;
|
|
26
28
|
exports.emitConfigManifest = emitConfigManifest;
|
|
27
29
|
exports.emitBrowserFlowTs = emitBrowserFlowTs;
|
|
30
|
+
exports.emitIndexTs = emitIndexTs;
|
|
28
31
|
const node_fs_1 = require("node:fs");
|
|
29
32
|
const node_path_1 = require("node:path");
|
|
30
33
|
const errors_1 = require("../lib/errors");
|
|
31
34
|
const logging_1 = require("../lib/logging");
|
|
35
|
+
const plugin_api_version_1 = require("../plugins/plugin-api-version");
|
|
32
36
|
const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
|
|
33
37
|
const load_vocabulary_1 = require("../recon/load-vocabulary");
|
|
34
38
|
const vocabulary_1 = require("../recon/vocabulary");
|
|
@@ -1057,12 +1061,35 @@ const MAX_STATE_VALUE_LENGTH = 256;
|
|
|
1057
1061
|
* does not exist". Closed set, literal-string match — never expand to
|
|
1058
1062
|
* pattern-based detection (would trip the no-regex-on-open-sets rule). */
|
|
1059
1063
|
const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"]);
|
|
1064
|
+
/**
|
|
1065
|
+
* Splits a raw `Set-Cookie` response-header string into `name`/`value` pairs.
|
|
1066
|
+
* Captures store `responseHeaders` as a flat `Record<string, string>`
|
|
1067
|
+
* (see recon-shared.ts's `Capture`), so multiple `Set-Cookie` headers from the
|
|
1068
|
+
* same response — if the recon browser's CDP session folds them together —
|
|
1069
|
+
* would already have lost their individual boundaries before reaching here;
|
|
1070
|
+
* this only recovers name/value pairs from whatever single string survives.
|
|
1071
|
+
*/
|
|
1072
|
+
function* walkSetCookiePairs(rawSetCookie) {
|
|
1073
|
+
const pair = rawSetCookie.split(";", 1)[0] ?? "";
|
|
1074
|
+
const eq = pair.indexOf("=");
|
|
1075
|
+
if (eq === -1)
|
|
1076
|
+
return;
|
|
1077
|
+
const name = pair.slice(0, eq).trim();
|
|
1078
|
+
const value = pair.slice(eq + 1).trim();
|
|
1079
|
+
if (name && value)
|
|
1080
|
+
yield { name, value };
|
|
1081
|
+
}
|
|
1060
1082
|
/**
|
|
1061
1083
|
* Walks every capture's response (including GETs — formHistoryId-style values
|
|
1062
1084
|
* may originate in a state-load GET, not a POST). Indexes every string leaf
|
|
1063
1085
|
* whose length is in [MIN, MAX], recording the EARLIEST capture index that
|
|
1064
1086
|
* produced it. Later occurrences of the same value reuse the earliest origin.
|
|
1065
1087
|
*
|
|
1088
|
+
* Also indexes response-header/cookie-origin values (e.g. a `Set-Cookie`
|
|
1089
|
+
* auth token) the same way, tagged with `headerOrigin` instead of a body
|
|
1090
|
+
* `path` — this is what lets a stateful API's token-mint response feed a
|
|
1091
|
+
* later call's `Cookie` header via `compileActionSteps`.
|
|
1092
|
+
*
|
|
1066
1093
|
* The index is intentionally permissive — it doesn't try to shape-match
|
|
1067
1094
|
* "what looks like a token" because token shapes are an open set across the
|
|
1068
1095
|
* web. Authoritative filtering happens downstream in `compileActionSteps`,
|
|
@@ -1073,6 +1100,8 @@ const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"
|
|
|
1073
1100
|
* the LATER non-placeholder occurrence at the same JSON path becomes the
|
|
1074
1101
|
* canonical binding instead.
|
|
1075
1102
|
*/
|
|
1103
|
+
/** Exported for unit testing — lets tests exercise the produces[] walk (body
|
|
1104
|
+
* AND header/cookie origins) directly against synthetic Capture sequences. */
|
|
1076
1105
|
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set()) {
|
|
1077
1106
|
const index = new Map();
|
|
1078
1107
|
// First pass: identify the earliest origin among ACTION captures for each
|
|
@@ -1085,10 +1114,32 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1085
1114
|
const haveActionFilter = actionCaptureIndices.size > 0;
|
|
1086
1115
|
for (let i = 0; i < captures.length; i++) {
|
|
1087
1116
|
const c = captures[i];
|
|
1088
|
-
if (c.responseBody === undefined || c.responseBody === null)
|
|
1089
|
-
continue;
|
|
1090
1117
|
if (haveActionFilter && !actionCaptureIndices.has(i))
|
|
1091
1118
|
continue;
|
|
1119
|
+
// Headers/cookies are indexed regardless of responseBody presence — a
|
|
1120
|
+
// token-mint call like disneycruise's `authz/private` returns `{}` and
|
|
1121
|
+
// carries its whole payload in `Set-Cookie`.
|
|
1122
|
+
const rawSetCookie = Object.entries(c.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
|
|
1123
|
+
if (rawSetCookie !== undefined) {
|
|
1124
|
+
for (const { name, value } of walkSetCookiePairs(rawSetCookie)) {
|
|
1125
|
+
if (value.length < MIN_STATE_VALUE_LENGTH)
|
|
1126
|
+
continue;
|
|
1127
|
+
if (value.length > MAX_STATE_VALUE_LENGTH)
|
|
1128
|
+
continue;
|
|
1129
|
+
if (PLACEHOLDER_STATE_VALUES.has(value))
|
|
1130
|
+
continue;
|
|
1131
|
+
if (!index.has(value)) {
|
|
1132
|
+
index.set(value, {
|
|
1133
|
+
value,
|
|
1134
|
+
originIndex: i,
|
|
1135
|
+
path: [],
|
|
1136
|
+
headerOrigin: { sourceHeader: "set-cookie", cookieName: name },
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (c.responseBody === undefined || c.responseBody === null)
|
|
1142
|
+
continue;
|
|
1092
1143
|
// For GET captures, only index UUID-shaped strings. GET captures (today,
|
|
1093
1144
|
// only the form-schema fetch inserted as an action step) surface stable
|
|
1094
1145
|
// structural identifiers — UUIDs that downstream POSTs need to thread.
|
|
@@ -1125,9 +1176,18 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1125
1176
|
function isValidJsIdentifier(s) {
|
|
1126
1177
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
1127
1178
|
}
|
|
1128
|
-
/**
|
|
1179
|
+
/**
|
|
1180
|
+
* Converts a path like ["Auth","Token"] to a JS access expression ".Auth.Token".
|
|
1181
|
+
* Bracket segments (numeric array indices, non-identifier keys) get a trailing
|
|
1182
|
+
* `!` — under `noUncheckedIndexedAccess` an array/index-signature access types
|
|
1183
|
+
* as `T | undefined`, and this accessor is only ever used against a real Zod-
|
|
1184
|
+
* inferred array/object type (payload fields, captured response bodies), never
|
|
1185
|
+
* against the object-literal assertion types `pathToAssertionType` builds (those
|
|
1186
|
+
* use known string-literal keys, which `noUncheckedIndexedAccess` does not
|
|
1187
|
+
* widen). Dot segments stay bare since object property access isn't affected.
|
|
1188
|
+
*/
|
|
1129
1189
|
function pathToAccessor(path) {
|
|
1130
|
-
return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]
|
|
1190
|
+
return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]!`)).join("");
|
|
1131
1191
|
}
|
|
1132
1192
|
/**
|
|
1133
1193
|
* Builds a nested TypeScript assertion type matching a JSON path. e.g.
|
|
@@ -1161,26 +1221,75 @@ function pathToVarName(path) {
|
|
|
1161
1221
|
* var name, the state values its response produces (used by downstream steps),
|
|
1162
1222
|
* and a multipart flag (request body bytes not captured).
|
|
1163
1223
|
*/
|
|
1224
|
+
/** Exported for unit testing — see `indexStateValues`. */
|
|
1164
1225
|
function compileActionSteps(actions, stateIndex) {
|
|
1165
1226
|
const usedValues = new Set();
|
|
1227
|
+
// Maps a used state value to the request-header NAME that carries it, for
|
|
1228
|
+
// values whose consuming reference is a request header (not the URL/body).
|
|
1229
|
+
// A header-origin produce needs this as its `targetHeader` — the header the
|
|
1230
|
+
// *next* httpClient call must send the bound value back on. Only the first
|
|
1231
|
+
// consuming header name observed wins; a value used in more than one distinct
|
|
1232
|
+
// header downstream isn't a shape this models (see http-client.ts's `bind`,
|
|
1233
|
+
// which is single-target per binding).
|
|
1234
|
+
const usedValueTargetHeader = new Map();
|
|
1166
1235
|
// Pre-scan: collect all state values referenced by ANY action's URL/headers/body
|
|
1167
1236
|
// so we only "produce" the values that are actually consumed downstream.
|
|
1168
1237
|
for (const { capture } of actions) {
|
|
1169
1238
|
const haystacks = [capture.url];
|
|
1170
|
-
for (const v of Object.values(capture.requestHeaders))
|
|
1171
|
-
haystacks.push(v);
|
|
1172
1239
|
if (capture.requestPostData)
|
|
1173
1240
|
haystacks.push(capture.requestPostData);
|
|
1174
1241
|
for (const sv of stateIndex.values()) {
|
|
1175
1242
|
if (haystacks.some((h) => h.includes(sv.value)))
|
|
1176
1243
|
usedValues.add(sv.value);
|
|
1177
1244
|
}
|
|
1245
|
+
for (const [headerName, headerValue] of Object.entries(capture.requestHeaders)) {
|
|
1246
|
+
for (const sv of stateIndex.values()) {
|
|
1247
|
+
if (!headerValue.includes(sv.value))
|
|
1248
|
+
continue;
|
|
1249
|
+
usedValues.add(sv.value);
|
|
1250
|
+
if (!usedValueTargetHeader.has(sv.value)) {
|
|
1251
|
+
usedValueTargetHeader.set(sv.value, headerName);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1178
1255
|
}
|
|
1179
1256
|
let lastHost = null;
|
|
1180
1257
|
return actions.map(({ capture, index }, i) => {
|
|
1181
1258
|
const varName = `r${i}`;
|
|
1182
1259
|
const produces = [];
|
|
1183
1260
|
const seenNames = new Set();
|
|
1261
|
+
// Header/cookie-origin produces — walked first so a value that appears in
|
|
1262
|
+
// BOTH a Set-Cookie and the JSON body (unlikely, but not ruled out) prefers
|
|
1263
|
+
// the header binding, which is what the runtime actually threads.
|
|
1264
|
+
const rawSetCookie = Object.entries(capture.responseHeaders).find(([k]) => k.toLowerCase() === "set-cookie")?.[1];
|
|
1265
|
+
if (rawSetCookie !== undefined) {
|
|
1266
|
+
for (const { name: cookieName, value } of walkSetCookiePairs(rawSetCookie)) {
|
|
1267
|
+
if (!usedValues.has(value))
|
|
1268
|
+
continue;
|
|
1269
|
+
const sv = stateIndex.get(value);
|
|
1270
|
+
if (!sv || sv.originIndex !== index || !sv.headerOrigin)
|
|
1271
|
+
continue;
|
|
1272
|
+
const targetHeader = usedValueTargetHeader.get(value);
|
|
1273
|
+
if (!targetHeader)
|
|
1274
|
+
continue;
|
|
1275
|
+
let name = `${cookieName.replace(/[^A-Za-z0-9]/g, "")}Cookie`;
|
|
1276
|
+
if (!/^[A-Za-z_$]/.test(name))
|
|
1277
|
+
name = `_${name}`;
|
|
1278
|
+
let suffix = 1;
|
|
1279
|
+
while (seenNames.has(name)) {
|
|
1280
|
+
suffix++;
|
|
1281
|
+
name = `${cookieName.replace(/[^A-Za-z0-9]/g, "")}Cookie${suffix}`;
|
|
1282
|
+
}
|
|
1283
|
+
seenNames.add(name);
|
|
1284
|
+
produces.push({
|
|
1285
|
+
kind: "header",
|
|
1286
|
+
name,
|
|
1287
|
+
sourceHeader: sv.headerOrigin.sourceHeader,
|
|
1288
|
+
cookieName: sv.headerOrigin.cookieName,
|
|
1289
|
+
targetHeader,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1184
1293
|
if (capture.responseBody !== undefined && capture.responseBody !== null) {
|
|
1185
1294
|
for (const { value, path } of walkStringLeaves(capture.responseBody)) {
|
|
1186
1295
|
if (!usedValues.has(value))
|
|
@@ -1196,7 +1305,7 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
1196
1305
|
name = `${pathToVarName(path)}${suffix}`;
|
|
1197
1306
|
}
|
|
1198
1307
|
seenNames.add(name);
|
|
1199
|
-
produces.push({ name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
|
|
1308
|
+
produces.push({ kind: "body", name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
|
|
1200
1309
|
}
|
|
1201
1310
|
}
|
|
1202
1311
|
const ct = Object.entries(capture.requestHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
@@ -1213,6 +1322,27 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
1213
1322
|
return { capture, varName, produces, isMultipart, isCrossDomain };
|
|
1214
1323
|
});
|
|
1215
1324
|
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Collects every header/cookie-origin produce across an action sequence, in
|
|
1327
|
+
* step order — this is what `emitContractTs` renders as `createHttpClient`'s
|
|
1328
|
+
* `bind` option so the generated `executeHttp` actually forwards a value like
|
|
1329
|
+
* disneycruise's `Set-Cookie: __pa=<jwt>` mint to the stateful call that 401s
|
|
1330
|
+
* without it. Deduped by `targetHeader`: `HttpResponseBinding` (http-client.ts)
|
|
1331
|
+
* is one binding per target header, so if two steps somehow produced the same
|
|
1332
|
+
* target the earliest wins.
|
|
1333
|
+
*/
|
|
1334
|
+
function collectHeaderBindings(actionSteps) {
|
|
1335
|
+
const byTarget = new Map();
|
|
1336
|
+
for (const step of actionSteps) {
|
|
1337
|
+
for (const p of step.produces) {
|
|
1338
|
+
if (p.kind !== "header")
|
|
1339
|
+
continue;
|
|
1340
|
+
if (!byTarget.has(p.targetHeader))
|
|
1341
|
+
byTarget.set(p.targetHeader, p);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return [...byTarget.values()];
|
|
1345
|
+
}
|
|
1216
1346
|
/**
|
|
1217
1347
|
* Replaces occurrences of state values in `template` with `${varName}`
|
|
1218
1348
|
* interpolations. Returns a JS template-literal string fragment (no backticks).
|
|
@@ -1227,6 +1357,12 @@ function interpolateStateValues(template, priorSteps, payloadAccessorByValue = n
|
|
|
1227
1357
|
const varNameByValue = new Map();
|
|
1228
1358
|
for (const step of priorSteps) {
|
|
1229
1359
|
for (const p of step.produces) {
|
|
1360
|
+
// Header/cookie-origin produces have no body path — their value never
|
|
1361
|
+
// appears as a literal in a URL/body template (http-client's `bind`
|
|
1362
|
+
// forwards it directly as a request header), so there's nothing to
|
|
1363
|
+
// interpolate here.
|
|
1364
|
+
if (p.kind === "header")
|
|
1365
|
+
continue;
|
|
1230
1366
|
let cursor = step.capture.responseBody;
|
|
1231
1367
|
for (const segment of p.path) {
|
|
1232
1368
|
if (cursor !== null &&
|
|
@@ -1820,6 +1956,11 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
1820
1956
|
}
|
|
1821
1957
|
}
|
|
1822
1958
|
for (const p of step.produces) {
|
|
1959
|
+
// Header/cookie-origin produces never surface as a JS accessor —
|
|
1960
|
+
// createHttpClient's `bind` option (rendered once, above the steps)
|
|
1961
|
+
// captures and forwards the value internally.
|
|
1962
|
+
if (p.kind === "header")
|
|
1963
|
+
continue;
|
|
1823
1964
|
if (declaredNames.has(p.name))
|
|
1824
1965
|
continue;
|
|
1825
1966
|
if (!referencedNames.has(p.name))
|
|
@@ -1845,11 +1986,30 @@ function summariseResponseShape(value) {
|
|
|
1845
1986
|
}
|
|
1846
1987
|
return obj;
|
|
1847
1988
|
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Renders `headerBindings` as a trailing `, bind: [...]` fragment for
|
|
1991
|
+
* `createHttpClient`'s options object literal — empty string when there are
|
|
1992
|
+
* none, so a plugin with no header/cookie-origin state keeps the exact output
|
|
1993
|
+
* this emitter already produced. Structurally matches `HttpResponseBinding`
|
|
1994
|
+
* (http-client.ts) without importing the type: the object literal typechecks
|
|
1995
|
+
* against `HttpClientOptions.bind` on its own shape.
|
|
1996
|
+
*/
|
|
1997
|
+
function bindOptionLiteral(headerBindings) {
|
|
1998
|
+
if (headerBindings.length === 0)
|
|
1999
|
+
return "";
|
|
2000
|
+
const entries = headerBindings
|
|
2001
|
+
.map((b) => {
|
|
2002
|
+
const cookieNameField = b.cookieName !== undefined ? ` cookieName: ${JSON.stringify(b.cookieName)},` : "";
|
|
2003
|
+
return `{ sourceHeader: ${JSON.stringify(b.sourceHeader)},${cookieNameField} targetHeader: ${JSON.stringify(b.targetHeader)} }`;
|
|
2004
|
+
})
|
|
2005
|
+
.join(", ");
|
|
2006
|
+
return `, bind: [${entries}]`;
|
|
2007
|
+
}
|
|
1848
2008
|
// ── code emitters ─────────────────────────────────────────────────────────────
|
|
1849
2009
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
1850
2010
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
1851
2011
|
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;
|
|
2012
|
+
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
2013
|
// Multi-step plugins thread responses through many different shapes that a
|
|
1854
2014
|
// single Zod schema can't cover — use z.unknown() so each per-step access
|
|
1855
2015
|
// compiles cleanly. Single-endpoint plugins keep the inferred schema.
|
|
@@ -2007,7 +2167,7 @@ function getGql(baseUrl: string): GqlFn {
|
|
|
2007
2167
|
}
|
|
2008
2168
|
`
|
|
2009
2169
|
: `
|
|
2010
|
-
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS });
|
|
2170
|
+
const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottleneck: limiter, baseHeaders: BASE_HEADERS${bindOptionLiteral(headerBindings)} });
|
|
2011
2171
|
`;
|
|
2012
2172
|
const executeHttpBody = multiStepBody
|
|
2013
2173
|
? multiStepBody
|
|
@@ -2029,6 +2189,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
2029
2189
|
const queryChecklistLine = gql
|
|
2030
2190
|
? `\n * [ ] Trim UI-only fields from ${pascal.toUpperCase()}_QUERY (keep only fields you need)`
|
|
2031
2191
|
: "";
|
|
2192
|
+
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2032
2193
|
return `/**
|
|
2033
2194
|
* Generated by recon-generate.ts — review before shipping.
|
|
2034
2195
|
*
|
|
@@ -2036,6 +2197,9 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
2036
2197
|
* [ ] Narrow ${pascal}ResponseSchema to match the real response shape
|
|
2037
2198
|
* [ ] Adjust ${pascal}PayloadSchema to your actual request parameters
|
|
2038
2199
|
* [ ] Verify BASE_HEADERS — remove any that aren't load-bearing
|
|
2200
|
+
* [ ] Out-of-tree: \`pnpm add bottleneck zod\` — this file imports both
|
|
2201
|
+
* directly, and a strict node_modules layout (pnpm) won't resolve
|
|
2202
|
+
* them as transitive deps of @enricai/barnacle alone
|
|
2039
2203
|
*/
|
|
2040
2204
|
|
|
2041
2205
|
import Bottleneck from "bottleneck";
|
|
@@ -2067,13 +2231,14 @@ ${queryConst}${gqlCacheBlock}${fixtureComments}
|
|
|
2067
2231
|
* Plugin for ${siteId}. Tries the direct-HTTP hot path first; falls back to
|
|
2068
2232
|
* Stagehand automatically on schema drift or bot challenge.
|
|
2069
2233
|
*/
|
|
2070
|
-
export const ${
|
|
2234
|
+
export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
2071
2235
|
meta: {
|
|
2072
2236
|
siteId: ${JSON.stringify(siteId)},
|
|
2073
2237
|
displayName: ${JSON.stringify(pascal.replace(/([A-Z])/g, " $1").trim())},
|
|
2074
2238
|
bodySchema: ${pascal}PayloadSchema,
|
|
2075
2239
|
responseSchema: ${pascal}ResponseSchema,
|
|
2076
|
-
defaultBaseUrl: ${JSON.stringify(baseUrl)}
|
|
2240
|
+
defaultBaseUrl: ${JSON.stringify(baseUrl)},
|
|
2241
|
+
apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${hasMultipartStep ? "\n multipart: true," : ""}
|
|
2077
2242
|
},
|
|
2078
2243
|
|
|
2079
2244
|
/** Hot path: direct HTTP — no browser, no LLM tokens. */
|
|
@@ -2094,6 +2259,11 @@ ${executeHttpBody}
|
|
|
2094
2259
|
return { data: raw as ${pascal}Response };
|
|
2095
2260
|
},
|
|
2096
2261
|
};
|
|
2262
|
+
|
|
2263
|
+
// Out-of-tree loader resolves \`m.plugin ?? m.default ?? m\` — this named
|
|
2264
|
+
// alias is what BARNACLE_PLUGINS finds; without it the loader would fall
|
|
2265
|
+
// through to \`m.default\` (the response schema above) and 404 at runtime.
|
|
2266
|
+
export { ${camel}Plugin as plugin };
|
|
2097
2267
|
`;
|
|
2098
2268
|
}
|
|
2099
2269
|
/**
|
|
@@ -2332,19 +2502,23 @@ ${flowStepsBlock}
|
|
|
2332
2502
|
`;
|
|
2333
2503
|
return { code, payloadFieldNames };
|
|
2334
2504
|
}
|
|
2505
|
+
/** Generates the site's index.ts barrel — exported so the out-of-tree e2e
|
|
2506
|
+
* test can drive the emitter directly without spawning the CLI. */
|
|
2335
2507
|
function emitIndexTs(opts) {
|
|
2336
2508
|
const { siteId } = opts;
|
|
2337
2509
|
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2338
2510
|
return `/**
|
|
2339
2511
|
* Generated by recon-generate.ts.
|
|
2340
|
-
*
|
|
2341
|
-
*
|
|
2512
|
+
* Build this package, then point BARNACLE_PLUGINS at the compiled module —
|
|
2513
|
+
* no core edits required:
|
|
2514
|
+
*
|
|
2515
|
+
* BARNACLE_PLUGINS=./dist/sites/${siteId}/index.js pnpm start
|
|
2342
2516
|
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2517
|
+
* The loader resolves \`m.plugin ?? m.default ?? m\` — the \`plugin\` alias
|
|
2518
|
+
* below is what it finds.
|
|
2345
2519
|
*/
|
|
2346
2520
|
|
|
2347
|
-
export { ${camel}Plugin } from "@/sites/${siteId}/contract";
|
|
2521
|
+
export { ${camel}Plugin, ${camel}Plugin as plugin } from "@/sites/${siteId}/contract";
|
|
2348
2522
|
`;
|
|
2349
2523
|
}
|
|
2350
2524
|
// ── main ──────────────────────────────────────────────────────────────────────
|
|
@@ -2613,6 +2787,7 @@ async function main() {
|
|
|
2613
2787
|
s.capture.url.includes("recruitingCEJobApplicationDrafts"));
|
|
2614
2788
|
if (draftPostStep && !draftPostStep.produces.some((p) => p.name === "draftId")) {
|
|
2615
2789
|
draftPostStep.produces.push({
|
|
2790
|
+
kind: "body",
|
|
2616
2791
|
name: "draftId",
|
|
2617
2792
|
pathExpr: `${draftPostStep.varName}.APPDraftId`,
|
|
2618
2793
|
path: ["APPDraftId"],
|
|
@@ -2621,6 +2796,7 @@ async function main() {
|
|
|
2621
2796
|
const attachPostStep = actionSteps.find((s) => s.capture.method === "POST" && s.capture.url.includes("/attachments"));
|
|
2622
2797
|
if (attachPostStep && !attachPostStep.produces.some((p) => p.name === "attachmentId")) {
|
|
2623
2798
|
attachPostStep.produces.push({
|
|
2799
|
+
kind: "body",
|
|
2624
2800
|
name: "attachmentId",
|
|
2625
2801
|
pathExpr: `${attachPostStep.varName}.Id`,
|
|
2626
2802
|
path: ["Id"],
|
|
@@ -2669,6 +2845,7 @@ async function main() {
|
|
|
2669
2845
|
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride)
|
|
2670
2846
|
: multiStepBody;
|
|
2671
2847
|
const hasMultipartStep = actionSteps.some((s) => s.isMultipart);
|
|
2848
|
+
const headerBindings = collectHeaderBindings(actionSteps);
|
|
2672
2849
|
// For submission flows the final action's response body is the most useful
|
|
2673
2850
|
// shape inference target (it's the terminal success signal). Fall back to
|
|
2674
2851
|
// the replay body for single-endpoint sites.
|
|
@@ -2726,6 +2903,7 @@ async function main() {
|
|
|
2726
2903
|
discoveredRawOptionFields,
|
|
2727
2904
|
discoveredAdditionalBodyKeys,
|
|
2728
2905
|
payloadFieldNames: browserFlow.payloadFieldNames,
|
|
2906
|
+
headerBindings,
|
|
2729
2907
|
}));
|
|
2730
2908
|
logger.info(`wrote ${outDir}/contract.ts`);
|
|
2731
2909
|
(0, node_fs_1.writeFileSync)(`${outDir}/flows/browser-flow.ts`, browserFlow.code);
|