@enricai/barnacle 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +958 -0
- package/dist/api/errors.d.ts +88 -0
- package/dist/api/errors.d.ts.map +1 -0
- package/dist/api/errors.js +164 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/helpers/envelope.d.ts +13 -0
- package/dist/api/helpers/envelope.d.ts.map +1 -0
- package/dist/api/helpers/envelope.js +20 -0
- package/dist/api/helpers/envelope.js.map +1 -0
- package/dist/api/helpers/reply.d.ts +9 -0
- package/dist/api/helpers/reply.d.ts.map +1 -0
- package/dist/api/helpers/reply.js +14 -0
- package/dist/api/helpers/reply.js.map +1 -0
- package/dist/api/plugins/auth.d.ts +27 -0
- package/dist/api/plugins/auth.d.ts.map +1 -0
- package/dist/api/plugins/auth.js +111 -0
- package/dist/api/plugins/auth.js.map +1 -0
- package/dist/api/plugins/error-handler.d.ts +12 -0
- package/dist/api/plugins/error-handler.d.ts.map +1 -0
- package/dist/api/plugins/error-handler.js +72 -0
- package/dist/api/plugins/error-handler.js.map +1 -0
- package/dist/api/plugins/request-context.d.ts +17 -0
- package/dist/api/plugins/request-context.d.ts.map +1 -0
- package/dist/api/plugins/request-context.js +61 -0
- package/dist/api/plugins/request-context.js.map +1 -0
- package/dist/api/routes/health.d.ts +55 -0
- package/dist/api/routes/health.d.ts.map +1 -0
- package/dist/api/routes/health.js +137 -0
- package/dist/api/routes/health.js.map +1 -0
- package/dist/api/routes/plugins-introspection.d.ts +17 -0
- package/dist/api/routes/plugins-introspection.d.ts.map +1 -0
- package/dist/api/routes/plugins-introspection.js +13 -0
- package/dist/api/routes/plugins-introspection.js.map +1 -0
- package/dist/api/schemas/common.d.ts +114 -0
- package/dist/api/schemas/common.d.ts.map +1 -0
- package/dist/api/schemas/common.js +109 -0
- package/dist/api/schemas/common.js.map +1 -0
- package/dist/api/schemas/telemetry.d.ts +66 -0
- package/dist/api/schemas/telemetry.d.ts.map +1 -0
- package/dist/api/schemas/telemetry.js +52 -0
- package/dist/api/schemas/telemetry.js.map +1 -0
- package/dist/cache/keyed-ttl-cache.d.ts +45 -0
- package/dist/cache/keyed-ttl-cache.d.ts.map +1 -0
- package/dist/cache/keyed-ttl-cache.js +69 -0
- package/dist/cache/keyed-ttl-cache.js.map +1 -0
- package/dist/cache/response-cache.d.ts +37 -0
- package/dist/cache/response-cache.d.ts.map +1 -0
- package/dist/cache/response-cache.js +114 -0
- package/dist/cache/response-cache.js.map +1 -0
- package/dist/config.d.ts +258 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/lib/applicant-payload.d.ts +28 -0
- package/dist/lib/applicant-payload.d.ts.map +1 -0
- package/dist/lib/applicant-payload.js +20 -0
- package/dist/lib/applicant-payload.js.map +1 -0
- package/dist/lib/application-address.d.ts +16 -0
- package/dist/lib/application-address.d.ts.map +1 -0
- package/dist/lib/application-address.js +20 -0
- package/dist/lib/application-address.js.map +1 -0
- package/dist/lib/application-answers.d.ts +151 -0
- package/dist/lib/application-answers.d.ts.map +1 -0
- package/dist/lib/application-answers.js +95 -0
- package/dist/lib/application-answers.js.map +1 -0
- package/dist/lib/application-identity.d.ts +18 -0
- package/dist/lib/application-identity.d.ts.map +1 -0
- package/dist/lib/application-identity.js +20 -0
- package/dist/lib/application-identity.js.map +1 -0
- package/dist/lib/application-resume.d.ts +21 -0
- package/dist/lib/application-resume.d.ts.map +1 -0
- package/dist/lib/application-resume.js +23 -0
- package/dist/lib/application-resume.js.map +1 -0
- package/dist/lib/bedrock.d.ts +22 -0
- package/dist/lib/bedrock.d.ts.map +1 -0
- package/dist/lib/bedrock.js +35 -0
- package/dist/lib/bedrock.js.map +1 -0
- package/dist/lib/case-insensitive-headers.d.ts +15 -0
- package/dist/lib/case-insensitive-headers.d.ts.map +1 -0
- package/dist/lib/case-insensitive-headers.js +29 -0
- package/dist/lib/case-insensitive-headers.js.map +1 -0
- package/dist/lib/chromium-client-hints.d.ts +11 -0
- package/dist/lib/chromium-client-hints.d.ts.map +1 -0
- package/dist/lib/chromium-client-hints.js +17 -0
- package/dist/lib/chromium-client-hints.js.map +1 -0
- package/dist/lib/cookie-value.d.ts +8 -0
- package/dist/lib/cookie-value.d.ts.map +1 -0
- package/dist/lib/cookie-value.js +14 -0
- package/dist/lib/cookie-value.js.map +1 -0
- package/dist/lib/datadog.d.ts +8 -0
- package/dist/lib/datadog.d.ts.map +1 -0
- package/dist/lib/datadog.js +27 -0
- package/dist/lib/datadog.js.map +1 -0
- package/dist/lib/dd-metrics.d.ts +28 -0
- package/dist/lib/dd-metrics.d.ts.map +1 -0
- package/dist/lib/dd-metrics.js +62 -0
- package/dist/lib/dd-metrics.js.map +1 -0
- package/dist/lib/dispatch-metrics.d.ts +19 -0
- package/dist/lib/dispatch-metrics.d.ts.map +1 -0
- package/dist/lib/dispatch-metrics.js +65 -0
- package/dist/lib/dispatch-metrics.js.map +1 -0
- package/dist/lib/env.d.ts +37 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +70 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +20 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/http.d.ts +7 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +14 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/job-tracking.d.ts +17 -0
- package/dist/lib/job-tracking.d.ts.map +1 -0
- package/dist/lib/job-tracking.js +19 -0
- package/dist/lib/job-tracking.js.map +1 -0
- package/dist/lib/llm/anthropic-client.d.ts +12 -0
- package/dist/lib/llm/anthropic-client.d.ts.map +1 -0
- package/dist/lib/llm/anthropic-client.js +23 -0
- package/dist/lib/llm/anthropic-client.js.map +1 -0
- package/dist/lib/llm/judge.d.ts +57 -0
- package/dist/lib/llm/judge.d.ts.map +1 -0
- package/dist/lib/llm/judge.js +123 -0
- package/dist/lib/llm/judge.js.map +1 -0
- package/dist/lib/llm/judges/error-messages.d.ts +38 -0
- package/dist/lib/llm/judges/error-messages.d.ts.map +1 -0
- package/dist/lib/llm/judges/error-messages.js +72 -0
- package/dist/lib/llm/judges/error-messages.js.map +1 -0
- package/dist/lib/llm/judges/invalid-fields.d.ts +43 -0
- package/dist/lib/llm/judges/invalid-fields.d.ts.map +1 -0
- package/dist/lib/llm/judges/invalid-fields.js +80 -0
- package/dist/lib/llm/judges/invalid-fields.js.map +1 -0
- package/dist/lib/llm/judges/modal-priority.d.ts +41 -0
- package/dist/lib/llm/judges/modal-priority.d.ts.map +1 -0
- package/dist/lib/llm/judges/modal-priority.js +74 -0
- package/dist/lib/llm/judges/modal-priority.js.map +1 -0
- package/dist/lib/llm/judges/select-option.d.ts +49 -0
- package/dist/lib/llm/judges/select-option.d.ts.map +1 -0
- package/dist/lib/llm/judges/select-option.js +90 -0
- package/dist/lib/llm/judges/select-option.js.map +1 -0
- package/dist/lib/llm/judges/verify-submit.d.ts +68 -0
- package/dist/lib/llm/judges/verify-submit.d.ts.map +1 -0
- package/dist/lib/llm/judges/verify-submit.js +89 -0
- package/dist/lib/llm/judges/verify-submit.js.map +1 -0
- package/dist/lib/llm/schemas.d.ts +220 -0
- package/dist/lib/llm/schemas.d.ts.map +1 -0
- package/dist/lib/llm/schemas.js +239 -0
- package/dist/lib/llm/schemas.js.map +1 -0
- package/dist/lib/logging.d.ts +39 -0
- package/dist/lib/logging.d.ts.map +1 -0
- package/dist/lib/logging.js +219 -0
- package/dist/lib/logging.js.map +1 -0
- package/dist/lib/multipart.d.ts +34 -0
- package/dist/lib/multipart.d.ts.map +1 -0
- package/dist/lib/multipart.js +55 -0
- package/dist/lib/multipart.js.map +1 -0
- package/dist/lib/option-matcher.d.ts +15 -0
- package/dist/lib/option-matcher.d.ts.map +1 -0
- package/dist/lib/option-matcher.js +69 -0
- package/dist/lib/option-matcher.js.map +1 -0
- package/dist/lib/phone.d.ts +11 -0
- package/dist/lib/phone.d.ts.map +1 -0
- package/dist/lib/phone.js +17 -0
- package/dist/lib/phone.js.map +1 -0
- package/dist/lib/random.d.ts +22 -0
- package/dist/lib/random.d.ts.map +1 -0
- package/dist/lib/random.js +33 -0
- package/dist/lib/random.js.map +1 -0
- package/dist/lib/statsd.d.ts +11 -0
- package/dist/lib/statsd.d.ts.map +1 -0
- package/dist/lib/statsd.js +68 -0
- package/dist/lib/statsd.js.map +1 -0
- package/dist/lib/telemetry/call-capture.d.ts +89 -0
- package/dist/lib/telemetry/call-capture.d.ts.map +1 -0
- package/dist/lib/telemetry/call-capture.js +190 -0
- package/dist/lib/telemetry/call-capture.js.map +1 -0
- package/dist/lib/telemetry/call-types.d.ts +65 -0
- package/dist/lib/telemetry/call-types.d.ts.map +1 -0
- package/dist/lib/telemetry/call-types.js +68 -0
- package/dist/lib/telemetry/call-types.js.map +1 -0
- package/dist/lib/telemetry/run-state.d.ts +15 -0
- package/dist/lib/telemetry/run-state.d.ts.map +1 -0
- package/dist/lib/telemetry/run-state.js +23 -0
- package/dist/lib/telemetry/run-state.js.map +1 -0
- package/dist/lib/telemetry/s3-sink.d.ts +48 -0
- package/dist/lib/telemetry/s3-sink.d.ts.map +1 -0
- package/dist/lib/telemetry/s3-sink.js +229 -0
- package/dist/lib/telemetry/s3-sink.js.map +1 -0
- package/dist/lib/telemetry/submission-capture.d.ts +45 -0
- package/dist/lib/telemetry/submission-capture.d.ts.map +1 -0
- package/dist/lib/telemetry/submission-capture.js +57 -0
- package/dist/lib/telemetry/submission-capture.js.map +1 -0
- package/dist/lib/telemetry/telemetry-paths.d.ts +59 -0
- package/dist/lib/telemetry/telemetry-paths.d.ts.map +1 -0
- package/dist/lib/telemetry/telemetry-paths.js +103 -0
- package/dist/lib/telemetry/telemetry-paths.js.map +1 -0
- package/dist/lib/tracking-click.d.ts +20 -0
- package/dist/lib/tracking-click.d.ts.map +1 -0
- package/dist/lib/tracking-click.js +75 -0
- package/dist/lib/tracking-click.js.map +1 -0
- package/dist/lib/url-capture.d.ts +8 -0
- package/dist/lib/url-capture.d.ts.map +1 -0
- package/dist/lib/url-capture.js +17 -0
- package/dist/lib/url-capture.js.map +1 -0
- package/dist/lib/us-states.d.ts +20 -0
- package/dist/lib/us-states.d.ts.map +1 -0
- package/dist/lib/us-states.js +86 -0
- package/dist/lib/us-states.js.map +1 -0
- package/dist/lib/zod-multipart.d.ts +18 -0
- package/dist/lib/zod-multipart.d.ts.map +1 -0
- package/dist/lib/zod-multipart.js +35 -0
- package/dist/lib/zod-multipart.js.map +1 -0
- package/dist/plugins/config-plugin.d.ts +77 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -0
- package/dist/plugins/config-plugin.js +216 -0
- package/dist/plugins/config-plugin.js.map +1 -0
- package/dist/plugins/discover.d.ts +72 -0
- package/dist/plugins/discover.d.ts.map +1 -0
- package/dist/plugins/discover.js +265 -0
- package/dist/plugins/discover.js.map +1 -0
- package/dist/plugins/json-schema-to-zod.d.ts +47 -0
- package/dist/plugins/json-schema-to-zod.d.ts.map +1 -0
- package/dist/plugins/json-schema-to-zod.js +85 -0
- package/dist/plugins/json-schema-to-zod.js.map +1 -0
- package/dist/plugins/loader.d.ts +34 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +348 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/plugin-api-version.d.ts +8 -0
- package/dist/plugins/plugin-api-version.d.ts.map +1 -0
- package/dist/plugins/plugin-api-version.js +11 -0
- package/dist/plugins/plugin-api-version.js.map +1 -0
- package/dist/plugins/plugin-manifest-envelope.d.ts +16 -0
- package/dist/plugins/plugin-manifest-envelope.d.ts.map +1 -0
- package/dist/plugins/plugin-manifest-envelope.js +19 -0
- package/dist/plugins/plugin-manifest-envelope.js.map +1 -0
- package/dist/scraper/behavioral-signals.d.ts +16 -0
- package/dist/scraper/behavioral-signals.d.ts.map +1 -0
- package/dist/scraper/behavioral-signals.js +30 -0
- package/dist/scraper/behavioral-signals.js.map +1 -0
- package/dist/scraper/errors.d.ts +193 -0
- package/dist/scraper/errors.d.ts.map +1 -0
- package/dist/scraper/errors.js +226 -0
- package/dist/scraper/errors.js.map +1 -0
- package/dist/scraper/fixtures.d.ts +23 -0
- package/dist/scraper/fixtures.d.ts.map +1 -0
- package/dist/scraper/fixtures.js +48 -0
- package/dist/scraper/fixtures.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +1319 -0
- package/dist/scraper/flow-runner.d.ts.map +1 -0
- package/dist/scraper/flow-runner.js +5637 -0
- package/dist/scraper/flow-runner.js.map +1 -0
- package/dist/scraper/graphql-client.d.ts +30 -0
- package/dist/scraper/graphql-client.d.ts.map +1 -0
- package/dist/scraper/graphql-client.js +14 -0
- package/dist/scraper/graphql-client.js.map +1 -0
- package/dist/scraper/http-client.d.ts +75 -0
- package/dist/scraper/http-client.d.ts.map +1 -0
- package/dist/scraper/http-client.js +189 -0
- package/dist/scraper/http-client.js.map +1 -0
- package/dist/scraper/http-status-classifier.d.ts +12 -0
- package/dist/scraper/http-status-classifier.d.ts.map +1 -0
- package/dist/scraper/http-status-classifier.js +29 -0
- package/dist/scraper/http-status-classifier.js.map +1 -0
- package/dist/scraper/metrics.d.ts +50 -0
- package/dist/scraper/metrics.d.ts.map +1 -0
- package/dist/scraper/metrics.js +85 -0
- package/dist/scraper/metrics.js.map +1 -0
- package/dist/scraper/navigate.d.ts +20 -0
- package/dist/scraper/navigate.d.ts.map +1 -0
- package/dist/scraper/navigate.js +30 -0
- package/dist/scraper/navigate.js.map +1 -0
- package/dist/scraper/oracle-sentinels.d.ts +22 -0
- package/dist/scraper/oracle-sentinels.d.ts.map +1 -0
- package/dist/scraper/oracle-sentinels.js +40 -0
- package/dist/scraper/oracle-sentinels.js.map +1 -0
- package/dist/scraper/parse-json-response.d.ts +16 -0
- package/dist/scraper/parse-json-response.d.ts.map +1 -0
- package/dist/scraper/parse-json-response.js +37 -0
- package/dist/scraper/parse-json-response.js.map +1 -0
- package/dist/scraper/pool.d.ts +37 -0
- package/dist/scraper/pool.d.ts.map +1 -0
- package/dist/scraper/pool.js +105 -0
- package/dist/scraper/pool.js.map +1 -0
- package/dist/scraper/rate-limited-json-client.d.ts +35 -0
- package/dist/scraper/rate-limited-json-client.d.ts.map +1 -0
- package/dist/scraper/rate-limited-json-client.js +29 -0
- package/dist/scraper/rate-limited-json-client.js.map +1 -0
- package/dist/scraper/raw-fetch.d.ts +49 -0
- package/dist/scraper/raw-fetch.d.ts.map +1 -0
- package/dist/scraper/raw-fetch.js +40 -0
- package/dist/scraper/raw-fetch.js.map +1 -0
- package/dist/scraper/require-response-field.d.ts +23 -0
- package/dist/scraper/require-response-field.d.ts.map +1 -0
- package/dist/scraper/require-response-field.js +42 -0
- package/dist/scraper/require-response-field.js.map +1 -0
- package/dist/scraper/retry.d.ts +33 -0
- package/dist/scraper/retry.d.ts.map +1 -0
- package/dist/scraper/retry.js +122 -0
- package/dist/scraper/retry.js.map +1 -0
- package/dist/scraper/session-browserbase.d.ts +35 -0
- package/dist/scraper/session-browserbase.d.ts.map +1 -0
- package/dist/scraper/session-browserbase.js +198 -0
- package/dist/scraper/session-browserbase.js.map +1 -0
- package/dist/scraper/session-shared.d.ts +29 -0
- package/dist/scraper/session-shared.d.ts.map +1 -0
- package/dist/scraper/session-shared.js +35 -0
- package/dist/scraper/session-shared.js.map +1 -0
- package/dist/scraper/session-steel.d.ts +31 -0
- package/dist/scraper/session-steel.d.ts.map +1 -0
- package/dist/scraper/session-steel.js +140 -0
- package/dist/scraper/session-steel.js.map +1 -0
- package/dist/scraper/session-warmup.d.ts +31 -0
- package/dist/scraper/session-warmup.d.ts.map +1 -0
- package/dist/scraper/session-warmup.js +46 -0
- package/dist/scraper/session-warmup.js.map +1 -0
- package/dist/scraper/session.d.ts +21 -0
- package/dist/scraper/session.d.ts.map +1 -0
- package/dist/scraper/session.js +28 -0
- package/dist/scraper/session.js.map +1 -0
- package/dist/scraper/stagehand-guard.d.ts +128 -0
- package/dist/scraper/stagehand-guard.d.ts.map +1 -0
- package/dist/scraper/stagehand-guard.js +337 -0
- package/dist/scraper/stagehand-guard.js.map +1 -0
- package/dist/scraper/throttle.d.ts +21 -0
- package/dist/scraper/throttle.d.ts.map +1 -0
- package/dist/scraper/throttle.js +45 -0
- package/dist/scraper/throttle.js.map +1 -0
- package/dist/scripts/hca-inbox-poll.d.ts +19 -0
- package/dist/scripts/hca-inbox-poll.d.ts.map +1 -0
- package/dist/scripts/hca-inbox-poll.js +151 -0
- package/dist/scripts/hca-inbox-poll.js.map +1 -0
- package/dist/scripts/judge-llm-batch.d.ts +98 -0
- package/dist/scripts/judge-llm-batch.d.ts.map +1 -0
- package/dist/scripts/judge-llm-batch.js +350 -0
- package/dist/scripts/judge-llm-batch.js.map +1 -0
- package/dist/scripts/llm-heal.d.ts +165 -0
- package/dist/scripts/llm-heal.d.ts.map +1 -0
- package/dist/scripts/llm-heal.js +595 -0
- package/dist/scripts/llm-heal.js.map +1 -0
- package/dist/scripts/migrate-telemetry-dir-names.d.ts +57 -0
- package/dist/scripts/migrate-telemetry-dir-names.d.ts.map +1 -0
- package/dist/scripts/migrate-telemetry-dir-names.js +156 -0
- package/dist/scripts/migrate-telemetry-dir-names.js.map +1 -0
- package/dist/scripts/probe-setinputfiles.d.ts +28 -0
- package/dist/scripts/probe-setinputfiles.d.ts.map +1 -0
- package/dist/scripts/probe-setinputfiles.js +95 -0
- package/dist/scripts/probe-setinputfiles.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts +304 -0
- package/dist/scripts/recon-browser.d.ts.map +1 -0
- package/dist/scripts/recon-browser.js +1833 -0
- package/dist/scripts/recon-browser.js.map +1 -0
- package/dist/scripts/recon-generate.d.ts +182 -0
- package/dist/scripts/recon-generate.d.ts.map +1 -0
- package/dist/scripts/recon-generate.js +2572 -0
- package/dist/scripts/recon-generate.js.map +1 -0
- package/dist/scripts/recon-heal.d.ts +176 -0
- package/dist/scripts/recon-heal.d.ts.map +1 -0
- package/dist/scripts/recon-heal.js +584 -0
- package/dist/scripts/recon-heal.js.map +1 -0
- package/dist/scripts/recon-http.d.ts +19 -0
- package/dist/scripts/recon-http.d.ts.map +1 -0
- package/dist/scripts/recon-http.js +377 -0
- package/dist/scripts/recon-http.js.map +1 -0
- package/dist/scripts/recon-replay-jobs.d.ts +21 -0
- package/dist/scripts/recon-replay-jobs.d.ts.map +1 -0
- package/dist/scripts/recon-replay-jobs.js +221 -0
- package/dist/scripts/recon-replay-jobs.js.map +1 -0
- package/dist/scripts/recon-shared.d.ts +52 -0
- package/dist/scripts/recon-shared.d.ts.map +1 -0
- package/dist/scripts/recon-shared.js +68 -0
- package/dist/scripts/recon-shared.js.map +1 -0
- package/dist/scripts/recon-summarize.d.ts +15 -0
- package/dist/scripts/recon-summarize.d.ts.map +1 -0
- package/dist/scripts/recon-summarize.js +233 -0
- package/dist/scripts/recon-summarize.js.map +1 -0
- package/dist/scripts/smoke-test.d.ts +22 -0
- package/dist/scripts/smoke-test.d.ts.map +1 -0
- package/dist/scripts/smoke-test.js +236 -0
- package/dist/scripts/smoke-test.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +189 -0
- package/dist/server.js.map +1 -0
- package/dist/site-plugin.d.ts +226 -0
- package/dist/site-plugin.d.ts.map +1 -0
- package/dist/site-plugin.js +13 -0
- package/dist/site-plugin.js.map +1 -0
- package/dist/testing/answers-fixture.d.ts +13 -0
- package/dist/testing/answers-fixture.d.ts.map +1 -0
- package/dist/testing/answers-fixture.js +37 -0
- package/dist/testing/answers-fixture.js.map +1 -0
- package/dist/testing/batch-email-confirmation.d.ts +58 -0
- package/dist/testing/batch-email-confirmation.d.ts.map +1 -0
- package/dist/testing/batch-email-confirmation.js +62 -0
- package/dist/testing/batch-email-confirmation.js.map +1 -0
- package/dist/testing/batch-report.d.ts +33 -0
- package/dist/testing/batch-report.d.ts.map +1 -0
- package/dist/testing/batch-report.js +30 -0
- package/dist/testing/batch-report.js.map +1 -0
- package/dist/testing/contract-parity-suite.d.ts +55 -0
- package/dist/testing/contract-parity-suite.d.ts.map +1 -0
- package/dist/testing/contract-parity-suite.js +39 -0
- package/dist/testing/contract-parity-suite.js.map +1 -0
- package/dist/testing/coverage-guard-suite.d.ts +46 -0
- package/dist/testing/coverage-guard-suite.d.ts.map +1 -0
- package/dist/testing/coverage-guard-suite.js +39 -0
- package/dist/testing/coverage-guard-suite.js.map +1 -0
- package/dist/testing/fixtures/resume.pdf +54 -0
- package/dist/testing/integration-runner.d.ts +62 -0
- package/dist/testing/integration-runner.d.ts.map +1 -0
- package/dist/testing/integration-runner.js +46 -0
- package/dist/testing/integration-runner.js.map +1 -0
- package/dist/testing/mock-fetch-response.d.ts +7 -0
- package/dist/testing/mock-fetch-response.d.ts.map +1 -0
- package/dist/testing/mock-fetch-response.js +16 -0
- package/dist/testing/mock-fetch-response.js.map +1 -0
- package/dist/testing/persona-fixture.d.ts +77 -0
- package/dist/testing/persona-fixture.d.ts.map +1 -0
- package/dist/testing/persona-fixture.js +83 -0
- package/dist/testing/persona-fixture.js.map +1 -0
- package/dist/testing/replay-integration-suite.d.ts +50 -0
- package/dist/testing/replay-integration-suite.d.ts.map +1 -0
- package/dist/testing/replay-integration-suite.js +45 -0
- package/dist/testing/replay-integration-suite.js.map +1 -0
- package/dist/testing/resume-fixture.d.ts +39 -0
- package/dist/testing/resume-fixture.d.ts.map +1 -0
- package/dist/testing/resume-fixture.js +38 -0
- package/dist/testing/resume-fixture.js.map +1 -0
- package/dist/testmail/client.d.ts +61 -0
- package/dist/testmail/client.d.ts.map +1 -0
- package/dist/testmail/client.js +190 -0
- package/dist/testmail/client.js.map +1 -0
- package/dist/testmail/errors.d.ts +18 -0
- package/dist/testmail/errors.d.ts.map +1 -0
- package/dist/testmail/errors.js +29 -0
- package/dist/testmail/errors.js.map +1 -0
- package/dist/types/dispatch-metrics.d.ts +46 -0
- package/dist/types/dispatch-metrics.d.ts.map +1 -0
- package/dist/types/dispatch-metrics.js +25 -0
- package/dist/types/dispatch-metrics.js.map +1 -0
- package/dist/types/logging.d.ts +11 -0
- package/dist/types/logging.d.ts.map +1 -0
- package/dist/types/logging.js +3 -0
- package/dist/types/logging.js.map +1 -0
- package/package.json +283 -0
package/README.md
ADDED
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
# Barnacle
|
|
2
|
+
|
|
3
|
+
Point Barnacle at a site, describe the user flow in plain English, and run three
|
|
4
|
+
recon commands. Barnacle drives a real browser through the flow, captures every
|
|
5
|
+
API call, replays them with plain HTTP to prove which ones work without a browser,
|
|
6
|
+
probes rate-limit ceilings, and then generates a complete plugin — Zod schemas
|
|
7
|
+
inferred from captured JSON, load-bearing headers, rate-limit ceiling, hot-path
|
|
8
|
+
HTTP client, and Stagehand browser fallback. Register the plugin in one line;
|
|
9
|
+
Barnacle handles sessions, retries, fallback routing, audit persistence, and
|
|
10
|
+
response envelope wrapping.
|
|
11
|
+
|
|
12
|
+
## How it works
|
|
13
|
+
|
|
14
|
+
### The mental model
|
|
15
|
+
|
|
16
|
+
Stagehand drives a real browser through your described user flow. Its only job is
|
|
17
|
+
to trigger the site's network traffic — not to extract DOM data. While it clicks,
|
|
18
|
+
a response listener wiretaps every API call to disk. Once that recon run is done,
|
|
19
|
+
a separate script replays those captures via plain `fetch()` — no browser, no AI —
|
|
20
|
+
to prove the endpoints work standalone. The surviving queries and headers become
|
|
21
|
+
committed constants. In production, the runtime hits those endpoints directly:
|
|
22
|
+
fast, cheap, deterministic. The browser only re-engages if the direct path breaks.
|
|
23
|
+
|
|
24
|
+
A nightly smoke test tells you the moment a contract drifts. When it fires, you
|
|
25
|
+
re-run the same recon command you ran the first time and diff the captures.
|
|
26
|
+
Human involvement is one recon run up front and a small PR when things change.
|
|
27
|
+
|
|
28
|
+
### The pipeline at a glance
|
|
29
|
+
|
|
30
|
+
| Phase | What runs | What you get |
|
|
31
|
+
|-------|-----------|--------------|
|
|
32
|
+
| **1 — Browser recon** | `pnpm run recon:browser` | Every API call the site makes, captured to `/tmp/recon/graphql/*.json` |
|
|
33
|
+
| **2–3 — HTTP replay + probing** | `pnpm run recon:http` | Proof each endpoint works without a browser; rate-limit ceiling; static fixtures |
|
|
34
|
+
| **4 — Plugin generation** | `pnpm run recon:generate` | A complete plugin: Zod schemas, headers, Bottleneck config, hot-path client, Stagehand fallback |
|
|
35
|
+
| **5+ — Runtime** | `pnpm start` | Direct HTTP hot path, automatic browser fallback, nightly smoke test, drift detection |
|
|
36
|
+
|
|
37
|
+
### Why this approach
|
|
38
|
+
|
|
39
|
+
You will get asked why not just use the browser for every request, or scrape HTML,
|
|
40
|
+
or reverse-engineer endpoints by hand. Here is the honest comparison:
|
|
41
|
+
|
|
42
|
+
| Approach | Cost/req | Latency | Fragile to UI | Fragile to API | Effort |
|
|
43
|
+
|----------|----------|---------|---------------|----------------|--------|
|
|
44
|
+
| Browser on every request | High | 5–15 s | Medium | Low | Low |
|
|
45
|
+
| HTML screen scraper | Low | Low | **High** | Low | Medium |
|
|
46
|
+
| Manual DevTools recon | Low | Low | Low | High (human redo) | High (ongoing) |
|
|
47
|
+
| HAR replay (mitmproxy) | Low | Low | Medium | **High** | Medium |
|
|
48
|
+
| **Recon → codify → direct HTTP + fallback (this)** | **Low** | **Low** | **Low** (re-runnable) | **Low** (fallback covers) | Medium, front-loaded |
|
|
49
|
+
|
|
50
|
+
The browser-on-every-call approach uses Steel minutes and Anthropic tokens on
|
|
51
|
+
every production call — orders of magnitude more expensive at scale. HTML
|
|
52
|
+
scrapers break on every UI redesign, and the API response usually carries richer
|
|
53
|
+
data than what the UI renders anyway. Manual DevTools recon is exactly what this
|
|
54
|
+
pipeline automates, but committed and re-runnable. Front-loaded recon work buys
|
|
55
|
+
an integration as cheap as direct HTTP, as robust as a browser fallback, and
|
|
56
|
+
maintainable in a way none of the hand-rolled options are.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Adding a New Site — The Recon Playbook
|
|
61
|
+
|
|
62
|
+
Every new site follows the same pipeline (Phases 0–6). The only human-authored
|
|
63
|
+
input is the flow definition you write once in Phase 0. After that, the scripts
|
|
64
|
+
run unattended — recon captures, HTTP replay proves endpoints, the generator
|
|
65
|
+
writes the plugin. When the site changes months later, you re-run the same
|
|
66
|
+
command and diff the captures. Human time is front-loaded to one recon run and
|
|
67
|
+
a small PR.
|
|
68
|
+
|
|
69
|
+
### Phase 0 — Define the user flow
|
|
70
|
+
|
|
71
|
+
Commit the flow steps to a file first — this makes recon re-runnable in one command without retyping it. When the site changes and you need to re-run recon months later, you `git pull` and run the same command you ran the first time:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# src/sites/my-site/recon-flow.json
|
|
75
|
+
["click the Electronics category filter", "open the first product result"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Phase 1 — Run the browser recon harness
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Preferred: load flow from committed file
|
|
82
|
+
pnpm run recon:browser -- \
|
|
83
|
+
--url https://example.com \
|
|
84
|
+
--flow-file src/sites/my-site/recon-flow.json
|
|
85
|
+
|
|
86
|
+
# Or inline (ephemeral — must be re-typed each recon run):
|
|
87
|
+
pnpm run recon:browser -- \
|
|
88
|
+
--url https://example.com \
|
|
89
|
+
--flow '["click the Electronics category filter", "open the first product result"]'
|
|
90
|
+
|
|
91
|
+
# For sites whose API paths don't match /graph, /api/, /graphql, /v1/, or *.json:
|
|
92
|
+
pnpm run recon:browser -- \
|
|
93
|
+
--url https://example.com \
|
|
94
|
+
--flow-file src/sites/my-site/recon-flow.json \
|
|
95
|
+
--capture-all
|
|
96
|
+
|
|
97
|
+
# Capture page-load XHRs only (no interaction — useful for pure GET-style SPAs):
|
|
98
|
+
pnpm run recon:browser -- --url https://example.com
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Drives a real Stagehand + Steel browser through your flow. Captures are wired via a single CDP session-level listener (`page.getSessionForFrame().on(...)`) — Stagehand V3 enables the Network domain internally, so attaching our `Network.requestWillBeSent` / `responseReceived` / `loadingFinished` listeners on the main session catches every response, including the early ones that fire before any page-level handler could be wired.
|
|
102
|
+
|
|
103
|
+
Captures every network call matching `/graph`, `/api/`, `/graphql`, `/v1/`, or `*.json` to `/tmp/recon/graphql/<NNN>-<phase>-<operationName>.json` — one file per call, diffable and greppable. Use `--capture-all` for sites with non-standard API paths; it captures every response, producing more noise but missing nothing. Omitting both `--flow` and `--flow-file` runs zero interaction steps and captures only the network activity that fires during page navigation — useful for pure GET-style SPAs that fetch everything they need on load.
|
|
104
|
+
|
|
105
|
+
Each step runs through a self-healing cascade (`act` → `observe + act` → `observe + act` with `ignoreSelectors` → Anthropic-SDK rephrase) verified by network-counter delta or URL change. The script's `main()` attempts up to two global flow replans before giving up; terminal failures dump a diagnostic bundle to `/tmp/recon/step-failures/`. See [docs/playbook.md#1c--self-healing-cascade](./docs/playbook.md#1c--self-healing-cascade) for the full design.
|
|
106
|
+
|
|
107
|
+
Total runtime: 20–40 minutes for a typical flow (longer if healing or replans fire), fully unattended.
|
|
108
|
+
|
|
109
|
+
### Phase 2–3 — Replay and probe
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pnpm run recon:http
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Replays every capture via plain `fetch()` — no browser, no AI — to prove endpoints work standalone. Every replay returning 200 proves the browser is unnecessary for production. Also runs GraphQL introspection, auxiliary fixture detection (static JSON to commit as fixtures), and a rate-limit probe at 1→3→5 rps (run last — if it triggers a ban, all captures are already saved). Results land in `/tmp/recon/replays/`.
|
|
116
|
+
|
|
117
|
+
See [docs/playbook.md](./docs/playbook.md#interpreting-replay-failures) for the full troubleshooting decision matrix when replays fail.
|
|
118
|
+
|
|
119
|
+
### Phase 4 — Generate the plugin
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm run recon:generate -- --site-id my-site
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Reads every artifact from Phases 1–3 — `/tmp/recon/graphql/*.json` (captures), `/tmp/recon/replays/*.json` (replay results), `/tmp/recon/replays/rate-limit.json` (probe findings), `/tmp/recon/aux/*.json` (static fixtures), and `src/sites/my-site/recon-flow.json` — and writes a complete plugin to `src/sites/my-site/`:
|
|
126
|
+
|
|
127
|
+
- `contract.ts` — Zod schemas inferred from captured JSON, load-bearing headers, Bottleneck ceiling, and `executeHttp` / `execute` implementations
|
|
128
|
+
- `flows/browser-flow.ts` — Stagehand fallback wired to your `recon-flow.json` steps
|
|
129
|
+
- `index.ts` — barrel export
|
|
130
|
+
- `fixtures/` — any static JSON found by the auxiliary probe, already copied in
|
|
131
|
+
|
|
132
|
+
Then review the generated files: trim UI-only fields from the GraphQL query, narrow any `z.unknown()` entries in the schema you care about, and verify the headers. If you need to regenerate after making changes to the recon flow, pass `--force`.
|
|
133
|
+
|
|
134
|
+
Optionally generate the human-readable findings doc alongside:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pnpm run recon:summarize -- --site-id my-site
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Writes `docs/my-site-recon.md` with: endpoints found, replay status, rate-limit ceiling, header frequency table, and hazards (Akamai, Cloudflare). Without `--site-id`, the default output path is `docs/target-recon.md`.
|
|
141
|
+
|
|
142
|
+
### Phase 5 — Register the plugin
|
|
143
|
+
|
|
144
|
+
See **[Plugin Authoring Guide → Register the plugin](#register-the-plugin)** below for plugin registration options.
|
|
145
|
+
|
|
146
|
+
### Phase 6 — Wire up drift detection
|
|
147
|
+
|
|
148
|
+
See **[Plugin Authoring Guide → Wire up the nightly smoke test](#wire-up-the-nightly-smoke-test)** below for the CI step that runs the smoke test nightly.
|
|
149
|
+
|
|
150
|
+
See [docs/playbook.md](./docs/playbook.md#phase-6--drift-detection) for the full detection ladder and maintenance loop.
|
|
151
|
+
|
|
152
|
+
### The whole loop, in one picture
|
|
153
|
+
|
|
154
|
+

|
|
155
|
+
|
|
156
|
+
The dashed `deploys` edge is the human-in-the-loop step (the contract PR merges and ships to Runtime). The solid orange edge from `smoke-test.ts` back into Phase 1 is the self-healing loop: when the contract drifts, recon reruns unattended (~20–40 min) and the next PR is a diff of captures, not a hand-rewrite. See [docs/architecture.md](./docs/architecture.md) for the design rationale behind each lane.
|
|
157
|
+
|
|
158
|
+
## Plugin Authoring Guide
|
|
159
|
+
|
|
160
|
+
A site plugin is a single TypeScript module that satisfies `SitePlugin<TInput, TOutput>`
|
|
161
|
+
from `src/site-plugin.ts`. Core registers built-in plugins via `BUILTIN_SITE_PLUGINS` in
|
|
162
|
+
`src/plugins/discover.ts`; out-of-tree plugins are loaded at startup via `BARNACLE_PLUGINS`.
|
|
163
|
+
|
|
164
|
+
### The SitePlugin interface
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
interface SitePlugin<TPayload, TResult> {
|
|
168
|
+
meta: SitePluginMeta;
|
|
169
|
+
// Optional direct-HTTP hot path — no browser, no LLM tokens, millisecond latency.
|
|
170
|
+
// Core tries this first; falls back to execute() on HttpSchemaError / HttpBotChallengeError / HttpServerError.
|
|
171
|
+
executeHttp?: (
|
|
172
|
+
payload: TPayload,
|
|
173
|
+
context: SitePluginContext
|
|
174
|
+
) => Promise<SitePluginResult<TResult>>;
|
|
175
|
+
// Browser fallback — Stagehand + Steel session, acquired from the pool by core.
|
|
176
|
+
execute(
|
|
177
|
+
payload: TPayload,
|
|
178
|
+
session: BrowserSession,
|
|
179
|
+
context: SitePluginContext
|
|
180
|
+
): Promise<SitePluginResult<TResult>>;
|
|
181
|
+
// Async work is supported. Note: NOT called on CaptchaError or EmptyResultsError —
|
|
182
|
+
// p-retry skips onFailedAttempt for AbortError, so those abort paths bypass this hook.
|
|
183
|
+
onRetry?: (error: ScraperError, attempt: number) => void | Promise<void>;
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### SitePluginMeta — required fields
|
|
188
|
+
|
|
189
|
+
| Field | Type | Purpose |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `siteId` | `string` | Stable key used for routing (`/v1/<siteId>/run`) and audit rows |
|
|
192
|
+
| `displayName` | `string` | Human-readable label for logs and Swagger docs |
|
|
193
|
+
| `bodySchema` | `ZodTypeAny` | Request body schema — core validates before calling `execute()` |
|
|
194
|
+
| `responseSchema` | `ZodTypeAny` | Success response schema — drives Swagger output shape |
|
|
195
|
+
| `routeOverride?` | `string` | Override the full route path (legacy compatibility only) |
|
|
196
|
+
| `defaultBaseUrl?` | `string` | Fallback base URL when `config.scraper.siteBaseUrls[siteId]` is absent |
|
|
197
|
+
| `taskTimeoutMs?` | `number` | Override the pool's 60-minute per-task hang ceiling for this plugin only — set when the site's normal latency is well below the default and a faster failure is preferable |
|
|
198
|
+
| `apiVersion?` | `string` | Semver range targeting a plugin API version (e.g. `"^1.0.0"`); core disables the plugin on a major-version mismatch. Absent means "accept any version." |
|
|
199
|
+
| `extraRoutes?` | `readonly SitePluginExtraRoute[]` | Extra non-run routes (OTP trigger, resume, etc.) that core registers as authenticated Fastify routes at startup. See `SitePluginExtraRoute` in `src/site-plugin.ts`. |
|
|
200
|
+
|
|
201
|
+
### Full plugin skeleton (hot path + browser fallback)
|
|
202
|
+
|
|
203
|
+
`pnpm run recon:generate` produces this structure automatically. Use `createRateLimitedJsonClient()` for REST endpoints that send Chromium client-hint headers (the common case) and `createGraphqlClient()` for GraphQL endpoints — `recon:generate` selects the right one based on what it captured. The skeleton below illustrates the REST hot-path pattern; for GraphQL sites, `recon-generate` uses `createGraphqlClient` instead and inlines the captured query as a constant.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// src/sites/my-site/contract.ts
|
|
207
|
+
import { z } from "zod/v4";
|
|
208
|
+
import { createRateLimitedJsonClient } from "@/scraper/rate-limited-json-client";
|
|
209
|
+
import type { BrowserSession } from "@/scraper/session";
|
|
210
|
+
import type { SitePlugin, SitePluginContext, SitePluginResult } from "@/site-plugin";
|
|
211
|
+
import { runMySiteBrowserFlow } from "@/sites/my-site/flows/browser-flow";
|
|
212
|
+
|
|
213
|
+
// Generated: Zod schemas inferred from captured JSON — tighten z.unknown() fields as needed.
|
|
214
|
+
const MySiteResponseSchema = z.object({ data: z.object({ items: z.array(z.object({ id: z.string() })) }) });
|
|
215
|
+
const MySitePayloadSchema = z.object({ query: z.string().min(1) });
|
|
216
|
+
|
|
217
|
+
type MySitePayload = z.infer<typeof MySitePayloadSchema>;
|
|
218
|
+
type MySiteResponse = z.infer<typeof MySiteResponseSchema>;
|
|
219
|
+
|
|
220
|
+
// Generated: rate-limit ceiling (5 rps) + Chromium hints + site-specific headers from recon.
|
|
221
|
+
// Use createHttpClient() directly only when you need manual Bottleneck or header control.
|
|
222
|
+
const httpClient = createRateLimitedJsonClient({
|
|
223
|
+
minTimeMs: 200,
|
|
224
|
+
userAgent: "Mozilla/5.0 ...",
|
|
225
|
+
secChUa: '"Chromium";v="..."',
|
|
226
|
+
platform: "Linux",
|
|
227
|
+
extraHeaders: {
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
Accept: "application/json, */*",
|
|
230
|
+
},
|
|
231
|
+
schema: MySiteResponseSchema,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export const mySitePlugin: SitePlugin<MySitePayload, MySiteResponse> = {
|
|
235
|
+
meta: {
|
|
236
|
+
siteId: "my-site",
|
|
237
|
+
displayName: "My Site",
|
|
238
|
+
bodySchema: MySitePayloadSchema,
|
|
239
|
+
responseSchema: MySiteResponseSchema,
|
|
240
|
+
defaultBaseUrl: "https://my-site.com",
|
|
241
|
+
},
|
|
242
|
+
// Hot path: direct HTTP — no browser, no LLM tokens.
|
|
243
|
+
async executeHttp(payload: MySitePayload, context: SitePluginContext): Promise<SitePluginResult<MySiteResponse>> {
|
|
244
|
+
const data = await httpClient(`${context.baseUrl}/api/search`, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
body: JSON.stringify({ query: payload.query }),
|
|
247
|
+
});
|
|
248
|
+
return { data };
|
|
249
|
+
},
|
|
250
|
+
// Browser fallback: Stagehand + Steel — invoked automatically when hot path fails.
|
|
251
|
+
async execute(payload: MySitePayload, session: BrowserSession, context: SitePluginContext): Promise<SitePluginResult<MySiteResponse>> {
|
|
252
|
+
const raw = await runMySiteBrowserFlow(session.stagehand, context.baseUrl, payload.query);
|
|
253
|
+
return { data: raw };
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### The auditPayload hook
|
|
259
|
+
|
|
260
|
+
`SitePluginResult` accepts an optional `auditPayload` field alongside `data`:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
return {
|
|
264
|
+
data: responseData,
|
|
265
|
+
auditPayload: { query: payload.query, resultCount: responseData.items.length },
|
|
266
|
+
};
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
When `auditPayload` is present, core writes it — not `data` — to the submission-envelope telemetry record. Use this to strip PII or large blobs from the audit trail while keeping the full response in the API reply. When absent, `data` is written as-is.
|
|
270
|
+
|
|
271
|
+
### Static fixtures
|
|
272
|
+
|
|
273
|
+
If Phase 3b (auxiliary fixture detection) found static JSON endpoints (markets, currencies, labels), `recon:generate` copies them to `src/sites/<id>/fixtures/`. Load them at module init via `loadFixture()` — zero per-request overhead, fails fast on deploy if the fixture is missing or stale:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { z } from "zod/v4";
|
|
277
|
+
import { loadFixture } from "@/scraper/fixtures";
|
|
278
|
+
|
|
279
|
+
const MarketsSchema = z.array(z.object({ id: z.string(), name: z.string() }));
|
|
280
|
+
|
|
281
|
+
// Loaded synchronously at module init. Throws at startup if file is missing
|
|
282
|
+
// or shape drifted — surface fixture breakage on deploy, not on the first request.
|
|
283
|
+
const markets = loadFixture("my-site", "markets.json", MarketsSchema);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
See [docs/playbook.md — Phase 3b](./docs/playbook.md#3b--auxiliary-fixture-detection) for how fixtures are detected and when to use them.
|
|
287
|
+
|
|
288
|
+
### Register the plugin
|
|
289
|
+
|
|
290
|
+
**Out-of-tree (recommended for operator-owned plugins):** point `BARNACLE_PLUGINS` at the compiled plugin module — no core edits required:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
BARNACLE_PLUGINS=./plugins/my-site/dist/index.js pnpm start
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Barnacle validates the export at startup and registers `POST /v1/my-site/run` automatically. See the [Out-of-tree plugins](#out-of-tree-plugins) env var table for `BARNACLE_PLUGINS_STRICT` and `BARNACLE_PLUGINS_DIR`. A copyable, runnable template lives at [`examples/plugins/hello-site/`](./examples/plugins/hello-site/).
|
|
297
|
+
|
|
298
|
+
**Config-only (no TypeScript, no compile step):** a browser-flow plugin can be a single JSON manifest. Point `BARNACLE_PLUGINS` at a `*.plugin.json` file, or drop manifests into a directory named by `BARNACLE_PLUGINS_CONFIG_DIR`:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
BARNACLE_PLUGINS=./plugins/acme-jobs.plugin.json pnpm start
|
|
302
|
+
# or, for directory-drop discovery of every *.plugin.json:
|
|
303
|
+
BARNACLE_PLUGINS_CONFIG_DIR=./plugins pnpm start
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The manifest wears the Kubernetes-style `apiVersion` / `kind` / `metadata` / `spec` envelope, declares its request/response/extract shapes as **JSON Schema**, and lists the browser flow as data (the same self-heal step format the recon toolchain authors). Core reads it at startup and registers `POST /v1/acme-jobs/run` — no per-site code. A site needing the direct-HTTP hot path can reference a compiled `executeHttp` module via `spec.httpModule`. A copyable manifest lives at [`examples/plugins/acme-jobs.plugin.json`](./examples/plugins/acme-jobs.plugin.json).
|
|
307
|
+
|
|
308
|
+
The JSON Schema converter accepts a deliberately small subset — `object`, `string`, `number`, `integer`, `boolean`, `array` (with `items`), string `enum`, and `required` — and rejects anything else (e.g. `pattern`, `minLength`, `$ref`, `format` constraints) at load time. Flow steps interpolate request values with `{{ .request.FieldName }}`; a reference to a field the request schema does not declare fails loudly, while an optional declared field the caller omits splices as an empty string.
|
|
309
|
+
|
|
310
|
+
**In-tree (bundled built-ins only):** push to `BUILTIN_SITE_PLUGINS` in `src/plugins/discover.ts`:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { mySitePlugin } from "@/sites/my-site";
|
|
314
|
+
import { BUILTIN_SITE_PLUGINS } from "@/plugins/discover";
|
|
315
|
+
|
|
316
|
+
BUILTIN_SITE_PLUGINS.push(mySitePlugin as SitePlugin<unknown, unknown>);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`SITE_PLUGINS` in `src/plugins/loader.ts` is an alias for `BUILTIN_SITE_PLUGINS` kept for backwards compatibility; prefer the `discover.ts` import for new code.
|
|
320
|
+
|
|
321
|
+
Core registers `POST /v1/my-site/run` automatically at startup.
|
|
322
|
+
|
|
323
|
+
### Wire up the nightly smoke test
|
|
324
|
+
|
|
325
|
+
Add a step to `.github/workflows/smoke.yml`:
|
|
326
|
+
|
|
327
|
+
```yaml
|
|
328
|
+
- name: Run smoke test — my-site
|
|
329
|
+
if: steps.check-secrets.outputs.skip == 'false'
|
|
330
|
+
run: |
|
|
331
|
+
pnpm run smoke -- \
|
|
332
|
+
--site my-site \
|
|
333
|
+
--payload '{"query":"test"}' \
|
|
334
|
+
--host "$SMOKE_HOST" \
|
|
335
|
+
--fallback \
|
|
336
|
+
--response-schema src/sites/my-site/contract.ts
|
|
337
|
+
env:
|
|
338
|
+
API_KEY: ${{ secrets.SMOKE_API_KEY }}
|
|
339
|
+
SMOKE_HOST: ${{ secrets.SMOKE_HOST }}
|
|
340
|
+
NODE_ENV: production
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`--response-schema` points to a module whose **default export is a Zod schema**. The smoke test validates the full response body against it — not just the envelope shape — so any schema drift on the data payload fails the pipeline immediately.
|
|
344
|
+
|
|
345
|
+
`--fallback` additionally runs a second request via the Stagehand browser path. This catches Stagehand cache staleness: if the page DOM changed and the cached selector now points at the wrong element, the hot-path test passes but the fallback test fails — alerting you before the fallback is invoked in production.
|
|
346
|
+
|
|
347
|
+
### Maintenance loop
|
|
348
|
+
|
|
349
|
+
When the smoke test fails: re-run `pnpm run recon:browser` → diff `/tmp/recon/graphql/*<operationName>*.json` against `src/sites/<id>/contract.ts` → update query / headers / Zod schema → ship. See [docs/playbook.md](./docs/playbook.md#phase-6--drift-detection) for the full maintenance loop and change severity table.
|
|
350
|
+
|
|
351
|
+
## Runtime internals
|
|
352
|
+
|
|
353
|
+
### Hot-path fallback triggers
|
|
354
|
+
|
|
355
|
+
`dispatch()` (`src/plugins/loader.ts`) tries `executeHttp()` first. Which errors trigger the browser fallback and which don't:
|
|
356
|
+
|
|
357
|
+
| Hot-path error | Status | Triggers browser fallback? | Reason |
|
|
358
|
+
|---------------|--------|--------------------------|--------|
|
|
359
|
+
| `HttpSchemaError` | Any | **Yes** | Response shape drifted; browser may still work |
|
|
360
|
+
| `HttpBotChallengeError` | 401 / 403 | **Yes** | Residential proxy IP may get through |
|
|
361
|
+
| `HttpServerError` | 5xx | **Yes** | Server-side outage; recovery strategy is the same |
|
|
362
|
+
| `HttpRateLimitError` | 429 | **No** | A 429 means the configured rps ceiling is too high. Routing to the browser path would just hit the same ceiling and waste a Steel session. The right response is to lower the Bottleneck `minTime` in `contract.ts` and re-deploy. |
|
|
363
|
+
| `HttpUrlLockedError` | 429 | **No** | Oracle HCM returned `ORA_URL_LOCKED` — the requisition URL is locked at Oracle's end. Neither a retry nor a browser session can succeed; the caller must back off and surface a "retry later" state. |
|
|
364
|
+
| `OracleTokenExpiredError` | Any | **No** | Oracle HCM returned a plain-text `ORA_IRC_TOKEN_EXPIRED` sentinel. Retryable so p-retry keeps retrying, but distinct from `UnknownScraperError` so the encompass http-flow can catch exactly this class and re-mint the AccessCode before retrying. |
|
|
365
|
+
| `UnknownScraperError` | Any | **No** | Transient network failure or unclassified non-JSON response. `createHttpClient` retries up to 2 times internally; if all attempts fail, the error propagates as `ScrapeFailureError`. |
|
|
366
|
+
|
|
367
|
+
### Cache deduplication
|
|
368
|
+
|
|
369
|
+
`getCachedResponse()` checks the LRU cache first. On a miss, `getOrCreateInFlight()` registers a promise in an `inFlight` map before awaiting it — meaning concurrent identical requests all await the same upstream call rather than fanning out. First caller wins; all others coalesce onto its promise.
|
|
370
|
+
|
|
371
|
+
Cache key: `<endpoint>:<sha256(canonical payload)[:32]>` — the endpoint is a literal prefix; the hash covers only the canonical payload. Object key order and primitive array element order are normalized so `{a:1,b:2}` and `{b:2,a:1}` hit the same entry. Default TTL: 15 minutes (`CACHE_TTL_MS`). Max entries: 1000 (`CACHE_MAX_ENTRIES`). Only successful responses are cached; errors propagate and never poison the cache.
|
|
372
|
+
|
|
373
|
+
### Session pool and timeouts
|
|
374
|
+
|
|
375
|
+
`runWithSession()` (`src/scraper/pool.ts`) queues tasks through a `p-queue` bounded by `SESSION_POOL_SIZE` (default: 3). Sessions are created on demand — not pre-warmed — so Steel billing stays proportional to actual traffic.
|
|
376
|
+
|
|
377
|
+
**Per-task hang ceiling:** each queued task races against `TASK_TIMEOUT_MS` (`src/scraper/pool.ts`, **60 minutes** by default). A hung `execute()` — frozen CDP connection, infinite network wait — converts to `SessionTimeoutError`, which the retry policy handles by tearing down the broken session and creating a fresh one. The default is sized for long browser flows; shorten per-plugin via `SitePluginMeta.taskTimeoutMs`. This is a hang-recovery floor, not a p99 latency budget.
|
|
378
|
+
|
|
379
|
+
**Retry policy:** `withScraperRetry` (`src/scraper/retry.ts`) uses p-retry with `factor: 2`, `minTimeout: 500ms`, `maxTimeout: 5000ms`, `randomize: true`, and default `maxAttempts: 3`. `EmptyResultsError` and abort signals short-circuit retries; `SessionTimeoutError` triggers a one-time session restart between attempts.
|
|
380
|
+
|
|
381
|
+
**Graceful shutdown:** `drainPool()` is called during graceful shutdown — `SIGTERM`/`SIGINT` triggers `app.close()`, which fires Fastify's `onClose` hook, which calls `drainPool()`. It pauses new intake, waits up to 20 seconds for in-flight tasks to close their Steel sessions, then resolves. Without this, process exit leaves live sessions billing until Steel's own timeout.
|
|
382
|
+
|
|
383
|
+
### Viewport rotation
|
|
384
|
+
|
|
385
|
+
`createBrowserSession()` (`src/scraper/session.ts`) picks a random desktop viewport per session from: `1280×720`, `1366×768`, `1440×900`, `1920×1080`. A fixed pixel size is an easy bot-detection fingerprint; rotating it makes sessions harder to cluster.
|
|
386
|
+
|
|
387
|
+
### LLM routing: Anthropic vs. AWS Bedrock
|
|
388
|
+
|
|
389
|
+
By default, Stagehand calls the Anthropic API directly (`ANTHROPIC_API_KEY`). Set `USE_BEDROCK=true` to route through AWS Bedrock instead:
|
|
390
|
+
|
|
391
|
+
```bash
|
|
392
|
+
USE_BEDROCK=true
|
|
393
|
+
AWS_REGION=us-east-1
|
|
394
|
+
AWS_ACCESS_KEY_ID=...
|
|
395
|
+
AWS_SECRET_ACCESS_KEY=...
|
|
396
|
+
BEDROCK_MODEL=us.anthropic.claude-sonnet-4-6[1m] # default
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The `[1m]` suffix selects the 1-million-token context variant on Bedrock. Both paths run Stagehand with `serverCache: true` (server-side action cache to skip LLM inference on replay) and `selfHeal: false` (recon-browser owns its own verify-and-retry cascade; see `src/scraper/session.ts` for the rationale).
|
|
400
|
+
|
|
401
|
+
When using Anthropic directly (not Bedrock), the model is controlled by `STAGEHAND_MODEL` (default: `anthropic/claude-sonnet-4-6`).
|
|
402
|
+
|
|
403
|
+
## Observability
|
|
404
|
+
|
|
405
|
+
`GET /readyz` returns readiness status plus per-site drift-detection metrics exposed by `src/scraper/metrics.ts`:
|
|
406
|
+
|
|
407
|
+
```json
|
|
408
|
+
{
|
|
409
|
+
"status": "ready",
|
|
410
|
+
"checks": {
|
|
411
|
+
"database": { "ok": true },
|
|
412
|
+
"scraperCredentials": { "ok": true },
|
|
413
|
+
"scraperPool": { "ok": true, "detail": "depth=0" }
|
|
414
|
+
},
|
|
415
|
+
"stats": {
|
|
416
|
+
"scraperPool": { "size": 0, "pending": 0, "concurrency": 3 },
|
|
417
|
+
"cache": { "size": 12, "max": 1000, "inFlight": 0 },
|
|
418
|
+
"metrics": {
|
|
419
|
+
"my-site": {
|
|
420
|
+
"hotPathSuccess": 4821,
|
|
421
|
+
"fallbackActivations": 3,
|
|
422
|
+
"rateLimitRejections": 0,
|
|
423
|
+
"p95LatencyMs": 187
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
"telemetry": {
|
|
428
|
+
"currentRunFile": "/path/to/.barnacle/events/run-123.ndjson",
|
|
429
|
+
"currentRunFileSizeBytes": 4096,
|
|
430
|
+
"orphansRecovered": 0
|
|
431
|
+
},
|
|
432
|
+
"heal": {
|
|
433
|
+
"my-site": { "verdict": "SUCCESS", "bestPassRate": 0.95, "reportPath": "heal-out/my-site/healing-my-site.md" }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**What rising `fallbackActivations` means:** the hot path is failing and the browser fallback is absorbing traffic. Cost and latency rise while error rate stays flat — users don't notice yet, but you will on your bill. This is your signal to re-run recon.
|
|
439
|
+
|
|
440
|
+
**`p95LatencyMs`** is reservoir-sampled (Vitter's Algorithm R, capped at 1000 samples) over actual upstream round-trips. Cache hits are excluded — they're memory reads and must not bias the upstream latency signal.
|
|
441
|
+
|
|
442
|
+
See [docs/playbook.md](./docs/playbook.md#6b--metrics-signals-the-detection-ladder) for the full detection ladder.
|
|
443
|
+
|
|
444
|
+
### NDJSON telemetry files
|
|
445
|
+
|
|
446
|
+
Barnacle writes two append-only NDJSON files alongside its metrics:
|
|
447
|
+
|
|
448
|
+
| File | Default path | Purpose |
|
|
449
|
+
|------|-------------|---------|
|
|
450
|
+
| LLM call samples | `.barnacle/calls.ndjson` | One line per LLM/Stagehand call; feed to the `judge:llm` and `slm-self-heal` skills |
|
|
451
|
+
| Run event stream | `.barnacle/events/<runId>.ndjson` | Per-run event stream written by the event-stream subsystem; path surfaced in `/readyz` `telemetry.currentRunFile` |
|
|
452
|
+
|
|
453
|
+
#### LLM call sample schema
|
|
454
|
+
|
|
455
|
+
Every line in `.barnacle/calls.ndjson` is a JSON object with these fields (source: `src/api/schemas/telemetry.ts`):
|
|
456
|
+
|
|
457
|
+
| Field | Type | Description |
|
|
458
|
+
|-------|------|-------------|
|
|
459
|
+
| `callId` | `string` | UUID generated per call |
|
|
460
|
+
| `callType` | `string` | Which LLM call site produced this sample — see table below |
|
|
461
|
+
| `model` | `string` | Model identifier string passed to the SDK |
|
|
462
|
+
| `systemPrompt` | `string \| null` | System-prompt text, or `null` when absent |
|
|
463
|
+
| `userContent` | `string` | Full user-turn content |
|
|
464
|
+
| `responseContent` | `string \| null` | Raw response text, or `null` on SDK error |
|
|
465
|
+
| `parsedOk` | `boolean` | Whether the response was successfully parsed into the expected schema |
|
|
466
|
+
| `inputTokens` | `number \| null` | Input token count from SDK usage metadata |
|
|
467
|
+
| `outputTokens` | `number \| null` | Output token count from SDK usage metadata |
|
|
468
|
+
| `latencyMs` | `number \| null` | Wall-clock latency of the SDK call in milliseconds |
|
|
469
|
+
| `success` | `boolean` | Whether the call site considered the call successful end-to-end |
|
|
470
|
+
| `ts` | `string` | ISO-8601 timestamp at write time |
|
|
471
|
+
|
|
472
|
+
#### Call types
|
|
473
|
+
|
|
474
|
+
`callType` is a stable string constant defined in `src/lib/telemetry/call-types.ts`:
|
|
475
|
+
|
|
476
|
+
| `callType` | Source | When emitted |
|
|
477
|
+
|------------|--------|--------------|
|
|
478
|
+
| `recon-rephrase` | `src/scripts/recon-browser.ts` | Attempt-4 rephrase inside the recon-browser step-healing cascade — Anthropic SDK is asked to reword the failing step |
|
|
479
|
+
| `recon-replan` | `src/scripts/recon-browser.ts` | Global replan after a step terminally fails — Claude rewrites the remaining flow tail |
|
|
480
|
+
| `recon-flow-patch` | `src/scripts/recon-heal.ts` | Patch proposal from the recon-flow-patch-generator during the `recon-heal` self-healing loop |
|
|
481
|
+
| `llm-prompt-patch` | `src/scripts/llm-heal.ts` | Patch proposal from the llm-call-patch-generator during the `llm-heal` self-healing loop |
|
|
482
|
+
|
|
483
|
+
#### Tailing call samples with jq
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
# Stream all LLM call samples as they arrive
|
|
487
|
+
tail -f .barnacle/calls.ndjson | jq '.'
|
|
488
|
+
|
|
489
|
+
# Filter to a specific call type
|
|
490
|
+
tail -f .barnacle/calls.ndjson | jq 'select(.callType == "recon-rephrase")'
|
|
491
|
+
|
|
492
|
+
# Show only failures
|
|
493
|
+
tail -f .barnacle/calls.ndjson | jq 'select(.success == false) | {callId, callType, latencyMs}'
|
|
494
|
+
|
|
495
|
+
# Token usage summary by call type
|
|
496
|
+
jq -s 'group_by(.callType) | map({callType: .[0].callType, totalInputTokens: map(.inputTokens // 0) | add, totalOutputTokens: map(.outputTokens // 0) | add, n: length})' .barnacle/calls.ndjson
|
|
497
|
+
|
|
498
|
+
# Tail the current run event stream (path from /readyz telemetry.currentRunFile)
|
|
499
|
+
tail -f .barnacle/events/<runId>.ndjson | jq '.'
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Environment variables
|
|
503
|
+
|
|
504
|
+
All variables are read once at process start. Required variables cause the
|
|
505
|
+
process to exit on missing values; optional ones have safe defaults.
|
|
506
|
+
|
|
507
|
+
### Application
|
|
508
|
+
|
|
509
|
+
| Variable | Default | Required | Purpose |
|
|
510
|
+
|----------|---------|----------|---------|
|
|
511
|
+
| `APP_NAME` | `barnacle` | No | Application name used in logs |
|
|
512
|
+
| `NODE_ENV` | `development` | No | `development` / `production` / `test` |
|
|
513
|
+
| `PORT` | `3000` | No | HTTP listen port |
|
|
514
|
+
| `HOST` | `0.0.0.0` | No | HTTP listen address |
|
|
515
|
+
| `LOG_LEVEL` | `info` | No | Pino log level (`debug`, `info`, `warn`, `error`) |
|
|
516
|
+
|
|
517
|
+
### Auth
|
|
518
|
+
|
|
519
|
+
| Variable | Default | Required | Purpose |
|
|
520
|
+
|----------|---------|----------|---------|
|
|
521
|
+
| `API_KEYS_HASHED` | `""` | Yes (prod) | Comma-separated bcrypt hashes of plaintext bearer tokens. See [Generating an API key](#generating-an-api-key). |
|
|
522
|
+
| `DEV_BYPASS_AUTH` | `false` | No | Skip auth entirely. Local dev only — **never set in production**. |
|
|
523
|
+
|
|
524
|
+
### Browser automation (Steel + Stagehand)
|
|
525
|
+
|
|
526
|
+
| Variable | Default | Required | Purpose |
|
|
527
|
+
|----------|---------|----------|---------|
|
|
528
|
+
| `STEEL_API_KEY` | — | **Yes** | Steel account API key. Required for all browser automation. |
|
|
529
|
+
| `ANTHROPIC_API_KEY` | — | Yes (if not using Bedrock) | Anthropic API key for Stagehand's LLM calls. |
|
|
530
|
+
| `STAGEHAND_MODEL` | `anthropic/claude-sonnet-4-6` | No | Stagehand model. Use the `anthropic/` prefix — Stagehand 2.x's model map is stale and the prefix routes through AI-SDK's fallback path. |
|
|
531
|
+
| `SCRAPER_PROXY_TYPE` | `residential` | No | `residential` (paid Steel tiers) or `none` (free tier — Steel rejects `useProxy=true` on hobby plans). |
|
|
532
|
+
| `SCRAPER_SOLVE_CAPTCHA` | `true` | No | Enable Steel's built-in CAPTCHA solver. Requires a paid plan; set `false` on the free tier. |
|
|
533
|
+
| `SESSION_POOL_SIZE` | `3` | No | Maximum concurrent Steel browser sessions. |
|
|
534
|
+
| `SCRAPER_MIN_ACTION_DELAY_MS` | `500` | No | Minimum delay between scraper actions (ms). Jitter applied on top. |
|
|
535
|
+
| `SCRAPER_MAX_ACTION_DELAY_MS` | `1500` | No | Maximum delay between scraper actions (ms). |
|
|
536
|
+
| `STAGEHAND_API_TIMEOUT_MS` | `120000` | No | Anthropic SDK request timeout (ms). Raise on slow network paths to `api.anthropic.com`. |
|
|
537
|
+
| `STAGEHAND_CONNECT_TIMEOUT_MS` | `120000` | No | TCP connect timeout for all outbound fetch calls (ms). Raised from the undici default of 10 s to match `STAGEHAND_API_TIMEOUT_MS`. |
|
|
538
|
+
| `STEEL_SESSION_TIMEOUT_MS` | `3600000` | No | Steel session wall-clock timeout (ms). Default is 1 hour; lower on plans that enforce shorter maximum session durations. |
|
|
539
|
+
|
|
540
|
+
### AWS Bedrock (alternative LLM provider)
|
|
541
|
+
|
|
542
|
+
Set `USE_BEDROCK=true` to route Stagehand's LLM calls through AWS Bedrock
|
|
543
|
+
instead of the Anthropic API. When enabled, `ANTHROPIC_API_KEY` is not needed.
|
|
544
|
+
AWS credentials resolve in standard SDK order: explicit vars → ECS task role →
|
|
545
|
+
EC2 instance profile → `~/.aws/credentials`.
|
|
546
|
+
|
|
547
|
+
| Variable | Default | Required | Purpose |
|
|
548
|
+
|----------|---------|----------|---------|
|
|
549
|
+
| `USE_BEDROCK` | `false` | No | Master switch — routes LLM calls through Bedrock when `true`. |
|
|
550
|
+
| `AWS_REGION` | `us-east-1` | No | AWS region for Bedrock calls. |
|
|
551
|
+
| `AWS_ACCESS_KEY_ID` | — | No | Explicit AWS access key (leave blank for ambient IAM). |
|
|
552
|
+
| `AWS_SECRET_ACCESS_KEY` | — | No | Explicit AWS secret key. |
|
|
553
|
+
| `AWS_SESSION_TOKEN` | — | No | Required only for temporary STS credentials. |
|
|
554
|
+
| `BEDROCK_MODEL` | `us.anthropic.claude-sonnet-4-6[1m]` | No | Bedrock cross-region inference profile ID. The `us.` prefix enables automatic cross-region routing; the `[1m]` suffix selects the 1M-token context variant. |
|
|
555
|
+
|
|
556
|
+
### Cache
|
|
557
|
+
|
|
558
|
+
| Variable | Default | Purpose |
|
|
559
|
+
|----------|---------|---------|
|
|
560
|
+
| `CACHE_TTL_MS` | `900000` (15 min) | LRU response cache TTL. Cached responses skip the target API entirely. |
|
|
561
|
+
| `CACHE_MAX_ENTRIES` | `1000` | Maximum entries in the LRU cache. |
|
|
562
|
+
|
|
563
|
+
### Rate limiting (inbound)
|
|
564
|
+
|
|
565
|
+
These limit traffic *to* Barnacle's own API. See per-plugin Bottleneck config
|
|
566
|
+
in each `contract.ts` for outbound rate limits to target sites.
|
|
567
|
+
|
|
568
|
+
| Variable | Default | Purpose |
|
|
569
|
+
|----------|---------|---------|
|
|
570
|
+
| `RATE_LIMIT_MAX` | `120` | Max requests per window per API key (or IP for unauthenticated traffic). |
|
|
571
|
+
| `RATE_LIMIT_WINDOW_MS` | `60000` (1 min) | Rate limit window duration. |
|
|
572
|
+
| `TRUST_PROXY` | `true` | Trust `X-Forwarded-For` when behind a reverse proxy. Set `false` for bare-metal deploys to prevent spoofing. |
|
|
573
|
+
|
|
574
|
+
### Readiness / observability
|
|
575
|
+
|
|
576
|
+
| Variable | Default | Purpose |
|
|
577
|
+
|----------|---------|---------|
|
|
578
|
+
| `READINESS_QUEUE_THRESHOLD` | `20` | `/readyz` returns 503 when scraper queue depth exceeds this. Lets orchestrators shed load before the pool is saturated. |
|
|
579
|
+
| `ENABLE_DOCS` | `false` | Serve Swagger UI at `/docs`. Disable in production. |
|
|
580
|
+
|
|
581
|
+
### Telemetry
|
|
582
|
+
|
|
583
|
+
| Variable | Default | Purpose |
|
|
584
|
+
|----------|---------|---------|
|
|
585
|
+
| `TELEMETRY_ENABLED` | `true` | Master switch — set `false` to disable all NDJSON telemetry writes. |
|
|
586
|
+
| `TELEMETRY_EVENTS_DIR` | `.barnacle/events` | Directory for per-run NDJSON event stream files (`<eventsDir>/<runId>.ndjson`). |
|
|
587
|
+
| `CALLS_NDJSON_PATH` | `.barnacle/calls.ndjson` | Append-only NDJSON sink for LLM/Stagehand call samples. One line per call; feed to the judge and self-heal skills. |
|
|
588
|
+
| `SUBMISSIONS_NDJSON_PATH` | `.barnacle/submissions.ndjson` | Append-only NDJSON sink for dispatch submission envelopes. One line per plugin invocation captures siteId, requestId, inbound payload, status, audit payload, and duration — the durable source-of-truth for "what did we submit for jobId X and did it succeed." |
|
|
589
|
+
| `TELEMETRY_MAX_FILE_SIZE_BYTES` | `104857600` (100 MB) | Rotate/drop the calls NDJSON once it exceeds this byte count. |
|
|
590
|
+
| `TELEMETRY_MAX_RETENTION_MS` | `2592000000` (30 days) | Drop event-stream files older than this many milliseconds. |
|
|
591
|
+
| `TELEMETRY_S3_BUCKET` | — | Optional — destination bucket for the buffered S3 telemetry mirror. Sink is entirely inert (no client, no network calls) when unset. Credentials/region resolve the same way as Bedrock (`AWS_REGION`, standard SDK credential order). |
|
|
592
|
+
| `TELEMETRY_S3_PREFIX` | `telemetry` | Key prefix for uploaded NDJSON objects (`<prefix>/<calls\|submissions>/<date>/...`). |
|
|
593
|
+
| `TELEMETRY_S3_FLUSH_INTERVAL_MS` | `60000` | How often buffered lines are flushed to S3. |
|
|
594
|
+
| `TELEMETRY_S3_MAX_BUFFER_LINES` | `500` | Threshold-flush trigger — flush early if either buffer exceeds this many lines, ahead of the next scheduled interval. |
|
|
595
|
+
|
|
596
|
+
### LLM judging
|
|
597
|
+
|
|
598
|
+
| Variable | Default | Purpose |
|
|
599
|
+
|----------|---------|---------|
|
|
600
|
+
| `JUDGE_MODEL` | `us.anthropic.claude-sonnet-4-6[1m]` | Anthropic model used by the judge script. Reuses Bedrock creds via the cross-region inference profile. |
|
|
601
|
+
| `JUDGE_TEMPERATURE` | `0.2` | Sampling temperature for judge LLM calls. Keep low (≤ 0.3) for deterministic verdicts. |
|
|
602
|
+
| `JUDGE_BATCH_SIZE` | `10` | Number of call samples sent to the judge in one LLM request. |
|
|
603
|
+
| `JUDGE_TIMEOUT_MS` | `120000` (2 min) | Anthropic SDK request timeout for judge calls. |
|
|
604
|
+
|
|
605
|
+
### Self-heal
|
|
606
|
+
|
|
607
|
+
| Variable | Default | Purpose |
|
|
608
|
+
|----------|---------|---------|
|
|
609
|
+
| `SELFHEAL_MAX_ITERATIONS` | `5` | Maximum patch→replay→score iterations before BUDGET_EXHAUSTED. |
|
|
610
|
+
| `SELFHEAL_N_REPLAYS` | `5` | Number of replay runs per iteration arm. |
|
|
611
|
+
| `SELFHEAL_SUCCESS_THRESHOLD` | `0.9` | Minimum pass rate (0–1) to declare SUCCESS and stop iterating. |
|
|
612
|
+
| `SELFHEAL_PLATEAU_WINDOW` | `3` | Consecutive iterations below `SELFHEAL_PLATEAU_DELTA` that triggers PLATEAUED. |
|
|
613
|
+
| `SELFHEAL_PLATEAU_DELTA` | `0.03` | Minimum absolute pass-rate improvement per iteration to count as progress. |
|
|
614
|
+
| `SELFHEAL_TIMEOUT_MS` | `60000` (1 min) | Per-replay LLM request timeout. |
|
|
615
|
+
|
|
616
|
+
### Per-site base URL overrides
|
|
617
|
+
|
|
618
|
+
Set `BARNACLE_SITE_<UPPERCASE_SITE_ID>_BASE_URL` to override a plugin's
|
|
619
|
+
`defaultBaseUrl` without source changes. Underscores in the env key map to
|
|
620
|
+
hyphens in the `siteId`:
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
BARNACLE_SITE_MY_SHOP_BASE_URL="https://staging.my-shop.com" # overrides plugin `my-shop`
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Out-of-tree plugins
|
|
627
|
+
|
|
628
|
+
| Variable | Default | Purpose |
|
|
629
|
+
|----------|---------|---------|
|
|
630
|
+
| `BARNACLE_PLUGINS` | `""` | Comma-separated list of plugin specifiers to load at startup — relative paths (`./plugins/acme`) or package names (`@acme/barnacle-plugin`). Empty by default (built-ins only). |
|
|
631
|
+
| `BARNACLE_PLUGINS_STRICT` | `false` | When `true`, any plugin that fails to load aborts the process instead of producing a disabled record. |
|
|
632
|
+
| `BARNACLE_PLUGINS_DIR` | `process.cwd()` | Base directory used to resolve relative specifiers and locate the operator's `node_modules`. Defaults to wherever the binary is run — not the installed Barnacle package root. |
|
|
633
|
+
| `BARNACLE_PLUGINS_CONFIG_DIR` | _(unset)_ | Directory scanned at startup for `*.plugin.json` config manifests, each loaded as a config-only plugin. Lets operators register sites by dropping a JSON file in a directory instead of editing `BARNACLE_PLUGINS`. An unreadable directory is logged and skipped — it never crashes boot. |
|
|
634
|
+
|
|
635
|
+
**Resolution rule:** a specifier starting with `.` or `/` is treated as a filesystem path resolved relative to `BARNACLE_PLUGINS_DIR`. Anything else is treated as an npm package name and resolved via `require.resolve` against the operator's own `node_modules` inside `BARNACLE_PLUGINS_DIR`.
|
|
636
|
+
|
|
637
|
+
**Failure policy:** by default (non-strict), a plugin that fails to load is logged at `warn` level and recorded as `"disabled"` in the load report — the server still boots with the remaining plugins. Set `BARNACLE_PLUGINS_STRICT=true` to abort startup on any load failure instead.
|
|
638
|
+
|
|
639
|
+
**`zod/v4` requirement for plugin authors:** import Zod as `import { z } from "zod/v4"` in your plugin, not as bare `"zod"`. Barnacle uses `fastify-type-provider-zod` which compiles routes against core's own zod instance; a plugin schema built against a different zod import may pass load-time validation but fail at route registration.
|
|
640
|
+
|
|
641
|
+
`GET /v1/plugins` (authenticated) returns the full plugin load report — one record per built-in and out-of-tree specifier — including `siteId`, `displayName`, `route`, `specifier`, `resolvedPath`, `apiVersion`, `status` (`"loaded"` or `"disabled"`), and an optional `reason` when disabled. Requires a valid `Authorization: Bearer <token>` header (reveals filesystem paths, so it is separate from the auth-free `/healthz`/`/readyz` probes).
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## Usage
|
|
646
|
+
|
|
647
|
+
### Prerequisites
|
|
648
|
+
|
|
649
|
+
- Node.js 22+
|
|
650
|
+
- pnpm 10.4.1
|
|
651
|
+
- A Steel account (`STEEL_API_KEY`) for managed browser sessions
|
|
652
|
+
- An Anthropic key (`ANTHROPIC_API_KEY`) for Stagehand's LLM calls, **or** AWS Bedrock (`USE_BEDROCK=true` + AWS credentials) — see `.env.example` for details
|
|
653
|
+
|
|
654
|
+
### Install
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
pnpm install
|
|
658
|
+
cp .env.example .env # fill in STEEL_API_KEY and either ANTHROPIC_API_KEY or Bedrock creds
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Generating an API key
|
|
662
|
+
|
|
663
|
+
Barnacle validates every request using bcrypt-hashed bearer tokens stored in
|
|
664
|
+
`API_KEYS_HASHED`. To create one:
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# 1. Generate a random plaintext key — save this, you'll send it as Authorization: Bearer <key>
|
|
668
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
669
|
+
|
|
670
|
+
# 2. Hash it (bcrypt cost factor 10) — paste the output into API_KEYS_HASHED
|
|
671
|
+
node -e "const b=require('bcryptjs');b.hash(process.argv[1],10,(e,h)=>console.log(h))" <your-key>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Comma-separate multiple hashes in `API_KEYS_HASHED` to support key rotation.
|
|
675
|
+
|
|
676
|
+
For local development, set `DEV_BYPASS_AUTH=true` in `.env` to skip auth
|
|
677
|
+
entirely — never set this in production.
|
|
678
|
+
|
|
679
|
+
### Dev
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
pnpm run dev
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Build for production
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
pnpm run build
|
|
689
|
+
pnpm start
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Try it
|
|
693
|
+
|
|
694
|
+
Barnacle boots with the built-in plugins registered (see `BUILTIN_SITE_PLUGINS` in `src/plugins/discover.ts`). Follow [Adding a New Site](#adding-a-new-site--the-recon-playbook) above to build and register a plugin; core will register `POST /v1/<your-siteId>/run` automatically at startup.
|
|
695
|
+
|
|
696
|
+
With the dev server running (`pnpm run dev`), confirm the server is up:
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
curl -s http://localhost:3000/health | jq .
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Once a plugin is registered, every response follows the same envelope shape. The status block is always present; the plugin's `responseSchema` fields are spread alongside it at the root:
|
|
703
|
+
|
|
704
|
+
```json
|
|
705
|
+
{
|
|
706
|
+
"status": {
|
|
707
|
+
"httpStatus": "OK",
|
|
708
|
+
"dateTime": "2025-05-16T12:00:00.000Z",
|
|
709
|
+
"details": []
|
|
710
|
+
},
|
|
711
|
+
"items": []
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
The envelope is a **flat merge**, not nested — `status` lives at the root and the plugin's response fields are spread alongside it (`src/api/helpers/envelope.ts:8-25`). Parse as `{ status, ...pluginData }`, not `{ status, data: pluginData }`.
|
|
716
|
+
|
|
717
|
+
Every response — success or error — uses the same envelope shape so clients share a single parser. Error details appear in `status.details[]` with numeric codes:
|
|
718
|
+
|
|
719
|
+
| Code | Name | When |
|
|
720
|
+
|------|------|------|
|
|
721
|
+
| 1000 | `PARTIAL_CONTENT_SUCCESS` | Partial data returned |
|
|
722
|
+
| 1001 | `DECODING_ERROR` | Request body could not be parsed |
|
|
723
|
+
| 1002 | `FIELD_VIOLATION` | Schema validation failure on a field |
|
|
724
|
+
| 1003 | `EMPTY_REQUEST` | Request body was missing or empty |
|
|
725
|
+
| 1004 | `AUTHORIZATION_ERROR` | Bearer token missing or invalid |
|
|
726
|
+
| 1005 | `RESOURCE_NOT_FOUND` | Requested resource does not exist |
|
|
727
|
+
| 1006 | `INDEX_NOT_FOUND` | Internal index lookup failed |
|
|
728
|
+
| 1007 | `CLIENT_CALL_ERROR` | Downstream client call failed |
|
|
729
|
+
| 1008 | `GENERIC_ERROR` | Unclassified server error |
|
|
730
|
+
| 1009 | `EXTRA_DETAIL` | Supplemental detail entry (informational) |
|
|
731
|
+
| 1010 | `THROTTLED_REQUEST` | Rate limit exceeded (hot path 429) |
|
|
732
|
+
| 1011 | `TIME_OUT` | Request timed out |
|
|
733
|
+
| 2003 | `SCRAPE_FAILURE` | Browser automation failed after retries |
|
|
734
|
+
| 2004 | `CAPTCHA_ENCOUNTERED` | CAPTCHA challenge could not be resolved |
|
|
735
|
+
| 2005 | `EMPTY_RESULTS` | Scrape succeeded but returned no data |
|
|
736
|
+
| 2006 | `VERIFICATION_TRIGGER_FAILED` | OTP trigger to Oracle HCM failed |
|
|
737
|
+
| 2007 | `RESUME_INVALID_OTP` | Provided OTP was rejected by Oracle HCM |
|
|
738
|
+
| 2008 | `URL_LOCKED` | Upstream vendor locked the target URL; back off and retry later |
|
|
739
|
+
|
|
740
|
+
Full definitions: `src/api/schemas/common.ts`.
|
|
741
|
+
|
|
742
|
+
**How scraper exceptions map to API codes** (`src/plugins/loader.ts:149-153`):
|
|
743
|
+
|
|
744
|
+
- `CaptchaError` → `2004 CAPTCHA_ENCOUNTERED`
|
|
745
|
+
- `EmptyResultsError` → `2005 EMPTY_RESULTS`
|
|
746
|
+
- `HttpRateLimitError` → `1010 THROTTLED_REQUEST` (no browser fallback)
|
|
747
|
+
- `HttpUrlLockedError` → `2008 URL_LOCKED` (no browser fallback; distinct from rate-limit for metrics)
|
|
748
|
+
- Any other `ScraperError` → `2003 SCRAPE_FAILURE`
|
|
749
|
+
- Task exceeded `TASK_TIMEOUT_MS` → `1011 TIME_OUT`
|
|
750
|
+
|
|
751
|
+
## Endpoints
|
|
752
|
+
|
|
753
|
+
Each registered plugin exposes a POST route following the default convention:
|
|
754
|
+
`POST /v1/<siteId>/run`. When the hot path detects that required applicant
|
|
755
|
+
answers are absent (e.g. Gender, Degree, EducationLevel, SignatureFullName) or
|
|
756
|
+
a repeat-applicant OTP challenge, `/run` returns HTTP 200 with
|
|
757
|
+
`{ needsUserInfo: true, missingFields: [{ field, question }], requiresOtp }`
|
|
758
|
+
instead of a submission result, so Vivian can collect the gaps and hand back.
|
|
759
|
+
|
|
760
|
+
Plugin-specific extra routes (registered via `meta.extraRoutes` in each plugin):
|
|
761
|
+
|
|
762
|
+
Encompass Health:
|
|
763
|
+
- `POST /v1/encompasshealth/trigger-otp` — body `{ offerId, email }`; triggers Oracle HCM to email an OTP to a repeat applicant; returns `{ success: true }` or a `2006 VERIFICATION_TRIGGER_FAILED` error envelope
|
|
764
|
+
- `POST /v1/encompasshealth/resume` — body = the full original candidate payload plus `collectedData` and `otpCode`; re-runs the hot path with the collected answers and/or OTP; returns the same `{ verified }` envelope as `/run`, or `2007 RESUME_INVALID_OTP` if the OTP is rejected
|
|
765
|
+
|
|
766
|
+
Appcast:
|
|
767
|
+
- `POST /v1/appcast/resume` — body = the full original candidate payload plus `collectedData` (no `otpCode` — Appcast has no OTP challenge); re-runs the hot path with the chat-collected answers merged in; returns the same `{ verified, response }` envelope as `/run`
|
|
768
|
+
|
|
769
|
+
Operational routes:
|
|
770
|
+
- `GET /healthz` — liveness probe
|
|
771
|
+
- `GET /readyz` — readiness probe (checks scraper credentials, queue depth)
|
|
772
|
+
- `GET /docs` — Swagger UI (when `ENABLE_DOCS=true`)
|
|
773
|
+
- `GET /v1/plugins` — authenticated plugin load report (see [Out-of-tree plugins](#out-of-tree-plugins))
|
|
774
|
+
|
|
775
|
+
## Commands
|
|
776
|
+
|
|
777
|
+
| Command | What it does |
|
|
778
|
+
|---------|--------------|
|
|
779
|
+
| `pnpm run dev` | `tsx watch --env-file=.env src/server.ts` with hot reload |
|
|
780
|
+
| `pnpm run build` | compile to `dist/` (tsc + path alias rewriting + copy `src/sites/` fixtures and `src/testing/fixtures`) |
|
|
781
|
+
| `pnpm start` | `node dist/server.js` |
|
|
782
|
+
| `pnpm run typecheck` | strict TS noEmit |
|
|
783
|
+
| `pnpm run lint` / `lint:fix` | Biome |
|
|
784
|
+
| `pnpm run test` | Vitest unit + integration |
|
|
785
|
+
| `pnpm test src/scraper/fixtures.test.ts` | Run a single test file (NEVER use `--` before the filter) |
|
|
786
|
+
| `pnpm run test:watch` | Vitest in watch mode (re-runs on file changes) |
|
|
787
|
+
| `pnpm run test:coverage` | Vitest with v8 coverage report |
|
|
788
|
+
| `pnpm run format` | Biome format write |
|
|
789
|
+
| `pnpm run recon:browser` | Phase 1 — drive browser + capture API calls |
|
|
790
|
+
| `pnpm run recon:http` | Phases 2–3 — replay, introspect, probe rate limits |
|
|
791
|
+
| `pnpm run recon:generate -- --site-id <id>` | Phase 4 — generate complete plugin from artifacts |
|
|
792
|
+
| `pnpm run recon:summarize -- --site-id <id>` | Phase 4 (optional) — write human-readable findings doc |
|
|
793
|
+
| `pnpm run recon:heal -- --site-id <id> --url <url>` | Self-heal a failing recon flow without modifying the source file |
|
|
794
|
+
| `pnpm run smoke -- --site <id> --payload '...'` | Phase 6 — run nightly drift-detection smoke test |
|
|
795
|
+
| `pnpm run judge:llm -- --calls-ndjson <path> --call-type <type>` | Score captured LLM calls on a three-dimensional rubric; writes a verdict JSON to `judge-out/` |
|
|
796
|
+
| `pnpm run heal:llm -- --verdict-path <path> --call-type <type>` | Self-heal a failing prompt template: iterate patch→replay→score, write `healing-<callType>.md` with the best patch — production prompts are never modified |
|
|
797
|
+
|
|
798
|
+
## Architecture
|
|
799
|
+
|
|
800
|
+
```
|
|
801
|
+
src/
|
|
802
|
+
├── server.ts # Fastify bootstrap — calls loadAllPlugins(), registerRoutes(), site-agnostic
|
|
803
|
+
├── site-plugin.ts # SitePlugin<TInput,TOutput> interface (engine contract)
|
|
804
|
+
├── config.ts # frozen env-typed config singleton
|
|
805
|
+
├── plugins/
|
|
806
|
+
│ ├── loader.ts # dispatch(), registerRoutes(app, cfg, plugins)
|
|
807
|
+
│ └── discover.ts # BUILTIN_SITE_PLUGINS, loadAllPlugins(), loadPlugins()
|
|
808
|
+
├── sites/
|
|
809
|
+
│ ├── _shared/ # branch-local cross-plugin guards (coverage-expectations.test.ts)
|
|
810
|
+
│ └── <site-id>/ # one directory per registered plugin
|
|
811
|
+
├── api/
|
|
812
|
+
│ ├── plugins/ # auth, error-handler, request-context
|
|
813
|
+
│ ├── routes/ # health
|
|
814
|
+
│ ├── schemas/ # common envelope schemas; LLM telemetry + judge-verdict schemas
|
|
815
|
+
│ ├── helpers/envelope.ts # success envelope builder
|
|
816
|
+
│ └── errors.ts # error hierarchy + envelope builder
|
|
817
|
+
├── scraper/
|
|
818
|
+
│ ├── session.ts # Steel + Stagehand session factory
|
|
819
|
+
│ ├── pool.ts # p-queue over createBrowserSession
|
|
820
|
+
│ ├── throttle.ts # Bottleneck limiter + jitter
|
|
821
|
+
│ ├── retry.ts # p-retry + failure classification
|
|
822
|
+
│ ├── errors.ts # typed scraper error hierarchy
|
|
823
|
+
│ ├── http-client.ts # typed fetch wrapper (hot path)
|
|
824
|
+
│ ├── rate-limited-json-client.ts # factory: Bottleneck + chromiumClientHints + createHttpClient in one call — prefer this over the three-step scaffold for Chromium-hint plugins
|
|
825
|
+
│ ├── http-status-classifier.ts # pure status→ScraperError classifier for raw-fetch callers
|
|
826
|
+
│ ├── raw-fetch.ts # site-agnostic undici scaffold: network-error wrap, onResponse hook, optional classifyHttpStatus (skipClassify for callers that classify manually)
|
|
827
|
+
│ ├── graphql-client.ts # GraphQL POST wrapper
|
|
828
|
+
│ ├── metrics.ts # drift-detection counters
|
|
829
|
+
│ ├── fixtures.ts # static JSON fixture loader
|
|
830
|
+
│ ├── navigate.ts # shared awaitActivePage + goto(networkidle) helper
|
|
831
|
+
│ ├── behavioral-signals.ts # CDP synthetic mouse-move + scroll dispatcher for bot-detection warmup
|
|
832
|
+
│ ├── session-warmup.ts # generic pRetry browser-session runner: acquire → callback → close, with caller-supplied exhaustion mapping
|
|
833
|
+
│ └── require-response-field.ts # shared helpers for extracting required fields from HTTP response objects (HttpSchemaError on missing/null)
|
|
834
|
+
├── cache/
|
|
835
|
+
│ ├── response-cache.ts # lru-cache wrapper for deduplicating concurrent identical scraper requests
|
|
836
|
+
│ └── keyed-ttl-cache.ts # generic per-key TTL + single-flight coalescing cache factory
|
|
837
|
+
├── lib/ # logging, env, bedrock, db client, multipart, option-matcher, chromium-client-hints, telemetry/
|
|
838
|
+
├── scripts/ # recon-browser, recon-http, recon-generate, recon-summarize, recon-heal, recon-shared, smoke-test, judge-llm-batch, llm-heal
|
|
839
|
+
├── testing/
|
|
840
|
+
│ ├── integration-runner.ts # site-agnostic scaffold for integration tests (allocate inbox → dispatch → poll)
|
|
841
|
+
│ ├── mock-fetch-response.ts # shared undici-compatible Response stub factory for flow tests that mock fetch
|
|
842
|
+
│ ├── replay-integration-suite.ts # generic describe.skipIf/it.each scaffold; eliminates per-site integration boilerplate
|
|
843
|
+
│ ├── contract-parity-suite.ts # offline schema-parity scaffold; one-call drop-in for accept + rejection-case coverage
|
|
844
|
+
│ ├── coverage-guard-suite.ts # registry-driven structural guard; asserts contract.parity.test.ts exists per registered plugin
|
|
845
|
+
│ ├── batch-email-confirmation.ts # two-phase batch runner: submit jobs → poll inboxes (site-agnostic)
|
|
846
|
+
│ └── batch-report.ts # markdown table renderer for batch-test verdicts
|
|
847
|
+
└── types/
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Library choices** (battle-tested — no custom reinventions):
|
|
851
|
+
|
|
852
|
+
- API server: [`fastify`](https://fastify.dev/) + helmet + compress + rate-limit + swagger
|
|
853
|
+
- Schema: [`zod`](https://zod.dev/) via `fastify-type-provider-zod`
|
|
854
|
+
- Browser automation: [`@browserbasehq/stagehand`](https://github.com/browserbase/stagehand) + [`steel-sdk`](https://steel.dev)
|
|
855
|
+
- Concurrency: [`p-queue`](https://github.com/sindresorhus/p-queue), [`p-retry`](https://github.com/sindresorhus/p-retry), [`bottleneck`](https://github.com/SGrondin/bottleneck)
|
|
856
|
+
- Caching: [`lru-cache`](https://github.com/isaacs/node-lru-cache)
|
|
857
|
+
- Logging: [`pino`](https://github.com/pinojs/pino) with CloudWatch 256KB splitting + sensitive-field redaction
|
|
858
|
+
|
|
859
|
+
**Per-site base URL overrides:** set `BARNACLE_SITE_<UPPERCASE_SITE_ID>_BASE_URL` to override a plugin's `defaultBaseUrl` without source changes. Underscores in the env key map to hyphens in the `siteId` (e.g. `BARNACLE_SITE_MY_SHOP_BASE_URL` → plugin `my-shop`).
|
|
860
|
+
|
|
861
|
+
**Bypass header:** send `x-barnacle-force-fallback: true` on any plugin request to skip the hot path and go directly to the Stagehand browser path. Useful for debugging or when you know the hot path is broken. (Fastify lowercases incoming header keys; the dispatcher reads `request.headers["x-barnacle-force-fallback"]` — supply lowercase to match.)
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## Deployment
|
|
866
|
+
|
|
867
|
+
### Production checklist
|
|
868
|
+
|
|
869
|
+
```bash
|
|
870
|
+
# .env (production)
|
|
871
|
+
NODE_ENV=production
|
|
872
|
+
ENABLE_DOCS=false # never expose Swagger in prod
|
|
873
|
+
TRUST_PROXY=true # set false if deploying directly to the internet (no ALB/nginx)
|
|
874
|
+
DEV_BYPASS_AUTH=false # this is the default — confirm it's not set to true
|
|
875
|
+
API_KEYS_HASHED="<bcrypt-hash>,<bcrypt-hash>" # at least one key
|
|
876
|
+
STEEL_API_KEY="..."
|
|
877
|
+
ANTHROPIC_API_KEY="..." # or USE_BEDROCK=true + AWS creds
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Process management
|
|
881
|
+
|
|
882
|
+
Barnacle is a plain Node.js process. Use pm2 or systemd to keep it alive and
|
|
883
|
+
restart it on crash:
|
|
884
|
+
|
|
885
|
+
```bash
|
|
886
|
+
# pm2
|
|
887
|
+
pm2 start dist/server.js --name barnacle --env production
|
|
888
|
+
pm2 save && pm2 startup
|
|
889
|
+
|
|
890
|
+
# systemd (example unit)
|
|
891
|
+
[Service]
|
|
892
|
+
ExecStart=/usr/bin/node /srv/barnacle/dist/server.js
|
|
893
|
+
WorkingDirectory=/srv/barnacle
|
|
894
|
+
EnvironmentFile=/srv/barnacle/.env
|
|
895
|
+
Restart=on-failure
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Reverse proxy
|
|
899
|
+
|
|
900
|
+
Route traffic through nginx or an Application Load Balancer (ALB). Set
|
|
901
|
+
`TRUST_PROXY=true` so Fastify uses `X-Forwarded-For` for the client IP
|
|
902
|
+
(needed for rate limiting on unauthenticated traffic).
|
|
903
|
+
|
|
904
|
+
```nginx
|
|
905
|
+
location / {
|
|
906
|
+
proxy_pass http://127.0.0.1:3000;
|
|
907
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
908
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
909
|
+
proxy_set_header Host $host;
|
|
910
|
+
}
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### Health probes
|
|
914
|
+
|
|
915
|
+
Wire `/healthz` as the liveness probe and `/readyz` as the readiness probe:
|
|
916
|
+
|
|
917
|
+
```yaml
|
|
918
|
+
# Kubernetes example
|
|
919
|
+
livenessProbe:
|
|
920
|
+
httpGet: { path: /healthz, port: 3000 }
|
|
921
|
+
initialDelaySeconds: 5
|
|
922
|
+
readinessProbe:
|
|
923
|
+
httpGet: { path: /readyz, port: 3000 }
|
|
924
|
+
initialDelaySeconds: 10
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
`/readyz` returns 503 when the scraper pool queue is saturated (depth >
|
|
928
|
+
`READINESS_QUEUE_THRESHOLD`) or when required scraper credentials are missing.
|
|
929
|
+
|
|
930
|
+
---
|
|
931
|
+
|
|
932
|
+
## Common issues
|
|
933
|
+
|
|
934
|
+
| Symptom | Cause | Fix |
|
|
935
|
+
|---------|-------|-----|
|
|
936
|
+
| `Error: STEEL_API_KEY is required` | Missing env var | Add `STEEL_API_KEY` to `.env` |
|
|
937
|
+
| `useProxy rejected` / `402` from Steel | Free-tier plan doesn't support residential proxies | Set `SCRAPER_PROXY_TYPE=none` and `SCRAPER_SOLVE_CAPTCHA=false` |
|
|
938
|
+
| `401 Unauthorized` on every request | No API key configured or wrong plaintext key | Verify `API_KEYS_HASHED` is set; double-check the plaintext key. For dev, set `DEV_BYPASS_AUTH=true` |
|
|
939
|
+
| Stagehand throws `model not found` | Wrong model name format | Use the `anthropic/` prefix: `STAGEHAND_MODEL=anthropic/claude-sonnet-4-6` |
|
|
940
|
+
| `/readyz` returns 503 on `scraperCredentials` | `STEEL_API_KEY` or LLM key missing | Set the missing credential |
|
|
941
|
+
| Build succeeds but `dist/sites/` is empty | `tsc` ran but `cp -r src/sites dist/sites` was skipped | Run `pnpm run build` (not `tsc` directly) — the build script copies site sources after compilation |
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
## Reference
|
|
946
|
+
|
|
947
|
+
- Coding standards: [CLAUDE.md](./CLAUDE.md)
|
|
948
|
+
- Architecture & design rationale: [docs/architecture.md](./docs/architecture.md)
|
|
949
|
+
- Recon playbook (step-by-step): [docs/playbook.md](./docs/playbook.md)
|
|
950
|
+
- Testing guide: [docs/testing.md](./docs/testing.md)
|
|
951
|
+
- Telemetry & LLM judging concept guide: [docs/telemetry-and-judging.md](./docs/telemetry-and-judging.md)
|
|
952
|
+
- Per-site recon findings: [docs/target-recon.md](./docs/target-recon.md) (populated after first `pnpm run recon:summarize`)
|
|
953
|
+
|
|
954
|
+
## License
|
|
955
|
+
|
|
956
|
+
[MIT](./LICENSE) © Enricai
|
|
957
|
+
|
|
958
|
+
Contributions welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md).
|