@enricai/barnacle 1.3.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/README.md +35 -1
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js.map +1 -1
- package/dist/api/helpers/envelope.js.map +1 -1
- package/dist/api/helpers/reply.js.map +1 -1
- package/dist/api/plugins/auth.d.ts.map +1 -1
- package/dist/api/plugins/auth.js.map +1 -1
- package/dist/api/plugins/error-handler.d.ts.map +1 -1
- package/dist/api/plugins/error-handler.js.map +1 -1
- package/dist/api/plugins/request-context.d.ts.map +1 -1
- package/dist/api/plugins/request-context.js.map +1 -1
- package/dist/api/routes/health.js.map +1 -1
- package/dist/api/routes/plugins-introspection.js.map +1 -1
- package/dist/api/schemas/common.d.ts +12 -12
- package/dist/api/schemas/common.d.ts.map +1 -1
- package/dist/api/schemas/common.js.map +1 -1
- package/dist/cache/keyed-ttl-cache.js.map +1 -1
- package/dist/cache/response-cache.js.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/lib/applicant-payload.d.ts +3 -3
- package/dist/lib/bedrock.js.map +1 -1
- package/dist/lib/case-insensitive-headers.js.map +1 -1
- package/dist/lib/chromium-client-hints.js.map +1 -1
- package/dist/lib/cookie-value.js.map +1 -1
- package/dist/lib/datadog.js.map +1 -1
- package/dist/lib/dd-metrics.js.map +1 -1
- package/dist/lib/dispatch-metrics.d.ts.map +1 -1
- package/dist/lib/dispatch-metrics.js.map +1 -1
- package/dist/lib/env.js.map +1 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/llm/anthropic-client.js.map +1 -1
- package/dist/lib/llm/judge.js.map +1 -1
- package/dist/lib/llm/judges/error-messages.js.map +1 -1
- package/dist/lib/llm/judges/invalid-fields.js.map +1 -1
- package/dist/lib/llm/judges/modal-priority.js.map +1 -1
- package/dist/lib/llm/judges/select-option.js.map +1 -1
- package/dist/lib/llm/judges/verify-submit.js.map +1 -1
- package/dist/lib/llm/schemas.d.ts +7 -7
- package/dist/lib/llm/schemas.js.map +1 -1
- package/dist/lib/logging.js.map +1 -1
- package/dist/lib/multipart.js.map +1 -1
- package/dist/lib/option-matcher.js.map +1 -1
- package/dist/lib/phone.js.map +1 -1
- package/dist/lib/random.js.map +1 -1
- package/dist/lib/statsd.js.map +1 -1
- package/dist/lib/telemetry/call-capture.d.ts +6 -6
- package/dist/lib/telemetry/call-capture.js.map +1 -1
- package/dist/lib/telemetry/run-state.js.map +1 -1
- package/dist/lib/telemetry/s3-sink.js.map +1 -1
- package/dist/lib/telemetry/submission-capture.js.map +1 -1
- package/dist/lib/telemetry/telemetry-paths.js.map +1 -1
- package/dist/lib/tracking-click.d.ts.map +1 -1
- package/dist/lib/tracking-click.js +5 -1
- package/dist/lib/tracking-click.js.map +1 -1
- package/dist/lib/url-capture.js.map +1 -1
- package/dist/lib/us-states.js.map +1 -1
- package/dist/lib/zod-multipart.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +1 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +4 -0
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/discover.d.ts.map +1 -1
- package/dist/plugins/discover.js +1 -0
- package/dist/plugins/discover.js.map +1 -1
- package/dist/plugins/json-schema-to-zod.d.ts.map +1 -1
- package/dist/plugins/json-schema-to-zod.js.map +1 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +12 -2
- package/dist/plugins/loader.js.map +1 -1
- package/dist/recon/load-vocabulary.d.ts +24 -0
- package/dist/recon/load-vocabulary.d.ts.map +1 -0
- package/dist/recon/load-vocabulary.js +71 -0
- package/dist/recon/load-vocabulary.js.map +1 -0
- package/dist/recon/vocabulary.d.ts +62 -0
- package/dist/recon/vocabulary.d.ts.map +1 -0
- package/dist/recon/vocabulary.js +34 -0
- package/dist/recon/vocabulary.js.map +1 -0
- package/dist/scraper/behavioral-signals.js.map +1 -1
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/fixtures.js.map +1 -1
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/graphql-client.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/http-status-classifier.js.map +1 -1
- package/dist/scraper/metrics.js.map +1 -1
- package/dist/scraper/navigate.js.map +1 -1
- package/dist/scraper/oracle-sentinels.js.map +1 -1
- package/dist/scraper/parse-json-response.js.map +1 -1
- package/dist/scraper/pool.d.ts +2 -3
- package/dist/scraper/pool.d.ts.map +1 -1
- package/dist/scraper/pool.js.map +1 -1
- package/dist/scraper/rate-limited-json-client.js.map +1 -1
- package/dist/scraper/raw-fetch.js.map +1 -1
- package/dist/scraper/require-response-field.js.map +1 -1
- package/dist/scraper/retry.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts +15 -4
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +16 -0
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scraper/session-shared.d.ts +16 -0
- package/dist/scraper/session-shared.d.ts.map +1 -1
- package/dist/scraper/session-shared.js.map +1 -1
- package/dist/scraper/session-steel.js.map +1 -1
- package/dist/scraper/session-warmup.js.map +1 -1
- package/dist/scraper/session.d.ts +6 -6
- package/dist/scraper/session.d.ts.map +1 -1
- package/dist/scraper/session.js +7 -1
- package/dist/scraper/session.js.map +1 -1
- package/dist/scraper/stagehand-guard.d.ts.map +1 -1
- package/dist/scraper/stagehand-guard.js.map +1 -1
- package/dist/scraper/throttle.js.map +1 -1
- package/dist/scripts/judge-llm-batch.js.map +1 -1
- package/dist/scripts/llm-heal.js.map +1 -1
- package/dist/scripts/migrate-telemetry-dir-names.js.map +1 -1
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +92 -6
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +302 -71
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-heal.js.map +1 -1
- package/dist/scripts/recon-shared.js.map +1 -1
- package/dist/server.js.map +1 -1
- package/dist/site-plugin.d.ts +9 -0
- package/dist/site-plugin.d.ts.map +1 -1
- package/dist/testing/batch-email-confirmation.js.map +1 -1
- package/dist/testing/batch-report.js.map +1 -1
- package/dist/testing/integration-runner.js.map +1 -1
- package/dist/testing/mock-fetch-response.js.map +1 -1
- package/dist/testing/persona-fixture.d.ts.map +1 -1
- package/dist/testing/resume-fixture.js.map +1 -1
- package/dist/testmail/client.js.map +1 -1
- package/dist/testmail/errors.d.ts.map +1 -1
- package/dist/testmail/errors.js.map +1 -1
- package/dist/types/dispatch-metrics.d.ts +2 -2
- package/package.json +17 -2
|
@@ -20,16 +20,22 @@ 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");
|
|
37
|
+
const load_vocabulary_1 = require("../recon/load-vocabulary");
|
|
38
|
+
const vocabulary_1 = require("../recon/vocabulary");
|
|
33
39
|
const recon_shared_1 = require("../scripts/recon-shared");
|
|
34
40
|
const logger = (0, logging_1.getScriptLogger)("recon-generate");
|
|
35
41
|
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -40,46 +46,52 @@ function toPascalCase(siteId) {
|
|
|
40
46
|
.join("");
|
|
41
47
|
}
|
|
42
48
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
/employment history/i,
|
|
52
|
-
/\bcompany (name|phone)\b/i,
|
|
53
|
-
/\bemployer\b/i,
|
|
54
|
-
/signature/i,
|
|
55
|
-
/\bfull name\b/i,
|
|
56
|
-
/today'?s date/i,
|
|
57
|
-
/school|institution|degree|major|education/i,
|
|
58
|
-
// Screening-question shapes: "For '<question>' select '<answer>'" and any
|
|
59
|
-
// step framed around a 'question'. Here the first quoted string is the
|
|
60
|
-
// question label, not a candidate value — a label word inside the question
|
|
61
|
-
// (e.g. "...licensed in this state?") must NOT trigger a splice that would
|
|
62
|
-
// overwrite the question quote. Candidate-fill steps say "fill in the X
|
|
63
|
-
// field with '...'" / "Select '...' from the X dropdown" instead.
|
|
64
|
-
/^\s*for\s+'/i,
|
|
65
|
-
/\bquestion\b/i,
|
|
66
|
-
];
|
|
67
|
-
/**
|
|
68
|
-
* Ordered candidate-field label table. Each entry maps an English-label regex
|
|
69
|
-
* to the PascalCase payload field the generator should splice. Order matters:
|
|
70
|
-
* the first matching row wins, so more-specific labels precede broader ones.
|
|
49
|
+
* The recruiting vocabulary the engine used to hardcode, kept only so consumers
|
|
50
|
+
* who have not yet passed `--vocabulary` keep working through the 1.x line.
|
|
51
|
+
*
|
|
52
|
+
* @deprecated Pass `--vocabulary <specifier>` instead. The engine cannot know
|
|
53
|
+
* what any site's forms mean, and guessing mis-fires on every non-recruiting
|
|
54
|
+
* domain (a cruise site's "Select the departure port from the Country dropdown"
|
|
55
|
+
* became `${payload.Country}`). Slated for deletion in 2.0.0, after which an
|
|
56
|
+
* absent vocabulary on a spliceable flow is a hard error. See `src/recon/vocabulary.ts`.
|
|
71
57
|
*/
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
58
|
+
const DEPRECATED_BUILTIN_ATS_VOCABULARY = {
|
|
59
|
+
// Naming the subject is what separates "…select the test candidate's state"
|
|
60
|
+
// (fill with the caller's data) from "…the departure port from the Country
|
|
61
|
+
// dropdown" (a search facet that merely says Country).
|
|
62
|
+
subject: /\b(the\s+)?(test\s+)?(candidate|applicant)'?s\b/i,
|
|
63
|
+
exclusions: [
|
|
64
|
+
/reference\s*#?\s*\d/i,
|
|
65
|
+
/employment history/i,
|
|
66
|
+
/\bcompany (name|phone)\b/i,
|
|
67
|
+
/\bemployer\b/i,
|
|
68
|
+
/signature/i,
|
|
69
|
+
/\bfull name\b/i,
|
|
70
|
+
/today'?s date/i,
|
|
71
|
+
/school|institution|degree|major|education/i,
|
|
72
|
+
// Screening-question shapes: "For '<question>' select '<answer>'" and any
|
|
73
|
+
// step framed around a 'question'. Here the first quoted string is the
|
|
74
|
+
// question label, not a candidate value — a label word inside the question
|
|
75
|
+
// (e.g. "...licensed in this state?") must NOT trigger a splice that would
|
|
76
|
+
// overwrite the question quote. Candidate-fill steps say "fill in the X
|
|
77
|
+
// field with '...'" / "Select '...' from the X dropdown" instead.
|
|
78
|
+
/^\s*for\s+'/i,
|
|
79
|
+
/\bquestion\b/i,
|
|
80
|
+
// A secondary phone must not fill the primary MobilePhone field.
|
|
81
|
+
/\bsecondary\b[^.]*\bphone\b/i,
|
|
82
|
+
],
|
|
83
|
+
table: [
|
|
84
|
+
[/\bfirst name\b/i, "FirstName"],
|
|
85
|
+
[/\blast name\b/i, "LastName"],
|
|
86
|
+
[/\b(e-?mail|email address)\b/i, "Email"],
|
|
87
|
+
[/\b(mobile phone|primary phone|phone number|mobile)\b/i, "MobilePhone"],
|
|
88
|
+
[/\b(street address|address line 1)\b/i, "AddressLine1"],
|
|
89
|
+
[/\bcity\b/i, "City"],
|
|
90
|
+
[/\b(state|province|state\/region)\b/i, "State"],
|
|
91
|
+
[/\b(zip|postal)\b/i, "PostalCode"],
|
|
92
|
+
[/\bcountry\b/i, "Country"],
|
|
93
|
+
],
|
|
94
|
+
};
|
|
83
95
|
/**
|
|
84
96
|
* Decide whether a flow step should splice a runtime `payload.<field>` value in
|
|
85
97
|
* place of the frozen recon constant baked into its instruction. Exists so
|
|
@@ -92,27 +104,29 @@ const PAYLOAD_FIELD_TABLE = [
|
|
|
92
104
|
* @param instruction the flow step's plain-English instruction
|
|
93
105
|
* @param explicit an optional flow-authored `payloadField` override (wins outright)
|
|
94
106
|
* @param forceNone when true, force a literal step (the `payloadFieldNone` opt-out)
|
|
107
|
+
* @param vocabulary the consumer's domain vocabulary; defaults to the deprecated
|
|
108
|
+
* built-in recruiting table so 1.x consumers who pass nothing keep working
|
|
95
109
|
* @returns the PascalCase payload field name to splice, or null to keep literal
|
|
96
110
|
*/
|
|
97
|
-
function resolveStepPayloadField(instruction, explicit, forceNone) {
|
|
111
|
+
function resolveStepPayloadField(instruction, explicit, forceNone, vocabulary = DEPRECATED_BUILTIN_ATS_VOCABULARY) {
|
|
98
112
|
if (forceNone)
|
|
99
113
|
return null;
|
|
100
114
|
if (explicit)
|
|
101
115
|
return explicit;
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
// A quoted literal or ${RECON_EMAIL} IS the recon constant this step would
|
|
117
|
+
// replace, so it is spliceable on its own.
|
|
118
|
+
const hasQuotedConstant = /'[^']*'/.test(instruction) || /\$\{RECON_EMAIL\}/.test(instruction);
|
|
119
|
+
// A dropdown step carries no constant to replace, so a label match alone can't
|
|
120
|
+
// tell "select the test candidate's state" (the caller's data) from "select the
|
|
121
|
+
// departure port from the Country dropdown" (a facet that merely says Country).
|
|
122
|
+
// Requiring the subject is what keeps this from mis-firing off-domain.
|
|
123
|
+
const isDropdownStep = /\bdropdown\b/i.test(instruction) || /\bselect\b[^.]*\bfrom\b/i.test(instruction);
|
|
124
|
+
const hasSpliceable = hasQuotedConstant || (isDropdownStep && vocabulary.subject.test(instruction));
|
|
108
125
|
if (!hasSpliceable)
|
|
109
126
|
return null;
|
|
110
|
-
if (
|
|
111
|
-
return null;
|
|
112
|
-
// "Secondary Phone Number" must not fill the primary MobilePhone field.
|
|
113
|
-
if (/\bsecondary\b/i.test(instruction) && /\bphone\b/i.test(instruction))
|
|
127
|
+
if (vocabulary.exclusions.some((rx) => rx.test(instruction)))
|
|
114
128
|
return null;
|
|
115
|
-
for (const [rx, field] of
|
|
129
|
+
for (const [rx, field] of vocabulary.table) {
|
|
116
130
|
if (rx.test(instruction))
|
|
117
131
|
return field;
|
|
118
132
|
}
|
|
@@ -1047,12 +1061,35 @@ const MAX_STATE_VALUE_LENGTH = 256;
|
|
|
1047
1061
|
* does not exist". Closed set, literal-string match — never expand to
|
|
1048
1062
|
* pattern-based detection (would trip the no-regex-on-open-sets rule). */
|
|
1049
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
|
+
}
|
|
1050
1082
|
/**
|
|
1051
1083
|
* Walks every capture's response (including GETs — formHistoryId-style values
|
|
1052
1084
|
* may originate in a state-load GET, not a POST). Indexes every string leaf
|
|
1053
1085
|
* whose length is in [MIN, MAX], recording the EARLIEST capture index that
|
|
1054
1086
|
* produced it. Later occurrences of the same value reuse the earliest origin.
|
|
1055
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
|
+
*
|
|
1056
1093
|
* The index is intentionally permissive — it doesn't try to shape-match
|
|
1057
1094
|
* "what looks like a token" because token shapes are an open set across the
|
|
1058
1095
|
* web. Authoritative filtering happens downstream in `compileActionSteps`,
|
|
@@ -1063,6 +1100,8 @@ const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"
|
|
|
1063
1100
|
* the LATER non-placeholder occurrence at the same JSON path becomes the
|
|
1064
1101
|
* canonical binding instead.
|
|
1065
1102
|
*/
|
|
1103
|
+
/** Exported for unit testing — lets tests exercise the produces[] walk (body
|
|
1104
|
+
* AND header/cookie origins) directly against synthetic Capture sequences. */
|
|
1066
1105
|
function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndices = new Set()) {
|
|
1067
1106
|
const index = new Map();
|
|
1068
1107
|
// First pass: identify the earliest origin among ACTION captures for each
|
|
@@ -1075,10 +1114,32 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1075
1114
|
const haveActionFilter = actionCaptureIndices.size > 0;
|
|
1076
1115
|
for (let i = 0; i < captures.length; i++) {
|
|
1077
1116
|
const c = captures[i];
|
|
1078
|
-
if (c.responseBody === undefined || c.responseBody === null)
|
|
1079
|
-
continue;
|
|
1080
1117
|
if (haveActionFilter && !actionCaptureIndices.has(i))
|
|
1081
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;
|
|
1082
1143
|
// For GET captures, only index UUID-shaped strings. GET captures (today,
|
|
1083
1144
|
// only the form-schema fetch inserted as an action step) surface stable
|
|
1084
1145
|
// structural identifiers — UUIDs that downstream POSTs need to thread.
|
|
@@ -1115,9 +1176,18 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1115
1176
|
function isValidJsIdentifier(s) {
|
|
1116
1177
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
|
|
1117
1178
|
}
|
|
1118
|
-
/**
|
|
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
|
+
*/
|
|
1119
1189
|
function pathToAccessor(path) {
|
|
1120
|
-
return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]
|
|
1190
|
+
return path.map((p) => (isValidJsIdentifier(p) ? `.${p}` : `[${JSON.stringify(p)}]!`)).join("");
|
|
1121
1191
|
}
|
|
1122
1192
|
/**
|
|
1123
1193
|
* Builds a nested TypeScript assertion type matching a JSON path. e.g.
|
|
@@ -1151,26 +1221,75 @@ function pathToVarName(path) {
|
|
|
1151
1221
|
* var name, the state values its response produces (used by downstream steps),
|
|
1152
1222
|
* and a multipart flag (request body bytes not captured).
|
|
1153
1223
|
*/
|
|
1224
|
+
/** Exported for unit testing — see `indexStateValues`. */
|
|
1154
1225
|
function compileActionSteps(actions, stateIndex) {
|
|
1155
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();
|
|
1156
1235
|
// Pre-scan: collect all state values referenced by ANY action's URL/headers/body
|
|
1157
1236
|
// so we only "produce" the values that are actually consumed downstream.
|
|
1158
1237
|
for (const { capture } of actions) {
|
|
1159
1238
|
const haystacks = [capture.url];
|
|
1160
|
-
for (const v of Object.values(capture.requestHeaders))
|
|
1161
|
-
haystacks.push(v);
|
|
1162
1239
|
if (capture.requestPostData)
|
|
1163
1240
|
haystacks.push(capture.requestPostData);
|
|
1164
1241
|
for (const sv of stateIndex.values()) {
|
|
1165
1242
|
if (haystacks.some((h) => h.includes(sv.value)))
|
|
1166
1243
|
usedValues.add(sv.value);
|
|
1167
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
|
+
}
|
|
1168
1255
|
}
|
|
1169
1256
|
let lastHost = null;
|
|
1170
1257
|
return actions.map(({ capture, index }, i) => {
|
|
1171
1258
|
const varName = `r${i}`;
|
|
1172
1259
|
const produces = [];
|
|
1173
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
|
+
}
|
|
1174
1293
|
if (capture.responseBody !== undefined && capture.responseBody !== null) {
|
|
1175
1294
|
for (const { value, path } of walkStringLeaves(capture.responseBody)) {
|
|
1176
1295
|
if (!usedValues.has(value))
|
|
@@ -1186,7 +1305,7 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
1186
1305
|
name = `${pathToVarName(path)}${suffix}`;
|
|
1187
1306
|
}
|
|
1188
1307
|
seenNames.add(name);
|
|
1189
|
-
produces.push({ name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
|
|
1308
|
+
produces.push({ kind: "body", name, pathExpr: `${varName}${pathToAccessor(path)}`, path });
|
|
1190
1309
|
}
|
|
1191
1310
|
}
|
|
1192
1311
|
const ct = Object.entries(capture.requestHeaders).find(([k]) => k.toLowerCase() === "content-type");
|
|
@@ -1203,6 +1322,27 @@ function compileActionSteps(actions, stateIndex) {
|
|
|
1203
1322
|
return { capture, varName, produces, isMultipart, isCrossDomain };
|
|
1204
1323
|
});
|
|
1205
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
|
+
}
|
|
1206
1346
|
/**
|
|
1207
1347
|
* Replaces occurrences of state values in `template` with `${varName}`
|
|
1208
1348
|
* interpolations. Returns a JS template-literal string fragment (no backticks).
|
|
@@ -1217,6 +1357,12 @@ function interpolateStateValues(template, priorSteps, payloadAccessorByValue = n
|
|
|
1217
1357
|
const varNameByValue = new Map();
|
|
1218
1358
|
for (const step of priorSteps) {
|
|
1219
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;
|
|
1220
1366
|
let cursor = step.capture.responseBody;
|
|
1221
1367
|
for (const segment of p.path) {
|
|
1222
1368
|
if (cursor !== null &&
|
|
@@ -1810,6 +1956,11 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
1810
1956
|
}
|
|
1811
1957
|
}
|
|
1812
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;
|
|
1813
1964
|
if (declaredNames.has(p.name))
|
|
1814
1965
|
continue;
|
|
1815
1966
|
if (!referencedNames.has(p.name))
|
|
@@ -1835,11 +1986,30 @@ function summariseResponseShape(value) {
|
|
|
1835
1986
|
}
|
|
1836
1987
|
return obj;
|
|
1837
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
|
+
}
|
|
1838
2008
|
// ── code emitters ─────────────────────────────────────────────────────────────
|
|
1839
2009
|
/** Generates a complete contract.ts source string for a plugin — exported so
|
|
1840
2010
|
* unit tests can drive the emitter directly without spawning the CLI. */
|
|
1841
2011
|
function emitContractTs(opts) {
|
|
1842
|
-
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;
|
|
1843
2013
|
// Multi-step plugins thread responses through many different shapes that a
|
|
1844
2014
|
// single Zod schema can't cover — use z.unknown() so each per-step access
|
|
1845
2015
|
// compiles cleanly. Single-endpoint plugins keep the inferred schema.
|
|
@@ -1997,7 +2167,7 @@ function getGql(baseUrl: string): GqlFn {
|
|
|
1997
2167
|
}
|
|
1998
2168
|
`
|
|
1999
2169
|
: `
|
|
2000
|
-
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)} });
|
|
2001
2171
|
`;
|
|
2002
2172
|
const executeHttpBody = multiStepBody
|
|
2003
2173
|
? multiStepBody
|
|
@@ -2019,6 +2189,7 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
2019
2189
|
const queryChecklistLine = gql
|
|
2020
2190
|
? `\n * [ ] Trim UI-only fields from ${pascal.toUpperCase()}_QUERY (keep only fields you need)`
|
|
2021
2191
|
: "";
|
|
2192
|
+
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2022
2193
|
return `/**
|
|
2023
2194
|
* Generated by recon-generate.ts — review before shipping.
|
|
2024
2195
|
*
|
|
@@ -2026,6 +2197,9 @@ const httpClient = createHttpClient({ schema: ${pascal}ResponseSchema, bottlenec
|
|
|
2026
2197
|
* [ ] Narrow ${pascal}ResponseSchema to match the real response shape
|
|
2027
2198
|
* [ ] Adjust ${pascal}PayloadSchema to your actual request parameters
|
|
2028
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
|
|
2029
2203
|
*/
|
|
2030
2204
|
|
|
2031
2205
|
import Bottleneck from "bottleneck";
|
|
@@ -2057,13 +2231,14 @@ ${queryConst}${gqlCacheBlock}${fixtureComments}
|
|
|
2057
2231
|
* Plugin for ${siteId}. Tries the direct-HTTP hot path first; falls back to
|
|
2058
2232
|
* Stagehand automatically on schema drift or bot challenge.
|
|
2059
2233
|
*/
|
|
2060
|
-
export const ${
|
|
2234
|
+
export const ${camel}Plugin: SitePlugin<${pascal}Payload, ${pascal}Response> = {
|
|
2061
2235
|
meta: {
|
|
2062
2236
|
siteId: ${JSON.stringify(siteId)},
|
|
2063
2237
|
displayName: ${JSON.stringify(pascal.replace(/([A-Z])/g, " $1").trim())},
|
|
2064
2238
|
bodySchema: ${pascal}PayloadSchema,
|
|
2065
2239
|
responseSchema: ${pascal}ResponseSchema,
|
|
2066
|
-
defaultBaseUrl: ${JSON.stringify(baseUrl)}
|
|
2240
|
+
defaultBaseUrl: ${JSON.stringify(baseUrl)},
|
|
2241
|
+
apiVersion: ${JSON.stringify(plugin_api_version_1.PLUGIN_API_VERSION)},${hasMultipartStep ? "\n multipart: true," : ""}
|
|
2067
2242
|
},
|
|
2068
2243
|
|
|
2069
2244
|
/** Hot path: direct HTTP — no browser, no LLM tokens. */
|
|
@@ -2084,6 +2259,11 @@ ${executeHttpBody}
|
|
|
2084
2259
|
return { data: raw as ${pascal}Response };
|
|
2085
2260
|
},
|
|
2086
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 };
|
|
2087
2267
|
`;
|
|
2088
2268
|
}
|
|
2089
2269
|
/**
|
|
@@ -2162,12 +2342,12 @@ function buildManifestInstruction(instruction, field) {
|
|
|
2162
2342
|
* omitted; a site that needs it keeps the `.ts` path or wires `spec.httpModule`.
|
|
2163
2343
|
*/
|
|
2164
2344
|
function emitConfigManifest(opts) {
|
|
2165
|
-
const { siteId, displayName, baseUrl, flowSteps } = opts;
|
|
2345
|
+
const { siteId, displayName, baseUrl, flowSteps, vocabulary } = opts;
|
|
2166
2346
|
const payloadFieldNames = new Set();
|
|
2167
2347
|
const steps = flowSteps.map((step) => {
|
|
2168
2348
|
const isObj = typeof step !== "string";
|
|
2169
2349
|
const instruction = isObj ? step.step : step;
|
|
2170
|
-
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined);
|
|
2350
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
|
|
2171
2351
|
if (field !== null)
|
|
2172
2352
|
payloadFieldNames.add(field);
|
|
2173
2353
|
const rewritten = buildManifestInstruction(instruction, field);
|
|
@@ -2211,13 +2391,13 @@ function emitConfigManifest(opts) {
|
|
|
2211
2391
|
* payload schema (both are driven by this same set).
|
|
2212
2392
|
*/
|
|
2213
2393
|
function emitBrowserFlowTs(opts) {
|
|
2214
|
-
const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false } = opts;
|
|
2394
|
+
const { siteId, pascal, flowSteps, isSubmissionFlow, hasMultipartStep = false, vocabulary, } = opts;
|
|
2215
2395
|
const payloadFieldNames = new Set();
|
|
2216
2396
|
const hasUploadStep = flowSteps.some((s) => typeof s !== "string" && s.upload === true);
|
|
2217
2397
|
const stepLiterals = flowSteps.map((step) => {
|
|
2218
2398
|
const isObj = typeof step !== "string";
|
|
2219
2399
|
const instruction = isObj ? step.step : step;
|
|
2220
|
-
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined);
|
|
2400
|
+
const field = resolveStepPayloadField(instruction, isObj ? step.payloadField : undefined, isObj ? step.payloadFieldNone : undefined, vocabulary);
|
|
2221
2401
|
if (field !== null)
|
|
2222
2402
|
payloadFieldNames.add(field);
|
|
2223
2403
|
const instructionExpr = buildStepInstructionExpr(instruction, field);
|
|
@@ -2322,30 +2502,71 @@ ${flowStepsBlock}
|
|
|
2322
2502
|
`;
|
|
2323
2503
|
return { code, payloadFieldNames };
|
|
2324
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. */
|
|
2325
2507
|
function emitIndexTs(opts) {
|
|
2326
2508
|
const { siteId } = opts;
|
|
2327
2509
|
const camel = siteId.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
2328
2510
|
return `/**
|
|
2329
2511
|
* Generated by recon-generate.ts.
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
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
|
|
2332
2516
|
*
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2517
|
+
* The loader resolves \`m.plugin ?? m.default ?? m\` — the \`plugin\` alias
|
|
2518
|
+
* below is what it finds.
|
|
2335
2519
|
*/
|
|
2336
2520
|
|
|
2337
|
-
export { ${camel}Plugin } from "@/sites/${siteId}/contract";
|
|
2521
|
+
export { ${camel}Plugin, ${camel}Plugin as plugin } from "@/sites/${siteId}/contract";
|
|
2338
2522
|
`;
|
|
2339
2523
|
}
|
|
2340
2524
|
// ── main ──────────────────────────────────────────────────────────────────────
|
|
2525
|
+
/**
|
|
2526
|
+
* Resolves the vocabulary for this run and reports which one is in play.
|
|
2527
|
+
*
|
|
2528
|
+
* The deprecation warning is the whole point of the 1.x window: removing a
|
|
2529
|
+
* built-in default that consumers silently depend on is only safe if the
|
|
2530
|
+
* transition is loud. It fires only when the built-in would actually change the
|
|
2531
|
+
* output — a flow with nothing to splice gets no nag it can't act on.
|
|
2532
|
+
*/
|
|
2533
|
+
async function resolveVocabulary(specifier, flowSteps) {
|
|
2534
|
+
if (specifier) {
|
|
2535
|
+
const vocabulary = await (0, load_vocabulary_1.loadReconVocabulary)(specifier, process.cwd());
|
|
2536
|
+
logger.info(`vocabulary: ${specifier === load_vocabulary_1.VOCABULARY_NONE ? "none (splicing disabled)" : `${vocabulary.table.length} row(s) from ${specifier}`}`);
|
|
2537
|
+
return vocabulary;
|
|
2538
|
+
}
|
|
2539
|
+
// Warn only when the built-in table itself changes the outcome. A step with an
|
|
2540
|
+
// explicit payloadField resolves the same under any vocabulary, so nagging
|
|
2541
|
+
// about it would send consumers to fix something that isn't broken.
|
|
2542
|
+
const builtinChangesOutcome = flowSteps.some((step) => {
|
|
2543
|
+
const isObj = typeof step !== "string";
|
|
2544
|
+
const instruction = isObj ? step.step : step;
|
|
2545
|
+
const explicit = isObj ? step.payloadField : undefined;
|
|
2546
|
+
const forceNone = isObj ? step.payloadFieldNone : undefined;
|
|
2547
|
+
const withBuiltin = resolveStepPayloadField(instruction, explicit, forceNone, DEPRECATED_BUILTIN_ATS_VOCABULARY);
|
|
2548
|
+
const withNothing = resolveStepPayloadField(instruction, explicit, forceNone, vocabulary_1.EMPTY_VOCABULARY);
|
|
2549
|
+
return withBuiltin !== withNothing;
|
|
2550
|
+
});
|
|
2551
|
+
if (builtinChangesOutcome) {
|
|
2552
|
+
logger.warn(`DeprecationWarning: no --vocabulary given, falling back to the built-in recruiting table. ` +
|
|
2553
|
+
`It is removed in 2.0.0, after which this is an error. Fix: create a vocabulary module ` +
|
|
2554
|
+
`exporting a ReconVocabulary (see @enricai/barnacle/recon/vocabulary) and pass ` +
|
|
2555
|
+
`--vocabulary ./src/recon/<name>.ts, or --vocabulary none if this site splices no caller data.`);
|
|
2556
|
+
}
|
|
2557
|
+
return DEPRECATED_BUILTIN_ATS_VOCABULARY;
|
|
2558
|
+
}
|
|
2341
2559
|
async function main() {
|
|
2342
2560
|
const args = process.argv.slice(2);
|
|
2343
2561
|
let siteId = "";
|
|
2344
2562
|
let force = false;
|
|
2345
2563
|
let emit = "ts";
|
|
2564
|
+
let vocabularySpecifier = "";
|
|
2346
2565
|
for (let i = 0; i < args.length; i++) {
|
|
2347
2566
|
if (args[i] === "--site-id" && args[i + 1])
|
|
2348
2567
|
siteId = args[++i];
|
|
2568
|
+
else if (args[i] === "--vocabulary" && args[i + 1])
|
|
2569
|
+
vocabularySpecifier = args[++i];
|
|
2349
2570
|
else if (args[i] === "--force")
|
|
2350
2571
|
force = true;
|
|
2351
2572
|
else if (args[i] === "--emit" && args[i + 1]) {
|
|
@@ -2412,6 +2633,10 @@ async function main() {
|
|
|
2412
2633
|
return [];
|
|
2413
2634
|
}
|
|
2414
2635
|
})();
|
|
2636
|
+
// Resolved once and threaded down, never captured into a module const: a
|
|
2637
|
+
// module-level const would freeze at import time, which is the bug that makes
|
|
2638
|
+
// RECON_QUESTION_KEYWORDS silently inert for anyone setting it after load.
|
|
2639
|
+
const vocabulary = await resolveVocabulary(vocabularySpecifier, flowSteps);
|
|
2415
2640
|
const pascal = toPascalCase(siteId);
|
|
2416
2641
|
const baseUrl = deriveBaseUrl(captures);
|
|
2417
2642
|
const baseHeaders = deriveRequestHeaders(captures, replays, baseUrl);
|
|
@@ -2562,6 +2787,7 @@ async function main() {
|
|
|
2562
2787
|
s.capture.url.includes("recruitingCEJobApplicationDrafts"));
|
|
2563
2788
|
if (draftPostStep && !draftPostStep.produces.some((p) => p.name === "draftId")) {
|
|
2564
2789
|
draftPostStep.produces.push({
|
|
2790
|
+
kind: "body",
|
|
2565
2791
|
name: "draftId",
|
|
2566
2792
|
pathExpr: `${draftPostStep.varName}.APPDraftId`,
|
|
2567
2793
|
path: ["APPDraftId"],
|
|
@@ -2570,6 +2796,7 @@ async function main() {
|
|
|
2570
2796
|
const attachPostStep = actionSteps.find((s) => s.capture.method === "POST" && s.capture.url.includes("/attachments"));
|
|
2571
2797
|
if (attachPostStep && !attachPostStep.produces.some((p) => p.name === "attachmentId")) {
|
|
2572
2798
|
attachPostStep.produces.push({
|
|
2799
|
+
kind: "body",
|
|
2573
2800
|
name: "attachmentId",
|
|
2574
2801
|
pathExpr: `${attachPostStep.varName}.Id`,
|
|
2575
2802
|
path: ["Id"],
|
|
@@ -2618,6 +2845,7 @@ async function main() {
|
|
|
2618
2845
|
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride)
|
|
2619
2846
|
: multiStepBody;
|
|
2620
2847
|
const hasMultipartStep = actionSteps.some((s) => s.isMultipart);
|
|
2848
|
+
const headerBindings = collectHeaderBindings(actionSteps);
|
|
2621
2849
|
// For submission flows the final action's response body is the most useful
|
|
2622
2850
|
// shape inference target (it's the terminal success signal). Fall back to
|
|
2623
2851
|
// the replay body for single-endpoint sites.
|
|
@@ -2632,6 +2860,7 @@ async function main() {
|
|
|
2632
2860
|
displayName: pascal,
|
|
2633
2861
|
baseUrl,
|
|
2634
2862
|
flowSteps,
|
|
2863
|
+
vocabulary,
|
|
2635
2864
|
}));
|
|
2636
2865
|
logger.info(`wrote ${manifestPath}`);
|
|
2637
2866
|
logger.info(`done — review ${manifestPath}, fill in response/extract schemas, then load via BARNACLE_PLUGINS or BARNACLE_PLUGINS_CONFIG_DIR (no compile step)`);
|
|
@@ -2648,6 +2877,7 @@ async function main() {
|
|
|
2648
2877
|
flowSteps,
|
|
2649
2878
|
isSubmissionFlow,
|
|
2650
2879
|
hasMultipartStep,
|
|
2880
|
+
vocabulary,
|
|
2651
2881
|
});
|
|
2652
2882
|
(0, node_fs_1.writeFileSync)(`${outDir}/contract.ts`, emitContractTs({
|
|
2653
2883
|
siteId,
|
|
@@ -2673,6 +2903,7 @@ async function main() {
|
|
|
2673
2903
|
discoveredRawOptionFields,
|
|
2674
2904
|
discoveredAdditionalBodyKeys,
|
|
2675
2905
|
payloadFieldNames: browserFlow.payloadFieldNames,
|
|
2906
|
+
headerBindings,
|
|
2676
2907
|
}));
|
|
2677
2908
|
logger.info(`wrote ${outDir}/contract.ts`);
|
|
2678
2909
|
(0, node_fs_1.writeFileSync)(`${outDir}/flows/browser-flow.ts`, browserFlow.code);
|