@enricai/barnacle 1.5.0 → 1.6.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/README.md +29 -0
- package/dist/api/errors.d.ts +2 -1
- package/dist/api/errors.d.ts.map +1 -1
- package/dist/api/errors.js +3 -2
- package/dist/api/errors.js.map +1 -1
- package/dist/lib/llm/judge.d.ts +1 -1
- package/dist/lib/llm/judge.js +1 -1
- package/dist/recon/capture-filters.d.ts +40 -0
- package/dist/recon/capture-filters.d.ts.map +1 -0
- package/dist/recon/capture-filters.js +106 -0
- package/dist/recon/capture-filters.js.map +1 -0
- package/dist/recon/form-schema.d.ts +51 -0
- package/dist/recon/form-schema.d.ts.map +1 -0
- package/dist/recon/form-schema.js +20 -0
- package/dist/recon/form-schema.js.map +1 -0
- package/dist/recon/load-form-schema.d.ts +24 -0
- package/dist/recon/load-form-schema.d.ts.map +1 -0
- package/dist/recon/load-form-schema.js +66 -0
- package/dist/recon/load-form-schema.js.map +1 -0
- package/dist/scraper/errors.d.ts +14 -14
- package/dist/scraper/errors.d.ts.map +1 -1
- package/dist/scraper/errors.js +15 -15
- package/dist/scraper/errors.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +9 -8
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/http-client.d.ts +12 -0
- package/dist/scraper/http-client.d.ts.map +1 -1
- package/dist/scraper/http-client.js +11 -15
- package/dist/scraper/http-client.js.map +1 -1
- package/dist/scraper/parse-json-response.d.ts +7 -5
- package/dist/scraper/parse-json-response.d.ts.map +1 -1
- package/dist/scraper/parse-json-response.js +9 -8
- package/dist/scraper/parse-json-response.js.map +1 -1
- package/dist/scraper/rate-limited-json-client.d.ts +8 -1
- package/dist/scraper/rate-limited-json-client.d.ts.map +1 -1
- package/dist/scraper/rate-limited-json-client.js +6 -1
- package/dist/scraper/rate-limited-json-client.js.map +1 -1
- package/dist/scraper/session-browserbase.d.ts +6 -0
- package/dist/scraper/session-browserbase.d.ts.map +1 -1
- package/dist/scraper/session-browserbase.js +6 -0
- package/dist/scraper/session-browserbase.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +3 -4
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +50 -21
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +190 -182
- package/dist/scripts/recon-generate.js.map +1 -1
- package/dist/scripts/recon-http.d.ts +5 -2
- package/dist/scripts/recon-http.d.ts.map +1 -1
- package/dist/scripts/recon-http.js +16 -4
- package/dist/scripts/recon-http.js.map +1 -1
- package/dist/testing/persona-fixture.d.ts +2 -2
- package/dist/testing/persona-fixture.d.ts.map +1 -1
- package/dist/testing/persona-fixture.js +2 -2
- package/dist/testing/persona-fixture.js.map +1 -1
- package/package.json +5 -1
- package/dist/scraper/oracle-sentinels.d.ts +0 -22
- package/dist/scraper/oracle-sentinels.d.ts.map +0 -1
- package/dist/scraper/oracle-sentinels.js +0 -40
- package/dist/scraper/oracle-sentinels.js.map +0 -1
|
@@ -21,6 +21,7 @@ exports.resolveStepPayloadField = resolveStepPayloadField;
|
|
|
21
21
|
exports.inferZodSchemaFromSamples = inferZodSchemaFromSamples;
|
|
22
22
|
exports.selectPayloadAction = selectPayloadAction;
|
|
23
23
|
exports.extractActionSequence = extractActionSequence;
|
|
24
|
+
exports.detectFormSchemaFieldNames = detectFormSchemaFieldNames;
|
|
24
25
|
exports.indexStateValues = indexStateValues;
|
|
25
26
|
exports.compileActionSteps = compileActionSteps;
|
|
26
27
|
exports.loadQuestionPromptKeywords = loadQuestionPromptKeywords;
|
|
@@ -35,6 +36,8 @@ const errors_1 = require("../lib/errors");
|
|
|
35
36
|
const logging_1 = require("../lib/logging");
|
|
36
37
|
const plugin_api_version_1 = require("../plugins/plugin-api-version");
|
|
37
38
|
const plugin_manifest_envelope_1 = require("../plugins/plugin-manifest-envelope");
|
|
39
|
+
const capture_filters_1 = require("../recon/capture-filters");
|
|
40
|
+
const load_form_schema_1 = require("../recon/load-form-schema");
|
|
38
41
|
const load_vocabulary_1 = require("../recon/load-vocabulary");
|
|
39
42
|
const vocabulary_1 = require("../recon/vocabulary");
|
|
40
43
|
const recon_shared_1 = require("../scripts/recon-shared");
|
|
@@ -334,9 +337,8 @@ const IGNORE_REQUEST_HEADERS = new Set([
|
|
|
334
337
|
* stateless replay phase can't thread), derive headers from the meaningful
|
|
335
338
|
* action POSTs instead — same `extractActionSequence` definition used by
|
|
336
339
|
* the submission-flow detector. This catches load-bearing site-specific
|
|
337
|
-
* headers (
|
|
338
|
-
*
|
|
339
|
-
* know about any particular site.
|
|
340
|
+
* headers (a `X-CSRF-Token`, a `Job-Boards-API-Token`, an `API-ShortName`,
|
|
341
|
+
* etc.) without the generator needing to know about any particular site.
|
|
340
342
|
*/
|
|
341
343
|
function deriveRequestHeaders(captures, replays, baseUrl) {
|
|
342
344
|
const successfulUrls = new Set(replays.filter((r) => r.success).map((r) => endpointKey(r.url)));
|
|
@@ -403,42 +405,6 @@ function firstEndpointPath(captures) {
|
|
|
403
405
|
}
|
|
404
406
|
return "/api/search";
|
|
405
407
|
}
|
|
406
|
-
// ── multi-step "submission flow" detection ────────────────────────────────────
|
|
407
|
-
//
|
|
408
|
-
// For transactional sites (apply forms, multi-step checkout, etc.) the captures
|
|
409
|
-
// form an ordered sequence of POSTs that thread state values through subsequent
|
|
410
|
-
// requests (auth tokens, candidate IDs, application IDs). Single-endpoint
|
|
411
|
-
// sites (job search, pricing APIs) have one action capture and skip this path.
|
|
412
|
-
/**
|
|
413
|
-
* Path elements we always treat as noise (analytics, logging). Site-specific
|
|
414
|
-
* trackers belong in RECON_TELEMETRY_URL_PATTERNS (comma-separated), not here —
|
|
415
|
-
* the engine must not carry any one site's ad-tech domains.
|
|
416
|
-
*/
|
|
417
|
-
const TELEMETRY_URL_PATTERNS = [
|
|
418
|
-
"/util/logging/vweb/message",
|
|
419
|
-
"/blank/page",
|
|
420
|
-
"stats.g.doubleclick.net",
|
|
421
|
-
"google-analytics.com",
|
|
422
|
-
...(process.env.RECON_TELEMETRY_URL_PATTERNS ?? "")
|
|
423
|
-
.split(",")
|
|
424
|
-
.map((p) => p.trim())
|
|
425
|
-
.filter(Boolean),
|
|
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;
|
|
442
408
|
/**
|
|
443
409
|
* Extracts the ordered sequence of meaningful POSTs that represent the
|
|
444
410
|
* transactional flow: same-host 2xx POSTs, minus telemetry and error-reporting
|
|
@@ -472,15 +438,8 @@ function extractActionSequence(captures, baseUrl) {
|
|
|
472
438
|
}
|
|
473
439
|
if (captureHost !== host)
|
|
474
440
|
return false;
|
|
475
|
-
if (
|
|
476
|
-
return false;
|
|
477
|
-
try {
|
|
478
|
-
if (ERROR_SINK_PATH_SEGMENT.test(new URL(capture.url).pathname))
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
481
|
-
catch {
|
|
441
|
+
if ((0, capture_filters_1.isNoiseUrl)(capture.url))
|
|
482
442
|
return false;
|
|
483
|
-
}
|
|
484
443
|
return true;
|
|
485
444
|
});
|
|
486
445
|
}
|
|
@@ -571,7 +530,7 @@ const KNOWN_TOP_LEVEL_ERROR_KEYS = new Set(["message", "error", "errormessage"])
|
|
|
571
530
|
/**
|
|
572
531
|
* Suffixes that mark a JSON key as carrying validation/data errors when its
|
|
573
532
|
* value is non-null. Case-sensitive because real APIs use mixed-case in the
|
|
574
|
-
* exact form they ship (e.g.
|
|
533
|
+
* exact form they ship (e.g. a `ResponseValidationErrors` key).
|
|
575
534
|
*/
|
|
576
535
|
const NESTED_ERROR_KEY_SUFFIXES = ["ValidationErrors", "DataErrors", "ValidationError"];
|
|
577
536
|
/**
|
|
@@ -581,7 +540,7 @@ const NESTED_ERROR_KEY_SUFFIXES = ["ValidationErrors", "DataErrors", "Validation
|
|
|
581
540
|
* responses (so legitimate success-only fields like `Name` aren't false-
|
|
582
541
|
* flagged as errors).
|
|
583
542
|
*
|
|
584
|
-
* Site-agnostic:
|
|
543
|
+
* Site-agnostic: one ATS may use `Message`/`Sections.ResponseValidationErrors`
|
|
585
544
|
* /`Sections.DataValidationErrors`; a different ATS using `error`/`errors[]`
|
|
586
545
|
* would emit guards for those instead.
|
|
587
546
|
*/
|
|
@@ -642,7 +601,7 @@ function walkForNestedErrorKeys(value, path, candidates) {
|
|
|
642
601
|
*/
|
|
643
602
|
const UUID_REGEX = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
644
603
|
/**
|
|
645
|
-
* Converts a
|
|
604
|
+
* Converts a machine code like "contact.first.name" or "address.country.subdivision"
|
|
646
605
|
* to PascalCase: "ContactFirstName", "AddressCountrySubdivision". Site-agnostic:
|
|
647
606
|
* operates only on the input string. Returns null for inputs that don't
|
|
648
607
|
* produce a valid JS identifier.
|
|
@@ -655,7 +614,7 @@ function sourceCodeToPascalCase(sourceCode) {
|
|
|
655
614
|
return isValidJsIdentifier(pascal) ? pascal : null;
|
|
656
615
|
}
|
|
657
616
|
/**
|
|
658
|
-
* Converts a free-form
|
|
617
|
+
* Converts a free-form field label like "Reference #1 First Name" or "Email" to
|
|
659
618
|
* PascalCase, stripping punctuation in a way that preserves position (so
|
|
660
619
|
* "Reference #1 First Name" → "Reference1FirstName" via a section-heading
|
|
661
620
|
* prefix). Site-agnostic: operates only on input strings.
|
|
@@ -691,60 +650,70 @@ function fieldNameToPascalCase(fieldName, prefix) {
|
|
|
691
650
|
}
|
|
692
651
|
/**
|
|
693
652
|
* Recon-driven detection of form-schema captures. Scans all response bodies
|
|
694
|
-
* for arrays whose objects look like
|
|
695
|
-
*
|
|
653
|
+
* for arrays whose objects look like form fields (a UUID-shaped field-id key
|
|
654
|
+
* plus at least one of the schema's name keys). Builds field-id → PascalCase
|
|
655
|
+
* name map.
|
|
656
|
+
*
|
|
657
|
+
* Site-agnostic: identifies form-schema captures by structural fingerprint
|
|
658
|
+
* against the consumer-supplied keys, not by URL or site name. Any ATS exposing
|
|
659
|
+
* a matching schema would match.
|
|
696
660
|
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
661
|
+
* Exported for unit testing — lets tests prove the rewiring: the same field is
|
|
662
|
+
* recovered whether the supplied keys are one vendor's names or a differing
|
|
663
|
+
* vendor's, and that a null schema recovers nothing.
|
|
699
664
|
*/
|
|
700
|
-
function detectFormSchemaFieldNames(captures) {
|
|
665
|
+
function detectFormSchemaFieldNames(captures, formSchema) {
|
|
701
666
|
const fieldNameMap = new Map();
|
|
702
667
|
const fieldOptionsMap = new Map();
|
|
703
668
|
const allSchemaUuids = new Set();
|
|
669
|
+
// No form-schema declared → recover nothing. The engine carries no vendor's
|
|
670
|
+
// wire keys, so there is nothing to fingerprint ATS responses against.
|
|
671
|
+
if (formSchema === null)
|
|
672
|
+
return { fieldNameMap, fieldOptionsMap, allSchemaUuids };
|
|
704
673
|
for (const capture of captures) {
|
|
705
|
-
walkForSectionFieldsArrays(capture.responseBody, fieldNameMap, fieldOptionsMap);
|
|
706
|
-
walkForSchemaUuids(capture.responseBody, allSchemaUuids);
|
|
674
|
+
walkForSectionFieldsArrays(capture.responseBody, fieldNameMap, fieldOptionsMap, formSchema);
|
|
675
|
+
walkForSchemaUuids(capture.responseBody, allSchemaUuids, formSchema);
|
|
707
676
|
}
|
|
708
677
|
return { fieldNameMap, fieldOptionsMap, allSchemaUuids };
|
|
709
678
|
}
|
|
710
679
|
/**
|
|
711
|
-
* Walks a response body collecting UUID-shaped strings under
|
|
712
|
-
* under the
|
|
713
|
-
* schema anchors that must be shielded
|
|
714
|
-
*
|
|
715
|
-
* when the field
|
|
680
|
+
* Walks a response body collecting UUID-shaped strings under the schema's
|
|
681
|
+
* field-id key, or under the option-id key of an entry in a sibling
|
|
682
|
+
* field-options array. These are stable schema anchors that must be shielded
|
|
683
|
+
* from state-threading even when detectFormSchemaFieldNames emits no
|
|
684
|
+
* payload-mappable name for the field (e.g. when the field name is too long for
|
|
685
|
+
* our naming heuristic).
|
|
716
686
|
*
|
|
717
|
-
* The
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
* here would re-broaden the very fingerprint that issue exists to narrow.
|
|
687
|
+
* The wire keys come from the consumer-supplied {@link ReconFormSchema}, so a
|
|
688
|
+
* vendor declares its own keys with `--form-schema` rather than the engine
|
|
689
|
+
* hardcoding any — the inversion issue #57 asked for.
|
|
721
690
|
*/
|
|
722
|
-
function walkForSchemaUuids(value, out) {
|
|
691
|
+
function walkForSchemaUuids(value, out, formSchema) {
|
|
723
692
|
if (value === null || typeof value !== "object")
|
|
724
693
|
return;
|
|
725
694
|
if (Array.isArray(value)) {
|
|
726
695
|
for (const item of value)
|
|
727
|
-
walkForSchemaUuids(item, out);
|
|
696
|
+
walkForSchemaUuids(item, out, formSchema);
|
|
728
697
|
return;
|
|
729
698
|
}
|
|
730
699
|
const obj = value;
|
|
731
|
-
const fieldIdRaw = obj.
|
|
700
|
+
const fieldIdRaw = obj[formSchema.fieldIdKey];
|
|
732
701
|
if (typeof fieldIdRaw === "string" && UUID_REGEX.test(fieldIdRaw)) {
|
|
733
702
|
out.add(fieldIdRaw);
|
|
734
703
|
}
|
|
735
|
-
const optionsRaw = obj.
|
|
704
|
+
const optionsRaw = obj[formSchema.fieldOptionsKey];
|
|
736
705
|
if (Array.isArray(optionsRaw)) {
|
|
737
706
|
for (const opt of optionsRaw) {
|
|
738
707
|
if (opt !== null && typeof opt === "object") {
|
|
739
|
-
const optId = opt.
|
|
708
|
+
const optId = opt[formSchema.optionIdKey];
|
|
740
709
|
if (typeof optId === "string" && UUID_REGEX.test(optId))
|
|
741
710
|
out.add(optId);
|
|
742
711
|
}
|
|
743
712
|
}
|
|
744
713
|
}
|
|
745
|
-
// Recurse into nested objects/arrays so nested
|
|
714
|
+
// Recurse into nested objects/arrays so nested form fields get walked too.
|
|
746
715
|
for (const v of Object.values(obj))
|
|
747
|
-
walkForSchemaUuids(v, out);
|
|
716
|
+
walkForSchemaUuids(v, out, formSchema);
|
|
748
717
|
}
|
|
749
718
|
/**
|
|
750
719
|
* Closed enum of well-known cache-buster query parameter names. Stripped
|
|
@@ -767,14 +736,14 @@ function stripCacheBusterParams(url) {
|
|
|
767
736
|
}
|
|
768
737
|
/**
|
|
769
738
|
* Recon-driven detection of the form-schema fetch capture. Returns the first
|
|
770
|
-
* GET capture (in recon order) whose response body contains a
|
|
771
|
-
* shaped array. Sites without such a capture get
|
|
772
|
-
* become no-ops.
|
|
739
|
+
* GET capture (in recon order) whose response body contains a form-fields-
|
|
740
|
+
* shaped array (per the supplied schema keys). Sites without such a capture get
|
|
741
|
+
* `null` and Phase B/C/D become no-ops.
|
|
773
742
|
*
|
|
774
743
|
* Site-agnostic: identifies the fetch by structural fingerprint of the
|
|
775
744
|
* response body, not by URL or site name.
|
|
776
745
|
*/
|
|
777
|
-
function detectFormSchemaFetchCapture(captures, baseUrl) {
|
|
746
|
+
function detectFormSchemaFetchCapture(captures, baseUrl, formSchema) {
|
|
778
747
|
let host;
|
|
779
748
|
try {
|
|
780
749
|
host = new URL(baseUrl).host;
|
|
@@ -797,52 +766,52 @@ function detectFormSchemaFetchCapture(captures, baseUrl) {
|
|
|
797
766
|
}
|
|
798
767
|
if (captureHost !== host)
|
|
799
768
|
continue;
|
|
800
|
-
if (
|
|
769
|
+
if ((0, capture_filters_1.telemetryUrlPatterns)().some((p) => capture.url.includes(p)))
|
|
801
770
|
continue;
|
|
802
|
-
if (responseContainsSectionFields(capture.responseBody)) {
|
|
771
|
+
if (responseContainsSectionFields(capture.responseBody, formSchema)) {
|
|
803
772
|
return { capture, index: i };
|
|
804
773
|
}
|
|
805
774
|
}
|
|
806
775
|
return null;
|
|
807
776
|
}
|
|
808
|
-
function responseContainsSectionFields(value) {
|
|
777
|
+
function responseContainsSectionFields(value, formSchema) {
|
|
809
778
|
if (value === null || typeof value !== "object")
|
|
810
779
|
return false;
|
|
811
780
|
if (Array.isArray(value)) {
|
|
812
|
-
if (looksLikeSectionFieldsArray(value))
|
|
781
|
+
if (looksLikeSectionFieldsArray(value, formSchema))
|
|
813
782
|
return true;
|
|
814
783
|
for (const item of value) {
|
|
815
|
-
if (responseContainsSectionFields(item))
|
|
784
|
+
if (responseContainsSectionFields(item, formSchema))
|
|
816
785
|
return true;
|
|
817
786
|
}
|
|
818
787
|
return false;
|
|
819
788
|
}
|
|
820
789
|
for (const v of Object.values(value)) {
|
|
821
|
-
if (responseContainsSectionFields(v))
|
|
790
|
+
if (responseContainsSectionFields(v, formSchema))
|
|
822
791
|
return true;
|
|
823
792
|
}
|
|
824
793
|
return false;
|
|
825
794
|
}
|
|
826
|
-
function walkForSectionFieldsArrays(value, fieldNameMap, fieldOptionsMap) {
|
|
795
|
+
function walkForSectionFieldsArrays(value, fieldNameMap, fieldOptionsMap, formSchema) {
|
|
827
796
|
if (value === null || typeof value !== "object")
|
|
828
797
|
return;
|
|
829
798
|
if (Array.isArray(value)) {
|
|
830
|
-
if (looksLikeSectionFieldsArray(value)) {
|
|
831
|
-
assignFieldNamesFromArray(value, fieldNameMap, fieldOptionsMap);
|
|
799
|
+
if (looksLikeSectionFieldsArray(value, formSchema)) {
|
|
800
|
+
assignFieldNamesFromArray(value, fieldNameMap, fieldOptionsMap, formSchema);
|
|
832
801
|
}
|
|
833
802
|
for (const item of value)
|
|
834
|
-
walkForSectionFieldsArrays(item, fieldNameMap, fieldOptionsMap);
|
|
803
|
+
walkForSectionFieldsArrays(item, fieldNameMap, fieldOptionsMap, formSchema);
|
|
835
804
|
return;
|
|
836
805
|
}
|
|
837
806
|
for (const v of Object.values(value)) {
|
|
838
|
-
walkForSectionFieldsArrays(v, fieldNameMap, fieldOptionsMap);
|
|
807
|
+
walkForSectionFieldsArrays(v, fieldNameMap, fieldOptionsMap, formSchema);
|
|
839
808
|
}
|
|
840
809
|
}
|
|
841
810
|
/**
|
|
842
811
|
* Structural fingerprint: array of objects, at least half of which have a
|
|
843
|
-
* UUID-shaped
|
|
812
|
+
* UUID-shaped field-id AND at least one of the schema's field-name keys.
|
|
844
813
|
*/
|
|
845
|
-
function looksLikeSectionFieldsArray(arr) {
|
|
814
|
+
function looksLikeSectionFieldsArray(arr, formSchema) {
|
|
846
815
|
if (arr.length === 0)
|
|
847
816
|
return false;
|
|
848
817
|
let matches = 0;
|
|
@@ -850,26 +819,31 @@ function looksLikeSectionFieldsArray(arr) {
|
|
|
850
819
|
if (item === null || typeof item !== "object")
|
|
851
820
|
continue;
|
|
852
821
|
const obj = item;
|
|
853
|
-
const fieldIdRaw = obj.
|
|
822
|
+
const fieldIdRaw = obj[formSchema.fieldIdKey];
|
|
854
823
|
if (typeof fieldIdRaw !== "string")
|
|
855
824
|
continue;
|
|
856
825
|
if (!UUID_REGEX.test(fieldIdRaw))
|
|
857
826
|
continue;
|
|
858
|
-
if (
|
|
827
|
+
if (formSchema.fieldNameKeys.some((key) => typeof obj[key] === "string")) {
|
|
859
828
|
matches++;
|
|
860
829
|
}
|
|
861
830
|
}
|
|
862
831
|
return matches >= Math.max(1, Math.floor(arr.length * 0.5));
|
|
863
832
|
}
|
|
864
|
-
function assignFieldNamesFromArray(arr, fieldNameMap, fieldOptionsMap) {
|
|
833
|
+
function assignFieldNamesFromArray(arr, fieldNameMap, fieldOptionsMap, formSchema) {
|
|
865
834
|
let currentPrefix = null;
|
|
866
835
|
const usedNames = new Set([...fieldNameMap.values()]);
|
|
836
|
+
// First field-name key is the machine code (preferred, PascalCased directly);
|
|
837
|
+
// any later key is a human label (subject to the section-heading heuristic).
|
|
838
|
+
// With one key both branches collapse to the label path.
|
|
839
|
+
const [codeKey, ...labelKeys] = formSchema.fieldNameKeys;
|
|
840
|
+
const labelKey = labelKeys[0];
|
|
867
841
|
for (const obj of arr) {
|
|
868
|
-
const fieldId = obj.
|
|
842
|
+
const fieldId = obj[formSchema.fieldIdKey];
|
|
869
843
|
if (typeof fieldId !== "string")
|
|
870
844
|
continue;
|
|
871
|
-
const sourceCode = obj
|
|
872
|
-
const name = obj
|
|
845
|
+
const sourceCode = codeKey !== undefined && labelKey !== undefined ? obj[codeKey] : undefined;
|
|
846
|
+
const name = labelKey !== undefined ? obj[labelKey] : obj[codeKey ?? ""];
|
|
873
847
|
let semantic = null;
|
|
874
848
|
if (typeof sourceCode === "string" && sourceCode.trim().length > 0) {
|
|
875
849
|
semantic = sourceCodeToPascalCase(sourceCode);
|
|
@@ -877,7 +851,7 @@ function assignFieldNamesFromArray(arr, fieldNameMap, fieldOptionsMap) {
|
|
|
877
851
|
}
|
|
878
852
|
else if (typeof name === "string" && name.trim().length > 0 && name.length < 250) {
|
|
879
853
|
const hasNoSourceCode = typeof sourceCode !== "string" || sourceCode.trim().length === 0;
|
|
880
|
-
// Section-heading heuristic: short
|
|
854
|
+
// Section-heading heuristic: short label, no machine code, MOSTLY
|
|
881
855
|
// uppercase letters (>= 70% of alphabetic chars) OR contains '#'.
|
|
882
856
|
// Whole-name uppercase ratio avoids false positives like "MM/DD/YYYY"
|
|
883
857
|
// appearing as a format hint inside a normal field label.
|
|
@@ -903,11 +877,11 @@ function assignFieldNamesFromArray(arr, fieldNameMap, fieldOptionsMap) {
|
|
|
903
877
|
}
|
|
904
878
|
fieldNameMap.set(fieldId, unique);
|
|
905
879
|
usedNames.add(unique);
|
|
906
|
-
// Capture
|
|
907
|
-
// semantic
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
const optionsRaw = obj.
|
|
880
|
+
// Capture the field's options when present and ALL options have non-empty
|
|
881
|
+
// semantic labels. Options with an empty label are skipped — with no
|
|
882
|
+
// semantic value we can't generate a meaningful enum, so we leave the
|
|
883
|
+
// field's option-id baked.
|
|
884
|
+
const optionsRaw = obj[formSchema.fieldOptionsKey];
|
|
911
885
|
if (Array.isArray(optionsRaw) && optionsRaw.length > 0) {
|
|
912
886
|
const options = [];
|
|
913
887
|
let allSemantic = true;
|
|
@@ -917,8 +891,8 @@ function assignFieldNamesFromArray(arr, fieldNameMap, fieldOptionsMap) {
|
|
|
917
891
|
break;
|
|
918
892
|
}
|
|
919
893
|
const opt = optRaw;
|
|
920
|
-
const optId = opt.
|
|
921
|
-
const optValue = opt.
|
|
894
|
+
const optId = opt[formSchema.optionIdKey];
|
|
895
|
+
const optValue = opt[formSchema.optionValueKey];
|
|
922
896
|
if (typeof optId !== "string" ||
|
|
923
897
|
typeof optValue !== "string" ||
|
|
924
898
|
optValue.trim().length === 0) {
|
|
@@ -935,20 +909,21 @@ function assignFieldNamesFromArray(arr, fieldNameMap, fieldOptionsMap) {
|
|
|
935
909
|
}
|
|
936
910
|
}
|
|
937
911
|
/**
|
|
938
|
-
* Substitutes
|
|
912
|
+
* Substitutes the submitted-value literals with payload accessors based on the
|
|
939
913
|
* field-name map from the form schema. Operates on the body string before
|
|
940
914
|
* state interpolation so already-substituted state values (e.g. ${firstName})
|
|
941
915
|
* are preserved.
|
|
942
916
|
*
|
|
943
|
-
* Closed-set substring matching: both
|
|
944
|
-
* the generator's own input (recon).
|
|
917
|
+
* Closed-set substring matching: both the field-id and the submitted value come
|
|
918
|
+
* from the generator's own input (recon).
|
|
945
919
|
*/
|
|
946
|
-
function applyFormSchemaSubstitutions(rawBody, fieldNameMap, outDiscoveredFields) {
|
|
920
|
+
function applyFormSchemaSubstitutions(rawBody, fieldNameMap, outDiscoveredFields, formSchema) {
|
|
947
921
|
if (fieldNameMap.size === 0)
|
|
948
922
|
return rawBody;
|
|
949
923
|
let result = rawBody;
|
|
924
|
+
const valueMarker = `"${formSchema.responseValueKey}":"`;
|
|
950
925
|
for (const [fieldId, semanticName] of fieldNameMap) {
|
|
951
|
-
const fieldIdMarker = `"
|
|
926
|
+
const fieldIdMarker = `"${formSchema.fieldIdKey}":"${fieldId}"`;
|
|
952
927
|
let cursor = 0;
|
|
953
928
|
while (true) {
|
|
954
929
|
const idx = result.indexOf(fieldIdMarker, cursor);
|
|
@@ -958,7 +933,6 @@ function applyFormSchemaSubstitutions(rawBody, fieldNameMap, outDiscoveredFields
|
|
|
958
933
|
if (objEnd === -1)
|
|
959
934
|
break;
|
|
960
935
|
const segment = result.slice(idx, objEnd);
|
|
961
|
-
const valueMarker = `"Value":"`;
|
|
962
936
|
const valueIdx = segment.indexOf(valueMarker);
|
|
963
937
|
if (valueIdx === -1) {
|
|
964
938
|
cursor = objEnd;
|
|
@@ -984,22 +958,23 @@ function applyFormSchemaSubstitutions(rawBody, fieldNameMap, outDiscoveredFields
|
|
|
984
958
|
return result;
|
|
985
959
|
}
|
|
986
960
|
/**
|
|
987
|
-
* Substitutes
|
|
988
|
-
* Operates on the body string before state interpolation. For each
|
|
989
|
-
*
|
|
990
|
-
* the matching
|
|
961
|
+
* Substitutes submitted option-id literals with payload-driven enum lookups.
|
|
962
|
+
* Operates on the body string before state interpolation. For each field with a
|
|
963
|
+
* captured FieldOptionsMapping, find the schema's field-id marker and rewrite
|
|
964
|
+
* the matching option-id value to `${OPT_<Name>[payload.<Name>]}`.
|
|
991
965
|
*
|
|
992
|
-
* Order-insensitive: matches
|
|
993
|
-
* JSON object as the
|
|
994
|
-
*
|
|
995
|
-
*
|
|
966
|
+
* Order-insensitive: matches the option-id marker anywhere within the same
|
|
967
|
+
* JSON object as the field-id marker (which is between it and the closing `}`).
|
|
968
|
+
* Closed-set substring matching: both marker values come from the generator's
|
|
969
|
+
* own input.
|
|
996
970
|
*/
|
|
997
|
-
function applyFormSchemaOptionIdSubstitutions(rawBody, fieldOptionsMap, outDiscoveredOptionFields) {
|
|
971
|
+
function applyFormSchemaOptionIdSubstitutions(rawBody, fieldOptionsMap, outDiscoveredOptionFields, formSchema) {
|
|
998
972
|
if (fieldOptionsMap.size === 0)
|
|
999
973
|
return rawBody;
|
|
1000
974
|
let result = rawBody;
|
|
975
|
+
const optionIdMarker = `"${formSchema.responseOptionIdKey}":"`;
|
|
1001
976
|
for (const [fieldId, mapping] of fieldOptionsMap) {
|
|
1002
|
-
const fieldIdMarker = `"
|
|
977
|
+
const fieldIdMarker = `"${formSchema.fieldIdKey}":"${fieldId}"`;
|
|
1003
978
|
let cursor = 0;
|
|
1004
979
|
while (true) {
|
|
1005
980
|
const idx = result.indexOf(fieldIdMarker, cursor);
|
|
@@ -1009,7 +984,6 @@ function applyFormSchemaOptionIdSubstitutions(rawBody, fieldOptionsMap, outDisco
|
|
|
1009
984
|
if (objEnd === -1)
|
|
1010
985
|
break;
|
|
1011
986
|
const segment = result.slice(idx, objEnd);
|
|
1012
|
-
const optionIdMarker = `"OptionId":"`;
|
|
1013
987
|
const optionIdLocal = segment.indexOf(optionIdMarker);
|
|
1014
988
|
if (optionIdLocal === -1) {
|
|
1015
989
|
cursor = objEnd;
|
|
@@ -1035,25 +1009,26 @@ function applyFormSchemaOptionIdSubstitutions(rawBody, fieldOptionsMap, outDisco
|
|
|
1035
1009
|
return result;
|
|
1036
1010
|
}
|
|
1037
1011
|
/**
|
|
1038
|
-
* For fields whose
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1012
|
+
* For fields whose options have NO semantic labels (the schema's option-label
|
|
1013
|
+
* value is empty), T3's OPT_* enum mapping can't be emitted. Instead,
|
|
1014
|
+
* parameterize the submitted option-id slot as a caller-supplied
|
|
1015
|
+
* `<Name>OptionId` payload field with the recon-observed UUID documented in a
|
|
1016
|
+
* TSDoc comment.
|
|
1043
1017
|
*
|
|
1044
|
-
* Operates on the same
|
|
1045
|
-
* applyFormSchemaOptionIdSubstitutions, but only fires when the
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1018
|
+
* Operates on the same field-id-anchored search as
|
|
1019
|
+
* applyFormSchemaOptionIdSubstitutions, but only fires when the field is in
|
|
1020
|
+
* fieldNameMap (has a semantic name) AND NOT in fieldOptionsMap (the structured
|
|
1021
|
+
* enum substitution didn't fire). Site-agnostic.
|
|
1048
1022
|
*/
|
|
1049
|
-
function applyRawOptionIdPayloadSubstitutions(rawBody, fieldNameMap, fieldOptionsMap, outDiscoveredRawOptionFields) {
|
|
1023
|
+
function applyRawOptionIdPayloadSubstitutions(rawBody, fieldNameMap, fieldOptionsMap, outDiscoveredRawOptionFields, formSchema) {
|
|
1050
1024
|
if (fieldNameMap.size === 0)
|
|
1051
1025
|
return rawBody;
|
|
1052
1026
|
let result = rawBody;
|
|
1027
|
+
const optionIdMarker = `"${formSchema.responseOptionIdKey}":"`;
|
|
1053
1028
|
for (const [fieldId, fieldName] of fieldNameMap) {
|
|
1054
1029
|
if (fieldOptionsMap.has(fieldId))
|
|
1055
1030
|
continue; // T3's OPT_* already handles this.
|
|
1056
|
-
const fieldIdMarker = `"
|
|
1031
|
+
const fieldIdMarker = `"${formSchema.fieldIdKey}":"${fieldId}"`;
|
|
1057
1032
|
let cursor = 0;
|
|
1058
1033
|
while (true) {
|
|
1059
1034
|
const idx = result.indexOf(fieldIdMarker, cursor);
|
|
@@ -1063,7 +1038,6 @@ function applyRawOptionIdPayloadSubstitutions(rawBody, fieldNameMap, fieldOption
|
|
|
1063
1038
|
if (objEnd === -1)
|
|
1064
1039
|
break;
|
|
1065
1040
|
const segment = result.slice(idx, objEnd);
|
|
1066
|
-
const optionIdMarker = `"OptionId":"`;
|
|
1067
1041
|
const optionIdLocal = segment.indexOf(optionIdMarker);
|
|
1068
1042
|
if (optionIdLocal === -1) {
|
|
1069
1043
|
cursor = objEnd;
|
|
@@ -1097,13 +1071,14 @@ function applyRawOptionIdPayloadSubstitutions(rawBody, fieldNameMap, fieldOption
|
|
|
1097
1071
|
* embedded base64 images, etc.) that aren't candidates for state threading. */
|
|
1098
1072
|
const MAX_STATE_VALUE_LENGTH = 256;
|
|
1099
1073
|
/** Canonical "uninitialized" sentinel values that some REST APIs return as
|
|
1100
|
-
* placeholders before a downstream call populates the real identifier.
|
|
1101
|
-
*
|
|
1102
|
-
* ApplyProcessId, then `/user/start
|
|
1103
|
-
* placeholder would lock the generated plugin's
|
|
1104
|
-
* the all-zero UUID — every downstream call would
|
|
1105
|
-
* does not exist". Closed set, literal-string match —
|
|
1106
|
-
* pattern-based detection (would trip the no-regex-on-open-sets
|
|
1074
|
+
* placeholders before a downstream call populates the real identifier. An ATS
|
|
1075
|
+
* whose `/user/create` returns these for CandidateId/ApplicationId/
|
|
1076
|
+
* ApplyProcessId, then yields the real values on `/user/start`, is the case that
|
|
1077
|
+
* motivated this: indexing the placeholder would lock the generated plugin's
|
|
1078
|
+
* `${candidateId}` binding to the all-zero UUID — every downstream call would
|
|
1079
|
+
* then 404 with "candidate does not exist". Closed set, literal-string match —
|
|
1080
|
+
* never expand to pattern-based detection (would trip the no-regex-on-open-sets
|
|
1081
|
+
* rule). */
|
|
1107
1082
|
const PLACEHOLDER_STATE_VALUES = new Set(["00000000-0000-0000-0000-000000000000"]);
|
|
1108
1083
|
/**
|
|
1109
1084
|
* Splits a raw `Set-Cookie` response-header string into `name`/`value` pairs.
|
|
@@ -1199,8 +1174,9 @@ function indexStateValues(captures, shieldedUuids = new Set(), actionCaptureIndi
|
|
|
1199
1174
|
continue;
|
|
1200
1175
|
if (PLACEHOLDER_STATE_VALUES.has(value))
|
|
1201
1176
|
continue;
|
|
1202
|
-
// Schema-identifier UUIDs (
|
|
1203
|
-
// T2/T3 substitution depends on remaining literal in body
|
|
1177
|
+
// Schema-identifier UUIDs (the field-id and option-id anchors) are stable
|
|
1178
|
+
// anchors that T2/T3 substitution depends on remaining literal in body
|
|
1179
|
+
// templates.
|
|
1204
1180
|
// Indexing them would let state-threading rewrite the anchors and
|
|
1205
1181
|
// corrupt T2/T3's already-substituted Values.
|
|
1206
1182
|
if (shieldedUuids.has(value))
|
|
@@ -1528,7 +1504,7 @@ function applyPayloadKeyValueSubstitutions(template, inputBody, additionalBodies
|
|
|
1528
1504
|
// ── base64 Content parameterization ──────────────────────────────────────────
|
|
1529
1505
|
/**
|
|
1530
1506
|
* Maps a site's screening-question prompts to the payload field that answers
|
|
1531
|
-
* them, as `{
|
|
1507
|
+
* them, as `{ payloadField: [keyword, …] }`.
|
|
1532
1508
|
*
|
|
1533
1509
|
* Empty by default and supplied by the operator via `RECON_QUESTION_KEYWORDS`
|
|
1534
1510
|
* (JSON) — the engine cannot know what any site asks or what a caller's payload
|
|
@@ -1737,14 +1713,12 @@ function emitErrorSignalGuards(varName, urlPath, signals) {
|
|
|
1737
1713
|
parentSegments.reverse();
|
|
1738
1714
|
out.push(` const ${parentVar} = (${varName} as { ${parentObjType} }).${parentAccessor};`);
|
|
1739
1715
|
for (const { errorKey } of errorKeys) {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
.trim()
|
|
1747
|
-
.toLowerCase();
|
|
1716
|
+
// Humanize the wire key generically (CamelCase → "camel case"); no
|
|
1717
|
+
// per-vendor special cases — the engine carries no vendor's key vocabulary.
|
|
1718
|
+
const label = errorKey
|
|
1719
|
+
.replace(/([A-Z])/g, " $1")
|
|
1720
|
+
.trim()
|
|
1721
|
+
.toLowerCase();
|
|
1748
1722
|
out.push(` if (${parentVar} != null && ${parentVar}.${errorKey} != null) throw new Error(\`step ${varName} ${label}: \${JSON.stringify(${parentVar}.${errorKey})}\`);`);
|
|
1749
1723
|
}
|
|
1750
1724
|
}
|
|
@@ -1752,7 +1726,7 @@ function emitErrorSignalGuards(varName, urlPath, signals) {
|
|
|
1752
1726
|
}
|
|
1753
1727
|
/** Exported for unit testing — lets tests drive the multipart-upload code path directly
|
|
1754
1728
|
* without going through the full emitContractTs pipeline. */
|
|
1755
|
-
function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap, outDiscoveredFields, fieldOptionsMap, outDiscoveredOptionFields, outDiscoveredRawOptionFields, outDiscoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride = new Map()) {
|
|
1729
|
+
function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap, outDiscoveredFields, fieldOptionsMap, outDiscoveredOptionFields, outDiscoveredRawOptionFields, outDiscoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride = new Map(), formSchema = null) {
|
|
1756
1730
|
// Walk the first action's request body to map each leaf string value to its
|
|
1757
1731
|
// `payload.<accessor>` expression. The emit's second interpolation pass uses
|
|
1758
1732
|
// this to substitute literal occurrences (e.g. "Reginald") with their
|
|
@@ -1791,7 +1765,7 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
1791
1765
|
outDiscoveredFields.add("BaseUrl");
|
|
1792
1766
|
}
|
|
1793
1767
|
// G2: register any tenant-subdomain header values as payload-supplied fields
|
|
1794
|
-
// (e.g.
|
|
1768
|
+
// (e.g. an `API-ShortName: "addus"` header becomes `payload.ApiShortName`).
|
|
1795
1769
|
for (const [headerName, _value] of tenantSubdomainHeaders) {
|
|
1796
1770
|
outDiscoveredFields.add(headerNameToPayloadFieldName(headerName));
|
|
1797
1771
|
}
|
|
@@ -1819,13 +1793,16 @@ function emitMultiStepExecuteHttp(actions, inputBody, errorSignals, fieldNameMap
|
|
|
1819
1793
|
const prior = actions.slice(0, i);
|
|
1820
1794
|
const url = interpolateStateValues(cap.url, prior, payloadAccessorByValue);
|
|
1821
1795
|
// Form-schema substitution runs first on the raw recon body so its
|
|
1822
|
-
//
|
|
1823
|
-
// payload key-value passes then run on top.
|
|
1824
|
-
// here too: same closed-set
|
|
1825
|
-
// slots to "${OPT_X[payload.X]}" lookups.
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1796
|
+
// field-id-anchored matches see the original JSON. State-threading and
|
|
1797
|
+
// payload key-value passes then run on top. Option-id substitution runs
|
|
1798
|
+
// here too: same closed-set field-id anchor; rewrites the submitted
|
|
1799
|
+
// option-id slots to "${OPT_X[payload.X]}" lookups.
|
|
1800
|
+
// Form-schema passes only fire when a `--form-schema` was supplied (which is
|
|
1801
|
+
// also the only way the field maps are non-empty); without one they are
|
|
1802
|
+
// no-ops and the raw recon body flows straight through.
|
|
1803
|
+
const rawBodyWithFormSubs = cap.requestPostData && formSchema !== null
|
|
1804
|
+
? applyRawOptionIdPayloadSubstitutions(applyFormSchemaOptionIdSubstitutions(applyFormSchemaSubstitutions(cap.requestPostData, fieldNameMap, outDiscoveredFields, formSchema), fieldOptionsMap, outDiscoveredOptionFields, formSchema), fieldNameMap, fieldOptionsMap, outDiscoveredRawOptionFields, formSchema)
|
|
1805
|
+
: (cap.requestPostData ?? "");
|
|
1829
1806
|
let bodyTemplate = rawBodyWithFormSubs
|
|
1830
1807
|
? applyPayloadKeyValueSubstitutions(interpolateStateValues(rawBodyWithFormSubs, prior, payloadAccessorByValue), inputBody, additionalBodies, outDiscoveredAdditionalBodyKeys)
|
|
1831
1808
|
: "";
|
|
@@ -2104,7 +2081,7 @@ function emitContractTs(opts) {
|
|
|
2104
2081
|
.join("\n")}\n})`
|
|
2105
2082
|
: "";
|
|
2106
2083
|
// Build per-field OPT_<Name> constant declarations + payload-schema enum
|
|
2107
|
-
// entries from the form schema's
|
|
2084
|
+
// entries from the form schema's options. Only fields whose option-id
|
|
2108
2085
|
// slots were actually rewritten in the body (i.e. that appear in
|
|
2109
2086
|
// discoveredOptionFields) get emitted; the rest leave their schema entries
|
|
2110
2087
|
// unused. Computed BEFORE payloadSchemaExpr so the extension string is
|
|
@@ -2131,16 +2108,16 @@ function emitContractTs(opts) {
|
|
|
2131
2108
|
.map((m) => ` ${m.semanticName}: z.enum([${m.options.map((o) => JSON.stringify(o.value)).join(", ")}]),`)
|
|
2132
2109
|
.join("\n")}\n})`
|
|
2133
2110
|
: "";
|
|
2134
|
-
// Phase E raw-option payload fields:
|
|
2135
|
-
//
|
|
2136
|
-
//
|
|
2137
|
-
//
|
|
2111
|
+
// Phase E raw-option payload fields: options whose label strings are empty in
|
|
2112
|
+
// the schema — no semantic enum is possible, so the caller supplies the
|
|
2113
|
+
// option-id UUID directly. The recon-observed UUID is documented in a TSDoc
|
|
2114
|
+
// comment so callers have a starting point.
|
|
2138
2115
|
const sortedRawOptionEntries = discoveredRawOptionFields
|
|
2139
2116
|
? [...discoveredRawOptionFields.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
2140
2117
|
: [];
|
|
2141
2118
|
const rawOptionSchemaExtension = sortedRawOptionEntries.length > 0
|
|
2142
2119
|
? `.extend({\n${sortedRawOptionEntries
|
|
2143
|
-
.map(([name, reconUuid]) => ` /** Recon-observed: ${reconUuid}. Caller supplies the
|
|
2120
|
+
.map(([name, reconUuid]) => ` /** Recon-observed: ${reconUuid}. Caller supplies the option-id UUID for this field. */\n ${name}: z.string(),`)
|
|
2144
2121
|
.join("\n")}\n})`
|
|
2145
2122
|
: "";
|
|
2146
2123
|
// Phase F: additional-body keys (from action POSTs beyond r0). Each gets a
|
|
@@ -2415,12 +2392,16 @@ function jsonSchemaTypeOf(value) {
|
|
|
2415
2392
|
* `recovered` carries the request contract the `.ts` path infers from real
|
|
2416
2393
|
* captures — the first POST body's fields plus form-schema discoveries — so
|
|
2417
2394
|
* `--emit config` no longer throws that away and emit a request schema built
|
|
2418
|
-
* only from the handful of flow-step splice hints.
|
|
2419
|
-
*
|
|
2420
|
-
* `
|
|
2395
|
+
* only from the handful of flow-step splice hints.
|
|
2396
|
+
*
|
|
2397
|
+
* When the site has a direct-HTTP path (a submission flow whose `.ts` emit would
|
|
2398
|
+
* carry an `executeHttp`), `httpModulePath` emits a `spec.httpModule` reference
|
|
2399
|
+
* to a compiled module the operator drops in — the config plugin's escape hatch
|
|
2400
|
+
* for the imperative hot path a JSON manifest cannot express. Absent that, the
|
|
2401
|
+
* browser `flow` is the only execution path, and the field is omitted.
|
|
2421
2402
|
*/
|
|
2422
2403
|
function emitConfigManifest(opts) {
|
|
2423
|
-
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields } = opts;
|
|
2404
|
+
const { siteId, displayName, baseUrl, flowSteps, vocabulary, inputBody, recoveredFields, httpModulePath, } = opts;
|
|
2424
2405
|
const payloadFieldNames = new Set();
|
|
2425
2406
|
const steps = flowSteps.map((step) => {
|
|
2426
2407
|
const isObj = typeof step !== "string";
|
|
@@ -2459,6 +2440,7 @@ function emitConfigManifest(opts) {
|
|
|
2459
2440
|
metadata: { siteId, displayName },
|
|
2460
2441
|
spec: {
|
|
2461
2442
|
defaultBaseUrl: baseUrl,
|
|
2443
|
+
...(httpModulePath ? { httpModule: httpModulePath } : {}),
|
|
2462
2444
|
request: { type: "object", properties: sortedRequestProperties },
|
|
2463
2445
|
response: {
|
|
2464
2446
|
type: "object",
|
|
@@ -2650,17 +2632,36 @@ async function resolveVocabulary(specifier, flowSteps) {
|
|
|
2650
2632
|
}
|
|
2651
2633
|
return DEPRECATED_BUILTIN_ATS_VOCABULARY;
|
|
2652
2634
|
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Resolves the form-schema for this run and reports which one is in play.
|
|
2637
|
+
*
|
|
2638
|
+
* The engine carries no vendor's wire format (issue #57): a consumer whose ATS
|
|
2639
|
+
* exposes a form definition declares its keys with `--form-schema`. Absent one
|
|
2640
|
+
* (or `--form-schema none`), form-key recovery does not run and the generator
|
|
2641
|
+
* recovers nothing from ATS-shaped responses — the same "absence means none"
|
|
2642
|
+
* discipline `--vocabulary` uses.
|
|
2643
|
+
*/
|
|
2644
|
+
async function resolveFormSchema(specifier) {
|
|
2645
|
+
if (!specifier)
|
|
2646
|
+
return null;
|
|
2647
|
+
const formSchema = await (0, load_form_schema_1.loadReconFormSchema)(specifier, process.cwd());
|
|
2648
|
+
logger.info(`form-schema: ${specifier === load_form_schema_1.FORM_SCHEMA_NONE ? "none (no ATS form recovery)" : `custom keys from ${specifier}`}`);
|
|
2649
|
+
return formSchema;
|
|
2650
|
+
}
|
|
2653
2651
|
async function main() {
|
|
2654
2652
|
const args = process.argv.slice(2);
|
|
2655
2653
|
let siteId = "";
|
|
2656
2654
|
let force = false;
|
|
2657
2655
|
let emit = "ts";
|
|
2658
2656
|
let vocabularySpecifier = "";
|
|
2657
|
+
let formSchemaSpecifier = "";
|
|
2659
2658
|
for (let i = 0; i < args.length; i++) {
|
|
2660
2659
|
if (args[i] === "--site-id" && args[i + 1])
|
|
2661
2660
|
siteId = args[++i];
|
|
2662
2661
|
else if (args[i] === "--vocabulary" && args[i + 1])
|
|
2663
2662
|
vocabularySpecifier = args[++i];
|
|
2663
|
+
else if (args[i] === "--form-schema" && args[i + 1])
|
|
2664
|
+
formSchemaSpecifier = args[++i];
|
|
2664
2665
|
else if (args[i] === "--force")
|
|
2665
2666
|
force = true;
|
|
2666
2667
|
else if (args[i] === "--emit" && args[i + 1]) {
|
|
@@ -2731,6 +2732,9 @@ async function main() {
|
|
|
2731
2732
|
// module-level const would freeze at import time, which is the bug that makes
|
|
2732
2733
|
// RECON_QUESTION_KEYWORDS silently inert for anyone setting it after load.
|
|
2733
2734
|
const vocabulary = await resolveVocabulary(vocabularySpecifier, flowSteps);
|
|
2735
|
+
// Consumer-supplied wire keys for ATS form-schema recovery, or null. When
|
|
2736
|
+
// null the recovery functions no-op — the engine hardcodes no vendor format.
|
|
2737
|
+
const formSchema = await resolveFormSchema(formSchemaSpecifier);
|
|
2734
2738
|
const pascal = toPascalCase(siteId);
|
|
2735
2739
|
const baseUrl = deriveBaseUrl(captures);
|
|
2736
2740
|
const baseHeaders = deriveRequestHeaders(captures, replays, baseUrl);
|
|
@@ -2746,15 +2750,15 @@ async function main() {
|
|
|
2746
2750
|
const rawActionCaptures = gql
|
|
2747
2751
|
? []
|
|
2748
2752
|
: collapseRedundantPatches(extractActionSequence(captures, baseUrl));
|
|
2749
|
-
// Form-schema detection runs BEFORE state-indexing so the
|
|
2753
|
+
// Form-schema detection runs BEFORE state-indexing so the field-id/option-id
|
|
2750
2754
|
// UUIDs can be shielded from indexing — those UUIDs are stable schema
|
|
2751
2755
|
// anchors that T2/T3 substitution depends on remaining literal in body
|
|
2752
2756
|
// templates.
|
|
2753
|
-
const { fieldNameMap, fieldOptionsMap, allSchemaUuids } = detectFormSchemaFieldNames(captures);
|
|
2754
|
-
// Shield ALL
|
|
2757
|
+
const { fieldNameMap, fieldOptionsMap, allSchemaUuids } = detectFormSchemaFieldNames(captures, formSchema);
|
|
2758
|
+
// Shield ALL field-id/option-id UUIDs that appear in any schema response, not
|
|
2755
2759
|
// just the ones that detectFormSchemaFieldNames emits a payload name for.
|
|
2756
|
-
// Some fields have
|
|
2757
|
-
//
|
|
2760
|
+
// Some fields have names too long for the naming heuristic (>80 chars) and
|
|
2761
|
+
// would be skipped by fieldNameMap; their field-ids still need shielding
|
|
2758
2762
|
// because they appear as anchors in the T2-substituted body templates.
|
|
2759
2763
|
const shieldedUuids = new Set(allSchemaUuids);
|
|
2760
2764
|
// T4 — Phase B+C: detect a form-schema GET capture and insert it into the
|
|
@@ -2764,7 +2768,7 @@ async function main() {
|
|
|
2764
2768
|
// params (recon timestamps) from the captured URL so the emitted runtime
|
|
2765
2769
|
// fetch uses a clean template. Sites without a schema-fetch capture
|
|
2766
2770
|
// (rawSchemaFetch === null) get unchanged behavior.
|
|
2767
|
-
const rawSchemaFetch = gql ? null : detectFormSchemaFetchCapture(captures, baseUrl);
|
|
2771
|
+
const rawSchemaFetch = gql || formSchema === null ? null : detectFormSchemaFetchCapture(captures, baseUrl, formSchema);
|
|
2768
2772
|
const schemaFetchCleaned = rawSchemaFetch
|
|
2769
2773
|
? { ...rawSchemaFetch.capture, url: stripCacheBusterParams(rawSchemaFetch.capture.url) }
|
|
2770
2774
|
: null;
|
|
@@ -2805,9 +2809,9 @@ async function main() {
|
|
|
2805
2809
|
const errorSignals = detectErrorSignals(actionSteps);
|
|
2806
2810
|
const discoveredFormFields = new Set();
|
|
2807
2811
|
const discoveredOptionFields = new Set();
|
|
2808
|
-
// Phase E: maps
|
|
2809
|
-
// "AreYouOverTheAgeOf18OptionId") → recon-observed
|
|
2810
|
-
// emit `<
|
|
2812
|
+
// Phase E: maps label-derived raw-option payload field name (e.g.
|
|
2813
|
+
// "AreYouOverTheAgeOf18OptionId") → recon-observed option-id UUID. Used to
|
|
2814
|
+
// emit `<Name>OptionId: z.string()` payload fields with TSDoc docs.
|
|
2811
2815
|
const discoveredRawOptionFields = new Map();
|
|
2812
2816
|
// Phase F: keys from additional action POST bodies (beyond inputBody/r0)
|
|
2813
2817
|
// that get parameterized. Recorded with their value type so the contract
|
|
@@ -2844,7 +2848,7 @@ async function main() {
|
|
|
2844
2848
|
}
|
|
2845
2849
|
}
|
|
2846
2850
|
const multiStepBody = isSubmissionFlow
|
|
2847
|
-
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders)
|
|
2851
|
+
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, new Map(), formSchema)
|
|
2848
2852
|
: undefined;
|
|
2849
2853
|
let base64ContentHelper = "";
|
|
2850
2854
|
const base64PatchOverride = new Map();
|
|
@@ -2936,7 +2940,7 @@ async function main() {
|
|
|
2936
2940
|
}
|
|
2937
2941
|
}
|
|
2938
2942
|
const processedMultiStepBody = isSubmissionFlow
|
|
2939
|
-
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride)
|
|
2943
|
+
? emitMultiStepExecuteHttp(actionSteps, inputBody, errorSignals, fieldNameMap, discoveredFormFields, fieldOptionsMap, discoveredOptionFields, discoveredRawOptionFields, discoveredAdditionalBodyKeys, baseUrl, baseUrlDerivedHeaders, tenantSubdomainHeaders, base64PatchOverride, formSchema)
|
|
2940
2944
|
: multiStepBody;
|
|
2941
2945
|
const hasMultipartStep = actionSteps.some((s) => s.isMultipart);
|
|
2942
2946
|
const headerBindings = collectHeaderBindings(actionSteps);
|
|
@@ -2957,6 +2961,10 @@ async function main() {
|
|
|
2957
2961
|
vocabulary,
|
|
2958
2962
|
inputBody,
|
|
2959
2963
|
recoveredFields: [...discoveredFormFields, ...discoveredOptionFields],
|
|
2964
|
+
// A submission flow is the case where the `.ts` emit carries an
|
|
2965
|
+
// executeHttp hot path; point the manifest at where the operator drops
|
|
2966
|
+
// the compiled module rather than silently dropping the direct path.
|
|
2967
|
+
httpModulePath: isSubmissionFlow ? `./${siteId}.http.js` : undefined,
|
|
2960
2968
|
}));
|
|
2961
2969
|
logger.info(`wrote ${manifestPath}`);
|
|
2962
2970
|
logger.info(`done — review ${manifestPath}, fill in response/extract schemas, then load via BARNACLE_PLUGINS or BARNACLE_PLUGINS_CONFIG_DIR (no compile step)`);
|