@abot-ai/runtime 1.1.0 → 1.2.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 +6 -3
- package/dist/src/capabilities/tool-types.d.ts +1 -1
- package/dist/src/model-gateway/providers/ollama/generation-options.d.ts +1 -3
- package/dist/src/model-gateway/providers/ollama/generation-options.js +1 -5
- package/dist/src/model-gateway/providers/ollama/payload.js +0 -8
- package/dist/src/model-gateway/server/request-body.js +1 -0
- package/dist/src/plugin-sdk/index.d.ts +2 -0
- package/dist/src/plugin-sdk/index.js +2 -0
- package/dist/src/plugin-sdk/tool-availability-brief.d.ts +9 -0
- package/dist/src/plugin-sdk/tool-availability-brief.js +48 -0
- package/dist/src/plugin-sdk/tool-availability-overview.d.ts +9 -0
- package/dist/src/plugin-sdk/tool-availability-overview.js +62 -0
- package/dist/src/runtime/adapters/registered-tool-payload-plan.d.ts +0 -9
- package/dist/src/runtime/adapters/registered-tool-payload-plan.js +0 -67
- package/dist/src/runtime/adapters/registered-tool-payload-related-artifacts.d.ts +13 -0
- package/dist/src/runtime/adapters/registered-tool-payload-related-artifacts.js +84 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/catalog.js +4 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/execution.js +3 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-lifecycle.d.ts +1 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-lifecycle.js +11 -1
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-stages.d.ts +1 -0
- package/dist/src/runtime/adapters/registered-tool-worker-capabilities/payload-stages.js +9 -1
- package/dist/src/runtime/capabilities/request-bound-tool-registry.js +1 -24
- package/dist/src/runtime/capabilities/tool-availability.d.ts +6 -0
- package/dist/src/runtime/capabilities/tool-availability.js +28 -0
- package/dist/src/runtime/context/capability-brief.d.ts +25 -0
- package/dist/src/runtime/context/capability-brief.js +106 -0
- package/dist/src/runtime/context/request-tool-results-message-binding.d.ts +6 -0
- package/dist/src/runtime/context/request-tool-results-message-binding.js +30 -0
- package/dist/src/runtime/context/request-tool-results-scope.d.ts +3 -0
- package/dist/src/runtime/context/request-tool-results-scope.js +8 -0
- package/dist/src/runtime/context/request-tool-results.d.ts +8 -4
- package/dist/src/runtime/context/request-tool-results.js +49 -38
- package/dist/src/runtime/orchestration/role-calls/candidate-validation.js +8 -1
- package/dist/src/runtime/orchestration/role-calls/child-return.d.ts +4 -0
- package/dist/src/runtime/orchestration/role-calls/child-return.js +25 -5
- package/dist/src/runtime/orchestration/role-calls/command-decoder.js +8 -3
- package/dist/src/runtime/orchestration/role-calls/command-dispatch/child-transitions.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/command-dispatch/child-transitions.js +17 -2
- package/dist/src/runtime/orchestration/role-calls/command-dispatch.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/command-dispatch.js +2 -2
- package/dist/src/runtime/orchestration/role-calls/contracts.d.ts +5 -2
- package/dist/src/runtime/orchestration/role-calls/contracts.js +4 -4
- package/dist/src/runtime/orchestration/role-calls/dependency-results.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/dependency-results.js +1 -0
- package/dist/src/runtime/orchestration/role-calls/index.d.ts +5 -1
- package/dist/src/runtime/orchestration/role-calls/index.js +5 -1
- package/dist/src/runtime/orchestration/role-calls/ledger.d.ts +3 -1
- package/dist/src/runtime/orchestration/role-calls/ledger.js +9 -2
- package/dist/src/runtime/orchestration/role-calls/reducer.d.ts +1 -1
- package/dist/src/runtime/orchestration/role-calls/reducer.js +2 -2
- package/dist/src/runtime/orchestration/role-calls/result-receipt.d.ts +49 -0
- package/dist/src/runtime/orchestration/role-calls/result-receipt.js +188 -0
- package/dist/src/runtime/orchestration/role-calls/return-result-receipt.d.ts +19 -0
- package/dist/src/runtime/orchestration/role-calls/return-result-receipt.js +52 -0
- package/dist/src/runtime/orchestration/role-calls/transactions.js +1 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-lineage.d.ts +34 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-lineage.js +185 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-receipt.d.ts +36 -0
- package/dist/src/runtime/orchestration/role-calls/work-result-receipt.js +187 -0
- package/dist/src/runtime/orchestration/role-executors/activation/result-normalization.js +6 -3
- package/dist/src/runtime/orchestration/role-executors/child-invocation/transaction.js +1 -0
- package/dist/src/runtime/orchestration/role-executors/contracts.d.ts +2 -1
- package/dist/src/runtime/orchestration/worker-capabilities/assignment-provenance.d.ts +36 -0
- package/dist/src/runtime/orchestration/worker-capabilities/assignment-provenance.js +171 -0
- package/dist/src/runtime/orchestration/worker-capabilities/contracts.d.ts +32 -1
- package/dist/src/runtime/orchestration/worker-capabilities/execution/invocation-preparation.d.ts +2 -1
- package/dist/src/runtime/orchestration/worker-capabilities/execution/invocation-preparation.js +3 -0
- package/dist/src/runtime/orchestration/worker-capabilities/execution.js +42 -19
- package/dist/src/runtime/orchestration/worker-capabilities/index.d.ts +2 -0
- package/dist/src/runtime/orchestration/worker-capabilities/index.js +2 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-assignment-validation.d.ts +3 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-assignment-validation.js +30 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-author.js +8 -19
- package/dist/src/runtime/orchestration/worker-capabilities/payload-contracts.d.ts +5 -1
- package/dist/src/runtime/orchestration/worker-capabilities/payload-instructions.js +1 -1
- package/dist/src/runtime/orchestration/worker-capabilities/payload-source-provenance.d.ts +14 -0
- package/dist/src/runtime/orchestration/worker-capabilities/payload-source-provenance.js +132 -0
- package/dist/src/runtime/request/capability-brief.d.ts +5 -0
- package/dist/src/runtime/request/capability-brief.js +24 -0
- package/dist/src/runtime/request/execution-scope.d.ts +2 -1
- package/dist/src/runtime/request/role-executor-composition.js +1 -1
- package/dist/src/runtime/request/worker-capability-composition.d.ts +2 -1
- package/dist/src/runtime/request/worker-capability-composition.js +6 -1
- package/dist/src/runtime/request/worker-capability-payload.js +11 -8
- package/dist/src/runtime/steps/execution-agent/prompt.js +4 -3
- package/dist/src/runtime/steps/planner-decision/capability-catalog-context.d.ts +14 -0
- package/dist/src/runtime/steps/planner-decision/capability-catalog-context.js +87 -0
- package/dist/src/runtime/steps/planner-decision/contracts.d.ts +3 -5
- package/dist/src/runtime/steps/planner-decision/contracts.js +3 -3
- package/dist/src/runtime/steps/planner-decision/format.d.ts +3 -2
- package/dist/src/runtime/steps/planner-decision/format.js +73 -76
- package/dist/src/runtime/steps/planner-decision/input.d.ts +4 -3
- package/dist/src/runtime/steps/planner-decision/input.js +10 -7
- package/dist/src/runtime/steps/planner-decision/parser.d.ts +3 -2
- package/dist/src/runtime/steps/planner-decision/parser.js +10 -19
- package/dist/src/runtime/steps/planner-decision/plan-parser.js +18 -12
- package/dist/src/runtime/steps/planner-decision/prompt.js +33 -28
- package/dist/src/runtime/steps/planner-decision/resume.d.ts +1 -1
- package/dist/src/runtime/steps/planner-decision/resume.js +38 -1
- package/dist/src/runtime/steps/planner-decision/run.d.ts +3 -2
- package/dist/src/runtime/steps/reviewer-decision/audit-coverage.d.ts +10 -0
- package/dist/src/runtime/steps/reviewer-decision/audit-coverage.js +186 -0
- package/dist/src/runtime/steps/reviewer-decision/contracts.d.ts +3 -8
- package/dist/src/runtime/steps/reviewer-decision/contracts.js +12 -6
- package/dist/src/runtime/steps/reviewer-decision/delegated-context.d.ts +24 -0
- package/dist/src/runtime/steps/reviewer-decision/delegated-context.js +70 -0
- package/dist/src/runtime/steps/reviewer-decision/diagnostics.js +1 -1
- package/dist/src/runtime/steps/reviewer-decision/format.js +37 -24
- package/dist/src/runtime/steps/reviewer-decision/input.js +1 -1
- package/dist/src/runtime/steps/reviewer-decision/model-context.d.ts +4 -3
- package/dist/src/runtime/steps/reviewer-decision/model-context.js +92 -10
- package/dist/src/runtime/steps/reviewer-decision/parser.js +19 -1
- package/dist/src/runtime/steps/reviewer-decision/projection.js +42 -68
- package/dist/src/runtime/steps/reviewer-decision/prompt.js +10 -5
- package/dist/src/runtime/steps/reviewer-decision/run.js +12 -3
- package/dist/src/runtime/steps/reviewer-decision/verification-coverage.d.ts +14 -0
- package/dist/src/runtime/steps/reviewer-decision/verification-coverage.js +52 -0
- package/dist/src/runtime/steps/supervisor-decision/capability-brief.d.ts +14 -0
- package/dist/src/runtime/steps/supervisor-decision/capability-brief.js +38 -0
- package/dist/src/runtime/steps/supervisor-decision/contracts.d.ts +7 -3
- package/dist/src/runtime/steps/supervisor-decision/diagnostics.d.ts +2 -0
- package/dist/src/runtime/steps/supervisor-decision/diagnostics.js +8 -2
- package/dist/src/runtime/steps/supervisor-decision/format.js +32 -20
- package/dist/src/runtime/steps/supervisor-decision/input.d.ts +4 -1
- package/dist/src/runtime/steps/supervisor-decision/input.js +19 -1
- package/dist/src/runtime/steps/supervisor-decision/parser.js +62 -59
- package/dist/src/runtime/steps/supervisor-decision/prompt.js +6 -7
- package/dist/src/runtime/steps/supervisor-decision/resume.js +47 -1
- package/dist/src/runtime/steps/supervisor-decision/run.js +10 -0
- package/dist/src/runtime/steps/supervisor-decision/working-directory.js +2 -1
- package/dist/src/runtime/steps/worker-decision/contracts.d.ts +2 -1
- package/dist/src/runtime/steps/worker-decision/input/canonical-state.js +8 -1
- package/dist/src/runtime/steps/worker-decision/input/diagnostic-projection.js +2 -2
- package/dist/src/runtime/steps/worker-decision/input/prompt-context.d.ts +1 -0
- package/dist/src/runtime/steps/worker-decision/input/prompt-context.js +4 -1
- package/dist/src/runtime/steps/worker-decision/input/reference-context.js +34 -14
- package/dist/src/runtime/steps/worker-decision/input/types.d.ts +2 -1
- package/dist/src/runtime/steps/worker-decision/input.d.ts +1 -0
- package/dist/src/runtime/steps/worker-decision/input.js +1 -0
- package/dist/src/runtime/steps/worker-decision/payload-dependency-results.d.ts +8 -0
- package/dist/src/runtime/steps/worker-decision/payload-dependency-results.js +19 -0
- package/dist/src/runtime/steps/worker-decision/prompt.js +8 -8
- package/dist/src/runtime/steps/worker-decision/result-author.js +18 -11
- package/dist/src/runtime/steps/worker-decision/run.js +2 -2
- package/dist/src/web-ui/app/ARCHITECTURE.md +19 -6
- package/dist/src/web-ui/app/app.js +6 -7
- package/dist/src/web-ui/app/components/composer-context-window.d.ts +25 -0
- package/dist/src/web-ui/app/components/composer-context-window.js +155 -0
- package/dist/src/web-ui/app/components/composer-plan.d.ts +9 -0
- package/dist/src/web-ui/app/components/composer-plan.js +128 -0
- package/dist/src/web-ui/app/components/conversation-activity.d.ts +5 -6
- package/dist/src/web-ui/app/components/conversation-activity.js +64 -157
- package/dist/src/web-ui/app/components/conversation-role-cards.d.ts +23 -0
- package/dist/src/web-ui/app/components/conversation-role-cards.js +181 -0
- package/dist/src/web-ui/app/components/conversation-sidebar-layout.js +12 -0
- package/dist/src/web-ui/app/components/conversation-sources.d.ts +7 -0
- package/dist/src/web-ui/app/components/conversation-sources.js +182 -0
- package/dist/src/web-ui/app/components/conversation-view.js +34 -6
- package/dist/src/web-ui/app/components/model-selector.js +4 -1
- package/dist/src/web-ui/app/components/operations-section.js +46 -0
- package/dist/src/web-ui/app/components/workspace-shell.js +86 -78
- package/dist/src/web-ui/app/controllers/realtime-event-controller.d.ts +1 -0
- package/dist/src/web-ui/app/controllers/realtime-event-controller.js +55 -90
- package/dist/src/web-ui/app/controllers/runtime-selection-controller.js +2 -0
- package/dist/src/web-ui/app/controllers/session-controller.js +17 -12
- package/dist/src/web-ui/app/index.html +209 -210
- package/dist/src/web-ui/app/lib/composer-plan-model.d.ts +32 -0
- package/dist/src/web-ui/app/lib/composer-plan-model.js +108 -0
- package/dist/src/web-ui/app/lib/conversation-role-model.d.ts +19 -0
- package/dist/src/web-ui/app/lib/conversation-role-model.js +193 -0
- package/dist/src/web-ui/app/lib/event-presentation.js +30 -9
- package/dist/src/web-ui/app/lib/source-url-policy.d.ts +2 -0
- package/dist/src/web-ui/app/lib/source-url-policy.js +31 -0
- package/dist/src/web-ui/app/lib/task-progress.d.ts +53 -0
- package/dist/src/web-ui/app/lib/task-progress.js +233 -0
- package/dist/src/web-ui/app/lib/tool-invocation-count.d.ts +5 -0
- package/dist/src/web-ui/app/lib/tool-invocation-count.js +21 -0
- package/dist/src/web-ui/app/lib/web-source-event.d.ts +8 -0
- package/dist/src/web-ui/app/lib/web-source-event.js +21 -0
- package/dist/src/web-ui/app/lib/web-sources.d.ts +36 -0
- package/dist/src/web-ui/app/lib/web-sources.js +177 -0
- package/dist/src/web-ui/app/styles/00-tokens-base.css +15 -3
- package/dist/src/web-ui/app/styles/10-shell-sessions.css +24 -34
- package/dist/src/web-ui/app/styles/12-conversation-sidebar.css +93 -0
- package/dist/src/web-ui/app/styles/20-chat-composer.css +55 -472
- package/dist/src/web-ui/app/styles/21-composer-controls.css +368 -0
- package/dist/src/web-ui/app/styles/22-composer-context-window.css +90 -0
- package/dist/src/web-ui/app/styles/23-composer-plan.css +171 -0
- package/dist/src/web-ui/app/styles/24-conversation-role-cards.css +205 -0
- package/dist/src/web-ui/app/styles/25-conversation-sources.css +141 -0
- package/dist/src/web-ui/app/styles/30-inspector-config.css +21 -209
- package/dist/src/web-ui/app/styles/35-memory-onboarding.css +8 -8
- package/dist/src/web-ui/app/styles/36-memory-management.css +13 -13
- package/dist/src/web-ui/app/styles/37-config-operations.css +197 -0
- package/dist/src/web-ui/app/styles/40-config-workspace.css +36 -36
- package/dist/src/web-ui/app/styles/90-responsive.css +14 -77
- package/dist/src/web-ui/app/styles/foundation.css +2 -2
- package/dist/src/web-ui/app/styles.css +7 -0
- package/dist/src/web-ui/app/ui-behavior.d.ts +1 -1
- package/dist/src/web-ui/app/ui-behavior.js +1 -1
- package/docs/configuration.md +15 -8
- package/docs/installation.md +7 -4
- package/docs/known-limitations.md +21 -0
- package/docs/plugins.md +28 -13
- package/docs/runtime-invariant-coverage.md +18 -17
- package/docs/runtime-prompt-policy-contracts.md +27 -17
- package/package.json +4 -3
- package/plugins/capability-brief/source/index.ts +5 -48
- package/plugins/capability-brief/src/index.cjs +46 -33
- package/plugins/system-probe/plugin.json +1 -0
- package/plugins/web/README.md +125 -0
- package/plugins/web/plugin.json +1 -1
- package/plugins/web/skills/web_fetch_skill/SKILL.md +2 -2
- package/plugins/web/skills/web_search_skill/SKILL.md +8 -4
- package/plugins/web/source/errors.ts +5 -1
- package/plugins/web/source/fetch-presentation.ts +61 -0
- package/plugins/web/source/fetch-service.ts +2 -31
- package/plugins/web/source/light/config.ts +196 -0
- package/plugins/web/source/light/crawl/origin-gate.ts +134 -0
- package/plugins/web/source/light/crawl/robots-cache.ts +71 -0
- package/plugins/web/source/light/crawl/robots-parser.ts +132 -0
- package/plugins/web/source/light/crawl/robots-policy.ts +154 -0
- package/plugins/web/source/light/crawl/session-budget.ts +141 -0
- package/plugins/web/source/light/crawl/transport.ts +216 -0
- package/plugins/web/source/light/discovery/candidate-values.ts +63 -0
- package/plugins/web/source/light/discovery/feed-document.ts +127 -0
- package/plugins/web/source/light/discovery/html-links.ts +80 -0
- package/plugins/web/source/light/discovery/html-robots-directives.ts +29 -0
- package/plugins/web/source/light/discovery/markup-tree.ts +145 -0
- package/plugins/web/source/light/discovery/origin-dates.ts +71 -0
- package/plugins/web/source/light/discovery/sitemap-document.ts +63 -0
- package/plugins/web/source/light/documents/cache-policy.ts +28 -0
- package/plugins/web/source/light/documents/document-cache.ts +99 -0
- package/plugins/web/source/light/documents/document-charset.ts +89 -0
- package/plugins/web/source/light/documents/document-contract.ts +20 -0
- package/plugins/web/source/light/documents/document-reader.ts +75 -0
- package/plugins/web/source/light/documents/extract-document.ts +124 -0
- package/plugins/web/source/light/documents/http-cache-freshness.ts +68 -0
- package/plugins/web/source/light/documents/response-decoding.ts +120 -0
- package/plugins/web/source/light/ranking/document-ranking.ts +114 -0
- package/plugins/web/source/light/ranking/query-normalization.ts +69 -0
- package/plugins/web/source/light/search-collection.ts +136 -0
- package/plugins/web/source/light/search-frontier.ts +246 -0
- package/plugins/web/source/light/search-metadata.ts +84 -0
- package/plugins/web/source/light/search-service.ts +154 -0
- package/plugins/web/source/light/search-unavailable.ts +33 -0
- package/plugins/web/source/light/sources/catalog.ts +312 -0
- package/plugins/web/source/light/sources/source-definition.ts +9 -0
- package/plugins/web/source/light/sources/source-selection.ts +35 -0
- package/plugins/web/source/plugin.ts +70 -56
- package/plugins/web/source/public-http.ts +10 -1
- package/plugins/web/source/search-presentation.ts +63 -63
- package/plugins/web/source/search-service-selector.ts +31 -0
- package/plugins/web/source/search-service.ts +6 -0
- package/plugins/web/source/search-source-presentation.ts +76 -0
- package/plugins/web/source/search-types.ts +4 -0
- package/plugins/web/source/source-output-receipts.ts +267 -0
- package/plugins/web/source/source-receipt-budget.ts +51 -0
- package/plugins/web/source/source-receipt-contract.ts +18 -0
- package/plugins/web/src/index.cjs +3214 -288
- package/src/runtime/README.md +30 -9
- package/dist/src/model-gateway/structured-output/budget.d.ts +0 -7
- package/dist/src/model-gateway/structured-output/budget.js +0 -20
- package/dist/src/model-gateway/structured-output/schema-character-estimator.d.ts +0 -1
- package/dist/src/model-gateway/structured-output/schema-character-estimator.js +0 -211
- package/dist/src/model-gateway/structured-output-budget.d.ts +0 -1
- package/dist/src/model-gateway/structured-output-budget.js +0 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { WebPluginError } from "../errors.js";
|
|
3
|
+
import { parsePublicHttpUrl } from "../network-policy.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_LIGHT_SOURCES,
|
|
6
|
+
LIGHT_SOURCE_SET_ID,
|
|
7
|
+
LIGHT_SOURCE_SET_VERSION,
|
|
8
|
+
} from "./sources/catalog.js";
|
|
9
|
+
import type { LightSource } from "./sources/source-definition.js";
|
|
10
|
+
|
|
11
|
+
export type LightConfig = Readonly<{
|
|
12
|
+
sourceSetId: string;
|
|
13
|
+
sourceSetVersion: number;
|
|
14
|
+
sources: readonly LightSource[];
|
|
15
|
+
sourceLimit: number;
|
|
16
|
+
maxCandidates: number;
|
|
17
|
+
maxDepth: number;
|
|
18
|
+
maxSitemaps: number;
|
|
19
|
+
softTimeoutMs: number;
|
|
20
|
+
hardTimeoutMs: number;
|
|
21
|
+
maxRequests: number;
|
|
22
|
+
maxResponseBytes: number;
|
|
23
|
+
maxTotalBytes: number;
|
|
24
|
+
maxConcurrency: number;
|
|
25
|
+
maxRequestsPerOrigin: number;
|
|
26
|
+
requestTimeoutMs: number;
|
|
27
|
+
maxRedirects: number;
|
|
28
|
+
maxRobotsRedirects: number;
|
|
29
|
+
robotsTtlMs: number;
|
|
30
|
+
originCooldownMs: number;
|
|
31
|
+
cacheMaxDocuments: number;
|
|
32
|
+
cacheMaxBytes: number;
|
|
33
|
+
feedTtlMs: number;
|
|
34
|
+
pageTtlMs: number;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
const NUMBER_SETTINGS = {
|
|
38
|
+
sourceLimit: [6, 1, 32],
|
|
39
|
+
maxCandidates: [256, 1, 1_024],
|
|
40
|
+
maxDepth: [2, 0, 3],
|
|
41
|
+
maxSitemaps: [4, 0, 8],
|
|
42
|
+
softTimeoutMs: [20_000, 1, 29_999],
|
|
43
|
+
hardTimeoutMs: [30_000, 2, 30_000],
|
|
44
|
+
maxRequests: [24, 1, 48],
|
|
45
|
+
maxResponseBytes: [512 * 1_024, 512 * 1_024, 512 * 1_024],
|
|
46
|
+
maxTotalBytes: [8 * 1_024 * 1_024, 512 * 1_024, 8 * 1_024 * 1_024],
|
|
47
|
+
maxConcurrency: [3, 1, 3],
|
|
48
|
+
maxRequestsPerOrigin: [1, 1, 1],
|
|
49
|
+
requestTimeoutMs: [10_000, 1, 10_000],
|
|
50
|
+
maxRedirects: [3, 0, 3],
|
|
51
|
+
maxRobotsRedirects: [5, 5, 5],
|
|
52
|
+
robotsTtlMs: [86_400_000, 0, 86_400_000],
|
|
53
|
+
originCooldownMs: [60_000, 1_000, 300_000],
|
|
54
|
+
cacheMaxDocuments: [500, 0, 500],
|
|
55
|
+
cacheMaxBytes: [16 * 1_024 * 1_024, 0, 16 * 1_024 * 1_024],
|
|
56
|
+
feedTtlMs: [300_000, 0, 300_000],
|
|
57
|
+
pageTtlMs: [1_800_000, 0, 1_800_000],
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
function invalidConfiguration(message: string): never {
|
|
61
|
+
throw new WebPluginError("web_search_configuration_invalid", message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isConfigurationRecord(
|
|
65
|
+
value: unknown,
|
|
66
|
+
): value is Record<string, unknown> {
|
|
67
|
+
if (value === null) return false;
|
|
68
|
+
if (Array.isArray(value)) return false;
|
|
69
|
+
return typeof value === "object";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readSourceStrings(
|
|
73
|
+
value: unknown,
|
|
74
|
+
name: string,
|
|
75
|
+
maximum: number,
|
|
76
|
+
): readonly string[] {
|
|
77
|
+
if (!Array.isArray(value))
|
|
78
|
+
return invalidConfiguration(`Light ${name} must be an array.`);
|
|
79
|
+
if (value.length === 0)
|
|
80
|
+
return invalidConfiguration(`Light ${name} must not be empty.`);
|
|
81
|
+
if (value.length > maximum)
|
|
82
|
+
return invalidConfiguration(`Light ${name} has too many entries.`);
|
|
83
|
+
return Object.freeze(
|
|
84
|
+
value.map((entry) => {
|
|
85
|
+
if (typeof entry !== "string")
|
|
86
|
+
return invalidConfiguration(`Light ${name} entries must be strings.`);
|
|
87
|
+
const text = entry.trim();
|
|
88
|
+
if (!text)
|
|
89
|
+
return invalidConfiguration(`Light ${name} entries must not be empty.`);
|
|
90
|
+
if (text.length > 4_096)
|
|
91
|
+
return invalidConfiguration(`Light ${name} entry is too long.`);
|
|
92
|
+
return text;
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readSourceText(value: unknown, name: string): string {
|
|
98
|
+
if (typeof value !== "string")
|
|
99
|
+
return invalidConfiguration(`Light source ${name} must be text.`);
|
|
100
|
+
const text = value.trim();
|
|
101
|
+
if (!text)
|
|
102
|
+
return invalidConfiguration(`Light source ${name} must not be empty.`);
|
|
103
|
+
if (text.length > 512)
|
|
104
|
+
return invalidConfiguration(`Light source ${name} is too long.`);
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readSource(value: unknown): LightSource {
|
|
109
|
+
if (!isConfigurationRecord(value))
|
|
110
|
+
return invalidConfiguration("Light source must be an object.");
|
|
111
|
+
const allowedOrigins = readSourceStrings(
|
|
112
|
+
value.allowedOrigins,
|
|
113
|
+
"allowedOrigins",
|
|
114
|
+
8,
|
|
115
|
+
).map((origin) => parsePublicHttpUrl(origin).origin);
|
|
116
|
+
const entryUrls = readSourceStrings(value.entryUrls, "entryUrls", 8).map(
|
|
117
|
+
(url) => parsePublicHttpUrl(url).toString(),
|
|
118
|
+
);
|
|
119
|
+
for (const url of entryUrls) {
|
|
120
|
+
if (url.length > 1_024)
|
|
121
|
+
return invalidConfiguration(
|
|
122
|
+
"Light entry URLs must not exceed 1024 characters.",
|
|
123
|
+
);
|
|
124
|
+
if (!allowedOrigins.includes(new URL(url).origin)) {
|
|
125
|
+
return invalidConfiguration(
|
|
126
|
+
"Every Light entry URL must belong to that source's allowed origins.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const id = readSourceText(value.id, "id");
|
|
131
|
+
if (id.length > 64)
|
|
132
|
+
return invalidConfiguration(
|
|
133
|
+
"Light source IDs must not exceed 64 characters.",
|
|
134
|
+
);
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
id,
|
|
137
|
+
title: readSourceText(value.title, "title"),
|
|
138
|
+
description: readSourceText(value.description, "description"),
|
|
139
|
+
keywords: readSourceStrings(value.keywords, "keywords", 64),
|
|
140
|
+
languages: readSourceStrings(value.languages, "languages", 8),
|
|
141
|
+
entryUrls: Object.freeze(entryUrls),
|
|
142
|
+
allowedOrigins: Object.freeze(allowedOrigins),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function readLightConfig(value: unknown): LightConfig {
|
|
147
|
+
const input = value === undefined ? {} : value;
|
|
148
|
+
if (!isConfigurationRecord(input))
|
|
149
|
+
return invalidConfiguration("Light settings must be an object.");
|
|
150
|
+
const allowedKeys = new Set([...Object.keys(NUMBER_SETTINGS), "sources"]);
|
|
151
|
+
for (const name of Object.keys(input)) {
|
|
152
|
+
if (!allowedKeys.has(name))
|
|
153
|
+
return invalidConfiguration(`Unknown Light setting: ${name}.`);
|
|
154
|
+
}
|
|
155
|
+
const numbers = {} as Record<keyof typeof NUMBER_SETTINGS, number>;
|
|
156
|
+
for (const [name, [fallback, minimum, maximum]] of Object.entries(
|
|
157
|
+
NUMBER_SETTINGS,
|
|
158
|
+
)) {
|
|
159
|
+
const candidate = input[name] ?? fallback;
|
|
160
|
+
if (typeof candidate !== "number")
|
|
161
|
+
return invalidConfiguration(`Light ${name} must be an integer.`);
|
|
162
|
+
if (!Number.isSafeInteger(candidate))
|
|
163
|
+
return invalidConfiguration(`Light ${name} must be an integer.`);
|
|
164
|
+
if (candidate < minimum)
|
|
165
|
+
return invalidConfiguration(`Light ${name} is below its minimum.`);
|
|
166
|
+
if (candidate > maximum)
|
|
167
|
+
return invalidConfiguration(`Light ${name} exceeds its maximum.`);
|
|
168
|
+
numbers[name as keyof typeof NUMBER_SETTINGS] = candidate;
|
|
169
|
+
}
|
|
170
|
+
if (numbers.softTimeoutMs >= numbers.hardTimeoutMs) {
|
|
171
|
+
return invalidConfiguration(
|
|
172
|
+
"Light soft timeout must be shorter than its hard timeout.",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const sourceInput = input.sources ?? DEFAULT_LIGHT_SOURCES;
|
|
176
|
+
if (!Array.isArray(sourceInput))
|
|
177
|
+
return invalidConfiguration("Light sources must be an array.");
|
|
178
|
+
if (sourceInput.length === 0)
|
|
179
|
+
return invalidConfiguration("Light sources must not be empty.");
|
|
180
|
+
if (sourceInput.length > 64)
|
|
181
|
+
return invalidConfiguration("Light supports at most 64 sources.");
|
|
182
|
+
const sources = sourceInput.map(readSource);
|
|
183
|
+
if (new Set(sources.map(({ id }) => id)).size !== sources.length) {
|
|
184
|
+
return invalidConfiguration("Light source IDs must be unique.");
|
|
185
|
+
}
|
|
186
|
+
const sourceSetId =
|
|
187
|
+
sourceInput === DEFAULT_LIGHT_SOURCES
|
|
188
|
+
? LIGHT_SOURCE_SET_ID
|
|
189
|
+
: `configured-${createHash("sha256").update(JSON.stringify(sources)).digest("hex").slice(0, 16)}`;
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
...numbers,
|
|
192
|
+
sources: Object.freeze(sources),
|
|
193
|
+
sourceSetId,
|
|
194
|
+
sourceSetVersion: LIGHT_SOURCE_SET_VERSION,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { withinDeadline } from "../../deadline.js";
|
|
2
|
+
import { WebPluginError } from "../../errors.js";
|
|
3
|
+
|
|
4
|
+
type Release = () => void;
|
|
5
|
+
type Waiter = {
|
|
6
|
+
origin: string;
|
|
7
|
+
resolve: (release: Release) => void;
|
|
8
|
+
reject: (error: WebPluginError) => void;
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
onAbort: () => void;
|
|
11
|
+
timer: ReturnType<typeof setTimeout>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function createOriginGate(maxActive: number, maxPerOrigin: number) {
|
|
15
|
+
const activeOrigins = new Map<string, number>();
|
|
16
|
+
const queue: Waiter[] = [];
|
|
17
|
+
let active = 0;
|
|
18
|
+
const hasOriginCapacity = (origin: string) =>
|
|
19
|
+
(activeOrigins.get(origin) ?? 0) < maxPerOrigin;
|
|
20
|
+
|
|
21
|
+
const canStartOriginRequest = (origin: string) => {
|
|
22
|
+
if (active >= maxActive) return false;
|
|
23
|
+
return hasOriginCapacity(origin);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const takePermit = (origin: string): Release => {
|
|
27
|
+
active += 1;
|
|
28
|
+
activeOrigins.set(origin, (activeOrigins.get(origin) ?? 0) + 1);
|
|
29
|
+
let released = false;
|
|
30
|
+
return () => {
|
|
31
|
+
if (released) return;
|
|
32
|
+
released = true;
|
|
33
|
+
active -= 1;
|
|
34
|
+
const remaining = activeOrigins.get(origin)! - 1;
|
|
35
|
+
if (remaining === 0) activeOrigins.delete(origin);
|
|
36
|
+
else activeOrigins.set(origin, remaining);
|
|
37
|
+
drain();
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const clearWaiter = (waiter: Waiter) => {
|
|
42
|
+
clearTimeout(waiter.timer);
|
|
43
|
+
waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const drain = () => {
|
|
47
|
+
while (active < maxActive) {
|
|
48
|
+
const index = queue.findIndex((waiter) =>
|
|
49
|
+
hasOriginCapacity(waiter.origin),
|
|
50
|
+
);
|
|
51
|
+
if (index < 0) return;
|
|
52
|
+
const waiter = queue.splice(index, 1)[0]!;
|
|
53
|
+
clearWaiter(waiter);
|
|
54
|
+
waiter.resolve(takePermit(waiter.origin));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const acquire = (
|
|
59
|
+
origin: string,
|
|
60
|
+
deadline: number,
|
|
61
|
+
signal: AbortSignal,
|
|
62
|
+
): Promise<Release> => {
|
|
63
|
+
if (signal.aborted) {
|
|
64
|
+
return Promise.reject(
|
|
65
|
+
new WebPluginError(
|
|
66
|
+
"web_request_aborted",
|
|
67
|
+
"The web request was aborted.",
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (Date.now() >= deadline) {
|
|
72
|
+
return Promise.reject(
|
|
73
|
+
new WebPluginError(
|
|
74
|
+
"web_request_timed_out",
|
|
75
|
+
"The Light search exceeded its deadline.",
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (canStartOriginRequest(origin)) {
|
|
80
|
+
return Promise.resolve(takePermit(origin));
|
|
81
|
+
}
|
|
82
|
+
if (queue.length >= 64) {
|
|
83
|
+
return Promise.reject(
|
|
84
|
+
new WebPluginError(
|
|
85
|
+
"web_request_capacity_exceeded",
|
|
86
|
+
"The Light search request queue is full.",
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const fail = (code: "web_request_aborted" | "web_request_timed_out") => {
|
|
92
|
+
const index = queue.indexOf(waiter);
|
|
93
|
+
if (index < 0) return;
|
|
94
|
+
queue.splice(index, 1);
|
|
95
|
+
clearWaiter(waiter);
|
|
96
|
+
reject(
|
|
97
|
+
new WebPluginError(
|
|
98
|
+
code,
|
|
99
|
+
"The queued Light request could not continue.",
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
const waiter: Waiter = {
|
|
104
|
+
origin,
|
|
105
|
+
resolve,
|
|
106
|
+
reject,
|
|
107
|
+
signal,
|
|
108
|
+
onAbort: () => fail("web_request_aborted"),
|
|
109
|
+
timer: setTimeout(
|
|
110
|
+
() => fail("web_request_timed_out"),
|
|
111
|
+
Math.max(1, deadline - Date.now()),
|
|
112
|
+
),
|
|
113
|
+
};
|
|
114
|
+
waiter.timer.unref?.();
|
|
115
|
+
signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
116
|
+
queue.push(waiter);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
async run<T>(
|
|
122
|
+
origin: string,
|
|
123
|
+
deadline: number,
|
|
124
|
+
signal: AbortSignal,
|
|
125
|
+
operation: () => Promise<T>,
|
|
126
|
+
): Promise<T> {
|
|
127
|
+
const release = await acquire(origin, deadline, signal);
|
|
128
|
+
const work = Promise.resolve().then(operation);
|
|
129
|
+
// Keep capacity reserved if an injected or non-cancellable operation outlives abort.
|
|
130
|
+
void work.then(release, release);
|
|
131
|
+
return await withinDeadline(work, deadline, signal);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RobotsDocument } from "./robots-parser.js";
|
|
2
|
+
|
|
3
|
+
type CachedRobots = Readonly<{
|
|
4
|
+
document: RobotsDocument;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
bytes: number;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
const MAX_ROBOTS_CACHE_BYTES = 4 * 1_024 * 1_024;
|
|
10
|
+
const MAX_ROBOTS_CACHE_ENTRIES = 64;
|
|
11
|
+
|
|
12
|
+
function estimatedRobotsBytes(
|
|
13
|
+
origin: string,
|
|
14
|
+
document: RobotsDocument,
|
|
15
|
+
): number {
|
|
16
|
+
const ruleBytes = document.rules.reduce(
|
|
17
|
+
(sum, rule) => sum + rule.path.length * 2 + 96,
|
|
18
|
+
0,
|
|
19
|
+
);
|
|
20
|
+
const sitemapBytes = document.sitemaps.reduce(
|
|
21
|
+
(sum, url) => sum + url.length * 2 + 32,
|
|
22
|
+
0,
|
|
23
|
+
);
|
|
24
|
+
return origin.length * 2 + ruleBytes + sitemapBytes + 128;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createRobotsCache() {
|
|
28
|
+
const entries = new Map<string, CachedRobots>();
|
|
29
|
+
let bytes = 0;
|
|
30
|
+
const remove = (origin: string) => {
|
|
31
|
+
const entry = entries.get(origin);
|
|
32
|
+
if (!entry) return;
|
|
33
|
+
bytes -= entry.bytes;
|
|
34
|
+
entries.delete(origin);
|
|
35
|
+
};
|
|
36
|
+
const exceedsRobotsCacheCapacity = (incomingBytes: number) => {
|
|
37
|
+
if (entries.size >= MAX_ROBOTS_CACHE_ENTRIES) return true;
|
|
38
|
+
return bytes + incomingBytes > MAX_ROBOTS_CACHE_BYTES;
|
|
39
|
+
};
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
get(origin: string): RobotsDocument | undefined {
|
|
42
|
+
const entry = entries.get(origin);
|
|
43
|
+
if (!entry) return undefined;
|
|
44
|
+
if (entry.expiresAt <= Date.now()) {
|
|
45
|
+
remove(origin);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
entries.delete(origin);
|
|
49
|
+
entries.set(origin, entry);
|
|
50
|
+
return entry.document;
|
|
51
|
+
},
|
|
52
|
+
set(origin: string, document: RobotsDocument, ttlMs: number) {
|
|
53
|
+
remove(origin);
|
|
54
|
+
const entryBytes = estimatedRobotsBytes(origin, document);
|
|
55
|
+
if (ttlMs <= 0) return;
|
|
56
|
+
if (entryBytes > MAX_ROBOTS_CACHE_BYTES) return;
|
|
57
|
+
for (const [key, entry] of entries) {
|
|
58
|
+
if (entry.expiresAt <= Date.now()) remove(key);
|
|
59
|
+
}
|
|
60
|
+
while (exceedsRobotsCacheCapacity(entryBytes)) {
|
|
61
|
+
remove(entries.keys().next().value!);
|
|
62
|
+
}
|
|
63
|
+
entries.set(origin, {
|
|
64
|
+
document,
|
|
65
|
+
bytes: entryBytes,
|
|
66
|
+
expiresAt: Date.now() + ttlMs,
|
|
67
|
+
});
|
|
68
|
+
bytes += entryBytes;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export type RobotsRule = Readonly<{ path: string; allow: boolean }>;
|
|
2
|
+
export type RobotsDocument = Readonly<{
|
|
3
|
+
rules: readonly RobotsRule[];
|
|
4
|
+
sitemaps: readonly string[];
|
|
5
|
+
}>;
|
|
6
|
+
|
|
7
|
+
type RobotsGroup = { agents: string[]; rules: RobotsRule[] };
|
|
8
|
+
|
|
9
|
+
function normalizeRobotsPath(value: string): string {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/[^\x00-\x7f]/gu, (character) => encodeURIComponent(character))
|
|
12
|
+
.replace(/%[0-9a-f]{2}/giu, (escape) => {
|
|
13
|
+
const character = String.fromCharCode(
|
|
14
|
+
Number.parseInt(escape.slice(1), 16),
|
|
15
|
+
);
|
|
16
|
+
return /^[a-z0-9._~-]$/iu.test(character)
|
|
17
|
+
? character
|
|
18
|
+
: escape.toUpperCase();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseRobotsDocument(
|
|
23
|
+
text: string,
|
|
24
|
+
productToken = "abot-runtime-web",
|
|
25
|
+
): RobotsDocument {
|
|
26
|
+
const groups: RobotsGroup[] = [];
|
|
27
|
+
const sitemaps: string[] = [];
|
|
28
|
+
let current: RobotsGroup | undefined;
|
|
29
|
+
let hasRules = false;
|
|
30
|
+
for (const rawLine of text.replace(/^\uFEFF/u, "").split(/\r?\n|\r/u)) {
|
|
31
|
+
const line = rawLine.split("#", 1)[0]!.trim();
|
|
32
|
+
const separator = line.indexOf(":");
|
|
33
|
+
if (separator < 0) continue;
|
|
34
|
+
const name = line.slice(0, separator).trim().toLowerCase();
|
|
35
|
+
const value = line.slice(separator + 1).trim();
|
|
36
|
+
if (name === "sitemap") {
|
|
37
|
+
if (value) sitemaps.push(value);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (name === "user-agent") {
|
|
41
|
+
if (!current || hasRules) {
|
|
42
|
+
current = { agents: [], rules: [] };
|
|
43
|
+
groups.push(current);
|
|
44
|
+
hasRules = false;
|
|
45
|
+
}
|
|
46
|
+
current.agents.push(value.toLowerCase());
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!current) continue;
|
|
50
|
+
const isAccessRule = name === "allow" || name === "disallow";
|
|
51
|
+
if (!isAccessRule) continue;
|
|
52
|
+
hasRules = true;
|
|
53
|
+
if (!value.startsWith("/")) continue;
|
|
54
|
+
current.rules.push({
|
|
55
|
+
path: normalizeRobotsPath(value),
|
|
56
|
+
allow: name === "allow",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const matching = groups.filter((group) =>
|
|
60
|
+
group.agents.includes(productToken.toLowerCase()),
|
|
61
|
+
);
|
|
62
|
+
const applicable =
|
|
63
|
+
matching.length > 0
|
|
64
|
+
? matching
|
|
65
|
+
: groups.filter((group) => group.agents.includes("*"));
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
rules: Object.freeze(applicable.flatMap((group) => group.rules)),
|
|
68
|
+
sitemaps: Object.freeze([...new Set(sitemaps)]),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A bounded wildcard matcher avoids regular-expression backtracking on robots input.
|
|
73
|
+
function matchesRobotsPath(
|
|
74
|
+
path: string,
|
|
75
|
+
pattern: string,
|
|
76
|
+
work: { remaining: number },
|
|
77
|
+
): boolean | undefined {
|
|
78
|
+
const anchored = pattern.endsWith("$");
|
|
79
|
+
const candidate = anchored ? pattern.slice(0, -1) : pattern;
|
|
80
|
+
let pathIndex = 0;
|
|
81
|
+
let patternIndex = 0;
|
|
82
|
+
let wildcardIndex = -1;
|
|
83
|
+
let wildcardEnd = 0;
|
|
84
|
+
while (pathIndex < path.length) {
|
|
85
|
+
work.remaining -= 1;
|
|
86
|
+
if (work.remaining < 0) return undefined;
|
|
87
|
+
if (patternIndex === candidate.length && !anchored) return true;
|
|
88
|
+
if (candidate[patternIndex] === "*") {
|
|
89
|
+
wildcardIndex = patternIndex++;
|
|
90
|
+
wildcardEnd = pathIndex;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (candidate[patternIndex] === path[pathIndex]) {
|
|
94
|
+
pathIndex += 1;
|
|
95
|
+
patternIndex += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (wildcardIndex < 0) return false;
|
|
99
|
+
patternIndex = wildcardIndex + 1;
|
|
100
|
+
pathIndex = ++wildcardEnd;
|
|
101
|
+
}
|
|
102
|
+
while (candidate[patternIndex] === "*") patternIndex += 1;
|
|
103
|
+
return patternIndex === candidate.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ruleSpecificity(path: string): number {
|
|
107
|
+
return path.length;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isRobotsPathAllowed(
|
|
111
|
+
document: RobotsDocument,
|
|
112
|
+
url: URL,
|
|
113
|
+
): boolean {
|
|
114
|
+
if (url.pathname === "/robots.txt") return true;
|
|
115
|
+
const path = normalizeRobotsPath(url.pathname + url.search)
|
|
116
|
+
.replace(/\*/gu, "%2A")
|
|
117
|
+
.replace(/\$/gu, "%24");
|
|
118
|
+
const work = { remaining: 2_000_000 };
|
|
119
|
+
let longestMatch = -1;
|
|
120
|
+
let allowed = true;
|
|
121
|
+
for (const rule of document.rules) {
|
|
122
|
+
const matches = matchesRobotsPath(path, rule.path, work);
|
|
123
|
+
if (matches === undefined) return false;
|
|
124
|
+
if (!matches) continue;
|
|
125
|
+
const specificity = ruleSpecificity(rule.path);
|
|
126
|
+
if (specificity < longestMatch) continue;
|
|
127
|
+
if (specificity === longestMatch && !rule.allow) continue;
|
|
128
|
+
longestMatch = specificity;
|
|
129
|
+
allowed = rule.allow;
|
|
130
|
+
}
|
|
131
|
+
return allowed;
|
|
132
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { isWebPluginError, WebPluginError } from "../../errors.js";
|
|
2
|
+
import { parsePublicHttpUrl } from "../../network-policy.js";
|
|
3
|
+
import { WEB_LIMITS } from "../../limits.js";
|
|
4
|
+
import type { PublicHttpResponse } from "../../public-http.js";
|
|
5
|
+
import type { LightConfig } from "../config.js";
|
|
6
|
+
import { decodeLightResponse } from "../documents/response-decoding.js";
|
|
7
|
+
import { httpCacheFreshnessMs } from "../documents/http-cache-freshness.js";
|
|
8
|
+
import {
|
|
9
|
+
isRobotsPathAllowed,
|
|
10
|
+
parseRobotsDocument,
|
|
11
|
+
type RobotsDocument,
|
|
12
|
+
} from "./robots-parser.js";
|
|
13
|
+
import type { LightSessionBudget } from "./session-budget.js";
|
|
14
|
+
import { createRobotsCache } from "./robots-cache.js";
|
|
15
|
+
|
|
16
|
+
type RobotsFetch = (url: URL) => Promise<PublicHttpResponse>;
|
|
17
|
+
const BLOCKED_DOCUMENT: RobotsDocument = Object.freeze({
|
|
18
|
+
rules: Object.freeze([{ path: "/", allow: false }]),
|
|
19
|
+
sitemaps: Object.freeze([]),
|
|
20
|
+
});
|
|
21
|
+
const EMPTY_DOCUMENT: RobotsDocument = Object.freeze({
|
|
22
|
+
rules: Object.freeze([]),
|
|
23
|
+
sitemaps: Object.freeze([]),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function shouldStopAfterRobotsFailure(error: unknown): boolean {
|
|
27
|
+
if (!isWebPluginError(error)) return false;
|
|
28
|
+
if (error.code === "web_request_aborted") return true;
|
|
29
|
+
return error.code === "web_search_budget_exhausted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasUnavailableRobots(status: number): boolean {
|
|
33
|
+
return status === 404 || status === 410;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasIncompleteRobots(response: PublicHttpResponse): boolean {
|
|
37
|
+
if (response.status === 206) return true;
|
|
38
|
+
if (response.headers["content-range"] !== undefined) return true;
|
|
39
|
+
return response.partialContent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasSuccessfulRobots(status: number): boolean {
|
|
43
|
+
if (status < 200) return false;
|
|
44
|
+
return status < 300;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readRobotsResponse(
|
|
48
|
+
response: PublicHttpResponse,
|
|
49
|
+
config: LightConfig,
|
|
50
|
+
budget: LightSessionBudget,
|
|
51
|
+
): Readonly<{ document: RobotsDocument; ttl: number }> {
|
|
52
|
+
if (hasIncompleteRobots(response)) {
|
|
53
|
+
return { document: BLOCKED_DOCUMENT, ttl: config.originCooldownMs };
|
|
54
|
+
}
|
|
55
|
+
if (hasUnavailableRobots(response.status)) {
|
|
56
|
+
return {
|
|
57
|
+
document: EMPTY_DOCUMENT,
|
|
58
|
+
ttl: httpCacheFreshnessMs(response, config.robotsTtlMs, Date.now()),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (!hasSuccessfulRobots(response.status)) {
|
|
62
|
+
return { document: BLOCKED_DOCUMENT, ttl: config.originCooldownMs };
|
|
63
|
+
}
|
|
64
|
+
const remainingBytes = config.maxTotalBytes - budget.snapshot().decodedBytes;
|
|
65
|
+
if (remainingBytes <= 0) {
|
|
66
|
+
throw new WebPluginError(
|
|
67
|
+
"web_search_budget_exhausted",
|
|
68
|
+
"The Light decoded byte budget was exhausted.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const decoded = decodeLightResponse(
|
|
72
|
+
response,
|
|
73
|
+
Math.min(config.maxResponseBytes, remainingBytes),
|
|
74
|
+
budget.consumeDecodedBytes,
|
|
75
|
+
);
|
|
76
|
+
if (decoded.partialContent) {
|
|
77
|
+
return { document: BLOCKED_DOCUMENT, ttl: config.originCooldownMs };
|
|
78
|
+
}
|
|
79
|
+
const document = parseRobotsDocument(
|
|
80
|
+
new TextDecoder("utf-8").decode(decoded.body),
|
|
81
|
+
);
|
|
82
|
+
const ttl = httpCacheFreshnessMs(response, config.robotsTtlMs, Date.now());
|
|
83
|
+
return { document, ttl };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createRobotsPolicy(
|
|
87
|
+
config: LightConfig,
|
|
88
|
+
admittedOrigins: ReadonlySet<string>,
|
|
89
|
+
) {
|
|
90
|
+
// Only configured origins own cache entries; redirected robots hosts never gain admission.
|
|
91
|
+
const completed = createRobotsCache();
|
|
92
|
+
|
|
93
|
+
const load = async (
|
|
94
|
+
origin: string,
|
|
95
|
+
fetch: RobotsFetch,
|
|
96
|
+
budget: LightSessionBudget,
|
|
97
|
+
): Promise<RobotsDocument> => {
|
|
98
|
+
const cached = completed.get(origin);
|
|
99
|
+
if (cached) return cached;
|
|
100
|
+
let document = BLOCKED_DOCUMENT;
|
|
101
|
+
let ttl = config.originCooldownMs;
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(new URL("/robots.txt", origin));
|
|
104
|
+
budget.assertActive();
|
|
105
|
+
const interpreted = readRobotsResponse(response, config, budget);
|
|
106
|
+
document = interpreted.document;
|
|
107
|
+
ttl = interpreted.ttl;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
budget.assertActive();
|
|
110
|
+
if (shouldStopAfterRobotsFailure(error)) throw error;
|
|
111
|
+
}
|
|
112
|
+
completed.set(origin, document, ttl);
|
|
113
|
+
return document;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return Object.freeze({
|
|
117
|
+
createSession(fetch: RobotsFetch, budget: LightSessionBudget) {
|
|
118
|
+
const documents = new Map<string, Promise<RobotsDocument>>();
|
|
119
|
+
const observed = new Map<string, RobotsDocument>();
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
async assertAllowed(url: URL) {
|
|
122
|
+
let pending = documents.get(url.origin);
|
|
123
|
+
if (!pending) {
|
|
124
|
+
pending = load(url.origin, fetch, budget);
|
|
125
|
+
documents.set(url.origin, pending);
|
|
126
|
+
}
|
|
127
|
+
const document = await pending;
|
|
128
|
+
observed.set(url.origin, document);
|
|
129
|
+
budget.assertActive();
|
|
130
|
+
if (!isRobotsPathAllowed(document, url)) {
|
|
131
|
+
throw new WebPluginError(
|
|
132
|
+
"web_search_source_blocked",
|
|
133
|
+
"The source robots policy does not allow this Light crawl.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
sitemapsFor(origin: string): readonly string[] {
|
|
138
|
+
const document = observed.get(origin) ?? completed.get(origin);
|
|
139
|
+
if (!document) return Object.freeze([]);
|
|
140
|
+
return Object.freeze(
|
|
141
|
+
document.sitemaps.filter((rawUrl) => {
|
|
142
|
+
if (rawUrl.length > WEB_LIMITS.upstreamUrlChars) return false;
|
|
143
|
+
try {
|
|
144
|
+
return admittedOrigins.has(parsePublicHttpUrl(rawUrl).origin);
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|