@cassiomc1/forgeloop 1.13.0 → 1.14.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/AGENT_COMPATIBILITY.md +8 -0
- package/DOCS_INDEX.md +35 -3
- package/ENG/nodejs-backend-development-eng.md +2 -2
- package/ENG/sec-code-eng.md +7 -7
- package/EXECUTION_STATE.md +12 -0
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +40 -25
- package/TERMINOLOGY.md +2 -0
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +19 -1
- package/completions/forgeloop.bash +37 -1
- package/completions/forgeloop.fish +123 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +27 -2
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +366 -6
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +32 -11
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +35 -8
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +38 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +750 -5
- package/docs/protocol-requirements.json +24 -0
- package/package.json +30 -3
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +4 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +241 -0
- package/src/core/command-executors.js +110 -0
- package/src/core/command-input.js +115 -43
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/gate-provenance.js +124 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/phase.js +128 -10
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +128 -52
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +118 -61
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_BROWSER_VERIFICATION_PROVIDER_INVALID,
|
|
3
|
+
E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
|
|
4
|
+
E_BROWSER_VERIFICATION_REQUEST_INVALID,
|
|
5
|
+
} from "../error-codes.js";
|
|
6
|
+
import {
|
|
7
|
+
assertPortableContextSafe,
|
|
8
|
+
deepFreeze,
|
|
9
|
+
normalizePortableText,
|
|
10
|
+
} from "../portable-context.js";
|
|
11
|
+
import {
|
|
12
|
+
BROWSER_VERIFICATION_ASSERTION_KINDS,
|
|
13
|
+
BROWSER_VERIFICATION_ASSERTION_STATUSES,
|
|
14
|
+
BROWSER_VERIFICATION_CAPTURE_POLICIES,
|
|
15
|
+
BROWSER_VERIFICATION_LIMITS,
|
|
16
|
+
BROWSER_VERIFICATION_LOCATOR_KINDS,
|
|
17
|
+
BROWSER_VERIFICATION_STEP_KINDS,
|
|
18
|
+
BROWSER_VERIFICATION_WAIT_CONDITIONS,
|
|
19
|
+
normalizeBrowserVerificationRequestOptions,
|
|
20
|
+
} from "./constants.js";
|
|
21
|
+
|
|
22
|
+
export const BROWSER_VERIFICATION_PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,62}[a-z0-9])?$/;
|
|
23
|
+
|
|
24
|
+
function requestError(message, details) {
|
|
25
|
+
const error = new Error(message);
|
|
26
|
+
error.code = E_BROWSER_VERIFICATION_REQUEST_INVALID;
|
|
27
|
+
error.details = details ?? null;
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function providerError(message, details) {
|
|
32
|
+
const error = new Error(message);
|
|
33
|
+
error.code = E_BROWSER_VERIFICATION_PROVIDER_INVALID;
|
|
34
|
+
error.details = details ?? null;
|
|
35
|
+
return error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function unavailableError(message, details) {
|
|
39
|
+
const error = new Error(message);
|
|
40
|
+
error.code = E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE;
|
|
41
|
+
error.details = details ?? null;
|
|
42
|
+
return error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toPortable(label, value, maxLength, { optional = false } = {}) {
|
|
46
|
+
try {
|
|
47
|
+
return normalizePortableText(value, { label, maxLength, optional });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw requestError(`${label} is invalid: ${error.message}`, { [label]: value });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertPlainObject(value, label) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
55
|
+
|| (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
56
|
+
throw requestError(`${label} must be an object.`, { [label]: value });
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rejectUnknown(source, allowed, label) {
|
|
62
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
63
|
+
if (typeof key !== "string" || !allowed.includes(key)) {
|
|
64
|
+
throw requestError(`${label}.${String(key)} is not supported.`, { field: String(key) });
|
|
65
|
+
}
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
67
|
+
if (!descriptor || descriptor.get || descriptor.set) {
|
|
68
|
+
throw requestError(`${label}.${key} must not be an accessor.`, { field: key });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertBoundedUrl(value, label) {
|
|
74
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
75
|
+
throw requestError(`${label} must be a non-empty http(s) URL.`, { [label]: value });
|
|
76
|
+
}
|
|
77
|
+
if (value.length > BROWSER_VERIFICATION_LIMITS.maxUrlChars) {
|
|
78
|
+
throw requestError(`${label} exceeds the ${BROWSER_VERIFICATION_LIMITS.maxUrlChars}-character limit.`, {
|
|
79
|
+
[label]: value,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
let url;
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(value);
|
|
85
|
+
} catch {
|
|
86
|
+
throw requestError(`${label} must be a valid http(s) URL.`, { [label]: value });
|
|
87
|
+
}
|
|
88
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
89
|
+
throw requestError(`${label} must use http or https.`, { [label]: value });
|
|
90
|
+
}
|
|
91
|
+
if (url.username !== "" || url.password !== "") {
|
|
92
|
+
throw requestError(`${label} must not include credentials (userinfo).`, { [label]: value });
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertHttpsOrigin(origin, index) {
|
|
98
|
+
if (typeof origin !== "string" || origin.trim() === "") {
|
|
99
|
+
throw requestError(`allowedOrigins[${index}] must be a non-empty http(s) origin.`, {
|
|
100
|
+
origin,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let url;
|
|
104
|
+
try {
|
|
105
|
+
url = new URL(origin);
|
|
106
|
+
} catch {
|
|
107
|
+
throw requestError(`allowedOrigins[${index}] must be a valid http(s) origin.`, { origin });
|
|
108
|
+
}
|
|
109
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
110
|
+
throw requestError(`allowedOrigins[${index}] must use http or https.`, { origin });
|
|
111
|
+
}
|
|
112
|
+
if (url.username !== "" || url.password !== "") {
|
|
113
|
+
throw requestError(`allowedOrigins[${index}] must not include credentials (userinfo).`, { origin });
|
|
114
|
+
}
|
|
115
|
+
if (url.search !== "" || url.hash !== "") {
|
|
116
|
+
throw requestError(`allowedOrigins[${index}] must not include query or fragment.`, { origin });
|
|
117
|
+
}
|
|
118
|
+
if (url.pathname !== "" && url.pathname !== "/") {
|
|
119
|
+
throw requestError(`allowedOrigins[${index}] must not include a path.`, { origin });
|
|
120
|
+
}
|
|
121
|
+
const hostname = url.hostname;
|
|
122
|
+
if (hostname.endsWith(".")) {
|
|
123
|
+
throw requestError(`allowedOrigins[${index}] must not have a trailing dot.`, { origin });
|
|
124
|
+
}
|
|
125
|
+
if (hostname.length === 0 || hostname.length > 253) {
|
|
126
|
+
throw requestError(`allowedOrigins[${index}] has an invalid hostname.`, { origin });
|
|
127
|
+
}
|
|
128
|
+
if (url.hostname.includes("[") || url.hostname.includes("]")) {
|
|
129
|
+
throw requestError(`allowedOrigins[${index}] must be a hostname, not an IP-literal bracket form.`, { origin });
|
|
130
|
+
}
|
|
131
|
+
const labels = hostname.split(".");
|
|
132
|
+
for (const label of labels) {
|
|
133
|
+
if (label.length === 0 || label.length > 63 || !/^[a-z0-9-]+$/i.test(label)) {
|
|
134
|
+
throw requestError(`allowedOrigins[${index}] has an invalid hostname label.`, { origin });
|
|
135
|
+
}
|
|
136
|
+
if (label.startsWith("-") || label.endsWith("-")) {
|
|
137
|
+
throw requestError(`allowedOrigins[${index}] has an invalid hostname label.`, { origin });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const scheme = url.protocol === "https:" ? "https" : "http";
|
|
141
|
+
return `${scheme}://${hostname.toLowerCase()}${url.port === "" ? "" : `:${url.port}`}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeAllowedOrigins(allowedOrigins) {
|
|
145
|
+
if (!Array.isArray(allowedOrigins) || allowedOrigins.length === 0) {
|
|
146
|
+
throw requestError("allowedOrigins must be a non-empty array.", { allowedOrigins });
|
|
147
|
+
}
|
|
148
|
+
if (allowedOrigins.length > BROWSER_VERIFICATION_LIMITS.maxAllowedOrigins) {
|
|
149
|
+
throw requestError(
|
|
150
|
+
`allowedOrigins exceeds the ${BROWSER_VERIFICATION_LIMITS.maxAllowedOrigins}-origin limit.`,
|
|
151
|
+
{ count: allowedOrigins.length },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const normalized = allowedOrigins.map((origin, index) => assertHttpsOrigin(origin, index));
|
|
155
|
+
const unique = [...new Set(normalized)];
|
|
156
|
+
if (unique.length !== normalized.length) {
|
|
157
|
+
throw requestError("allowedOrigins must not contain duplicates.", { allowedOrigins });
|
|
158
|
+
}
|
|
159
|
+
return unique;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function assertHttpOrHttpsUrl(value, label, { allowedOrigins = null } = {}) {
|
|
163
|
+
const result = assertBoundedUrl(value, label);
|
|
164
|
+
if (allowedOrigins !== null) {
|
|
165
|
+
const url = new URL(value);
|
|
166
|
+
const hostname = url.hostname.toLowerCase();
|
|
167
|
+
const origin = `${url.protocol}//${hostname}${url.port === "" ? "" : `:${url.port}`}`;
|
|
168
|
+
if (!allowedOrigins.includes(origin)) {
|
|
169
|
+
throw requestError(`${label} origin is not in allowedOrigins.`, { [label]: value });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizeLocator(locator, label) {
|
|
176
|
+
const source = assertPlainObject(locator, label);
|
|
177
|
+
rejectUnknown(source, ["kind", "value"], label);
|
|
178
|
+
if (!BROWSER_VERIFICATION_LOCATOR_KINDS.includes(source.kind)) {
|
|
179
|
+
throw requestError(`${label}.kind must be a supported locator kind.`, { kind: source.kind });
|
|
180
|
+
}
|
|
181
|
+
const value = toPortable(`${label}.value`, source.value, BROWSER_VERIFICATION_LIMITS.maxLocatorChars);
|
|
182
|
+
return deepFreeze({ kind: source.kind, value });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeWaitStep(source, index, allowedOrigins) {
|
|
186
|
+
if (source.condition === undefined || source.condition === null) {
|
|
187
|
+
throw requestError(`steps[${index}].condition is required for WAIT_FOR.`, { step: source });
|
|
188
|
+
}
|
|
189
|
+
if (!BROWSER_VERIFICATION_WAIT_CONDITIONS.includes(source.condition)) {
|
|
190
|
+
throw requestError(`steps[${index}].condition must be a supported wait condition.`, {
|
|
191
|
+
condition: source.condition,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const needsLocator = ["VISIBLE", "HIDDEN", "TEXT_CONTAINS"].includes(source.condition);
|
|
195
|
+
const needsExpected = ["TEXT_CONTAINS", "URL_IS", "URL_PREFIX"].includes(source.condition);
|
|
196
|
+
if (needsLocator && (source.locator === undefined || source.locator === null)) {
|
|
197
|
+
throw requestError(`steps[${index}].locator is required for ${source.condition}.`, { step: source });
|
|
198
|
+
}
|
|
199
|
+
if (needsExpected && (source.expected === undefined || source.expected === null)) {
|
|
200
|
+
throw requestError(`steps[${index}].expected is required for ${source.condition}.`, { step: source });
|
|
201
|
+
}
|
|
202
|
+
const normalized = { condition: source.condition };
|
|
203
|
+
if (source.locator !== undefined && source.locator !== null) {
|
|
204
|
+
normalized.locator = normalizeLocator(source.locator, `steps[${index}].locator`);
|
|
205
|
+
}
|
|
206
|
+
if (source.expected !== undefined && source.expected !== null) {
|
|
207
|
+
normalized.expected = toPortable(
|
|
208
|
+
`steps[${index}].expected`,
|
|
209
|
+
source.expected,
|
|
210
|
+
BROWSER_VERIFICATION_LIMITS.maxExpectedChars,
|
|
211
|
+
);
|
|
212
|
+
if (["URL_IS", "URL_PREFIX"].includes(source.condition)) {
|
|
213
|
+
assertHttpOrHttpsUrl(normalized.expected, `steps[${index}].expected`, { allowedOrigins });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return normalized;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeStep(step, index, allowedOrigins) {
|
|
220
|
+
const source = assertPlainObject(step, `steps[${index}]`);
|
|
221
|
+
rejectUnknown(source, ["id", "kind", "url", "locator", "text", "key", "condition", "expected"], `steps[${index}]`);
|
|
222
|
+
const id = toPortable(`steps[${index}].id`, source.id, BROWSER_VERIFICATION_LIMITS.maxStepIdChars);
|
|
223
|
+
if (!BROWSER_VERIFICATION_STEP_KINDS.includes(source.kind)) {
|
|
224
|
+
throw requestError(`steps[${index}].kind must be a supported step kind.`, { kind: source.kind });
|
|
225
|
+
}
|
|
226
|
+
const normalized = { id, kind: source.kind };
|
|
227
|
+
if (source.kind === "NAVIGATE") {
|
|
228
|
+
if (source.url === undefined || source.url === null) {
|
|
229
|
+
throw requestError(`steps[${index}].url is required for NAVIGATE.`, { step: source });
|
|
230
|
+
}
|
|
231
|
+
normalized.url = assertHttpOrHttpsUrl(source.url, `steps[${index}].url`, { allowedOrigins });
|
|
232
|
+
} else if (source.kind === "CLICK") {
|
|
233
|
+
if (source.locator === undefined || source.locator === null) {
|
|
234
|
+
throw requestError(`steps[${index}].locator is required for CLICK.`, { step: source });
|
|
235
|
+
}
|
|
236
|
+
normalized.locator = normalizeLocator(source.locator, `steps[${index}].locator`);
|
|
237
|
+
} else if (source.kind === "FILL") {
|
|
238
|
+
if (source.locator === undefined || source.locator === null) {
|
|
239
|
+
throw requestError(`steps[${index}].locator is required for FILL.`, { step: source });
|
|
240
|
+
}
|
|
241
|
+
normalized.locator = normalizeLocator(source.locator, `steps[${index}].locator`);
|
|
242
|
+
normalized.text = toPortable(`steps[${index}].text`, source.text, BROWSER_VERIFICATION_LIMITS.maxInputChars);
|
|
243
|
+
} else if (source.kind === "PRESS") {
|
|
244
|
+
if (source.locator === undefined || source.locator === null) {
|
|
245
|
+
throw requestError(`steps[${index}].locator is required for PRESS.`, { step: source });
|
|
246
|
+
}
|
|
247
|
+
normalized.locator = normalizeLocator(source.locator, `steps[${index}].locator`);
|
|
248
|
+
normalized.key = toPortable(`steps[${index}].key`, source.key, BROWSER_VERIFICATION_LIMITS.maxInputChars);
|
|
249
|
+
} else if (source.kind === "WAIT_FOR") Object.assign(normalized, normalizeWaitStep(source, index, allowedOrigins));
|
|
250
|
+
return deepFreeze(normalized);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function normalizeAssertion(assertion, index, allowedOrigins) {
|
|
254
|
+
const source = assertPlainObject(assertion, `assertions[${index}]`);
|
|
255
|
+
rejectUnknown(source, ["id", "kind", "locator", "attribute", "expected"], `assertions[${index}]`);
|
|
256
|
+
const id = toPortable(
|
|
257
|
+
`assertions[${index}].id`,
|
|
258
|
+
source.id,
|
|
259
|
+
BROWSER_VERIFICATION_LIMITS.maxAssertionIdChars,
|
|
260
|
+
);
|
|
261
|
+
if (!BROWSER_VERIFICATION_ASSERTION_KINDS.includes(source.kind)) {
|
|
262
|
+
throw requestError(`assertions[${index}].kind must be a supported assertion kind.`, {
|
|
263
|
+
kind: source.kind,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
source.status !== undefined &&
|
|
268
|
+
source.status !== null &&
|
|
269
|
+
!BROWSER_VERIFICATION_ASSERTION_STATUSES.includes(source.status)
|
|
270
|
+
) {
|
|
271
|
+
throw requestError(`assertions[${index}].status must be a supported assertion status.`, {
|
|
272
|
+
status: source.status,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const normalized = { id, kind: source.kind };
|
|
276
|
+
if (["VISIBLE", "HIDDEN", "TEXT_CONTAINS", "TEXT_EQUALS", "VALUE_EQUALS", "ATTRIBUTE_EQUALS"].includes(source.kind)) {
|
|
277
|
+
if (source.locator === undefined || source.locator === null) {
|
|
278
|
+
throw requestError(`assertions[${index}].locator is required for ${source.kind}.`, {
|
|
279
|
+
assertion: source,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
normalized.locator = normalizeLocator(source.locator, `assertions[${index}].locator`);
|
|
283
|
+
}
|
|
284
|
+
if (["TEXT_CONTAINS", "TEXT_EQUALS", "VALUE_EQUALS", "TITLE_EQUALS"].includes(source.kind)) {
|
|
285
|
+
if (source.expected === undefined || source.expected === null) {
|
|
286
|
+
throw requestError(`assertions[${index}].expected is required for ${source.kind}.`, {
|
|
287
|
+
assertion: source,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
normalized.expected = toPortable(
|
|
291
|
+
`assertions[${index}].expected`,
|
|
292
|
+
source.expected,
|
|
293
|
+
BROWSER_VERIFICATION_LIMITS.maxExpectedChars,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (source.kind === "ATTRIBUTE_EQUALS") {
|
|
297
|
+
normalized.attribute = toPortable(
|
|
298
|
+
`assertions[${index}].attribute`,
|
|
299
|
+
source.attribute,
|
|
300
|
+
BROWSER_VERIFICATION_LIMITS.maxAttributeChars,
|
|
301
|
+
);
|
|
302
|
+
if (source.expected === undefined || source.expected === null) {
|
|
303
|
+
throw requestError(`assertions[${index}].expected is required for ATTRIBUTE_EQUALS.`, {
|
|
304
|
+
assertion: source,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
normalized.expected = toPortable(
|
|
308
|
+
`assertions[${index}].expected`,
|
|
309
|
+
source.expected,
|
|
310
|
+
BROWSER_VERIFICATION_LIMITS.maxExpectedChars,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (source.kind === "URL_IS" || source.kind === "URL_PREFIX") {
|
|
314
|
+
if (source.expected === undefined || source.expected === null) {
|
|
315
|
+
throw requestError(`assertions[${index}].expected is required for ${source.kind}.`, {
|
|
316
|
+
assertion: source,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const expected = toPortable(
|
|
320
|
+
`assertions[${index}].expected`,
|
|
321
|
+
source.expected,
|
|
322
|
+
BROWSER_VERIFICATION_LIMITS.maxUrlChars,
|
|
323
|
+
);
|
|
324
|
+
assertHttpOrHttpsUrl(expected, `assertions[${index}].expected`, { allowedOrigins });
|
|
325
|
+
normalized.expected = expected;
|
|
326
|
+
}
|
|
327
|
+
return deepFreeze(normalized);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Validate and freeze a browser verification request.
|
|
332
|
+
*/
|
|
333
|
+
export function normalizeBrowserVerificationRequest(input) {
|
|
334
|
+
const source = assertPlainObject(input, "browser verification request");
|
|
335
|
+
rejectUnknown(source, [
|
|
336
|
+
"verificationId", "taskId", "target", "projectPath", "requirement", "startUrl", "allowedOrigins",
|
|
337
|
+
"viewport", "steps", "assertions", "capture", "timeoutMs",
|
|
338
|
+
], "browser verification request");
|
|
339
|
+
const verificationId = toPortable(
|
|
340
|
+
"verificationId",
|
|
341
|
+
source.verificationId,
|
|
342
|
+
BROWSER_VERIFICATION_LIMITS.maxVerificationIdChars,
|
|
343
|
+
);
|
|
344
|
+
const taskId = toPortable("taskId", source.taskId, BROWSER_VERIFICATION_LIMITS.maxVerificationIdChars);
|
|
345
|
+
const target = toPortable("target", source.target ?? source.projectPath, BROWSER_VERIFICATION_LIMITS.maxUrlChars);
|
|
346
|
+
const requirement = toPortable(
|
|
347
|
+
"requirement",
|
|
348
|
+
source.requirement,
|
|
349
|
+
BROWSER_VERIFICATION_LIMITS.maxRequirementChars,
|
|
350
|
+
);
|
|
351
|
+
const allowedOrigins = normalizeAllowedOrigins(source.allowedOrigins);
|
|
352
|
+
const startUrl = assertHttpOrHttpsUrl(source.startUrl, "startUrl", { allowedOrigins });
|
|
353
|
+
|
|
354
|
+
let viewport;
|
|
355
|
+
if (source.viewport !== undefined && source.viewport !== null) {
|
|
356
|
+
const candidate = assertPlainObject(source.viewport, "viewport");
|
|
357
|
+
rejectUnknown(candidate, ["width", "height"], "viewport");
|
|
358
|
+
if (!Number.isInteger(candidate.width) || candidate.width <= 0 || candidate.width > BROWSER_VERIFICATION_LIMITS.maxViewportWidth
|
|
359
|
+
|| !Number.isInteger(candidate.height) || candidate.height <= 0 || candidate.height > BROWSER_VERIFICATION_LIMITS.maxViewportHeight) {
|
|
360
|
+
throw requestError("viewport width and height must be positive integers within the viewport limits.", { viewport: candidate });
|
|
361
|
+
}
|
|
362
|
+
viewport = deepFreeze({ width: candidate.width, height: candidate.height });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let capture;
|
|
366
|
+
if (source.capture !== undefined && source.capture !== null) {
|
|
367
|
+
const candidate = assertPlainObject(source.capture, "capture");
|
|
368
|
+
rejectUnknown(candidate, ["screenshot"], "capture");
|
|
369
|
+
if (!BROWSER_VERIFICATION_CAPTURE_POLICIES.includes(candidate.screenshot)) {
|
|
370
|
+
throw requestError("capture.screenshot must be a supported capture policy.", { screenshot: candidate.screenshot });
|
|
371
|
+
}
|
|
372
|
+
capture = deepFreeze({ screenshot: candidate.screenshot });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!Array.isArray(source.steps) || source.steps.length === 0) {
|
|
376
|
+
throw requestError("steps must be a non-empty array.", { steps: source.steps });
|
|
377
|
+
}
|
|
378
|
+
if (source.steps.length > BROWSER_VERIFICATION_LIMITS.maxSteps) {
|
|
379
|
+
throw requestError(
|
|
380
|
+
`steps exceeds the ${BROWSER_VERIFICATION_LIMITS.maxSteps}-step limit.`,
|
|
381
|
+
{ count: source.steps.length },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const steps = source.steps.map((step, index) => normalizeStep(step, index, allowedOrigins));
|
|
385
|
+
const stepIds = steps.map((step) => step.id);
|
|
386
|
+
if (new Set(stepIds).size !== stepIds.length) {
|
|
387
|
+
throw requestError("steps must not contain duplicate ids.", { steps: source.steps });
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!Array.isArray(source.assertions) || source.assertions.length === 0) {
|
|
391
|
+
throw requestError("assertions must be a non-empty array.", { assertions: source.assertions });
|
|
392
|
+
}
|
|
393
|
+
if (source.assertions.length > BROWSER_VERIFICATION_LIMITS.maxAssertions) {
|
|
394
|
+
throw requestError(
|
|
395
|
+
`assertions exceeds the ${BROWSER_VERIFICATION_LIMITS.maxAssertions}-assertion limit.`,
|
|
396
|
+
{ count: source.assertions.length },
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const assertions = source.assertions.map((assertion, index) =>
|
|
400
|
+
normalizeAssertion(assertion, index, allowedOrigins),
|
|
401
|
+
);
|
|
402
|
+
const assertionIds = assertions.map((assertion) => assertion.id);
|
|
403
|
+
if (new Set(assertionIds).size !== assertionIds.length) {
|
|
404
|
+
throw requestError("assertions must not contain duplicate ids.", { assertions: source.assertions });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { timeoutMs } = normalizeBrowserVerificationRequestOptions({ timeoutMs: source.timeoutMs });
|
|
408
|
+
|
|
409
|
+
const normalized = {
|
|
410
|
+
verificationId,
|
|
411
|
+
taskId,
|
|
412
|
+
target,
|
|
413
|
+
requirement,
|
|
414
|
+
startUrl,
|
|
415
|
+
allowedOrigins: Object.freeze([...allowedOrigins]),
|
|
416
|
+
steps: Object.freeze([...steps]),
|
|
417
|
+
assertions: Object.freeze([...assertions]),
|
|
418
|
+
timeoutMs,
|
|
419
|
+
...(viewport ? { viewport } : {}),
|
|
420
|
+
...(capture ? { capture } : {}),
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
assertPortableContextSafe(normalized, { label: "browser verification request" });
|
|
424
|
+
} catch (error) {
|
|
425
|
+
throw requestError(`browser verification request failed safety verification: ${error.message}`, {
|
|
426
|
+
cause: error.message,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return deepFreeze(normalized);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function assertBrowserVerificationProvider(provider, { label = "browser verification provider" } = {}) {
|
|
433
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
434
|
+
throw providerError(`${label} must be an object.`, { provider });
|
|
435
|
+
}
|
|
436
|
+
if (typeof provider.id !== "string" || !BROWSER_VERIFICATION_PROVIDER_ID_PATTERN.test(provider.id)) {
|
|
437
|
+
throw providerError(`${label}.id must match [a-z0-9-], start/end alphanumeric, max 64 chars.`, {
|
|
438
|
+
id: provider?.id,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (typeof provider.verify !== "function") {
|
|
442
|
+
throw providerError(`${label}.verify must be a function.`, { id: provider.id });
|
|
443
|
+
}
|
|
444
|
+
if (provider.version !== undefined && typeof provider.version !== "string") {
|
|
445
|
+
throw providerError(`${label}.version must be a string when present.`, {
|
|
446
|
+
version: provider.version,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return provider;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export function assertBrowserVerificationProviderIdentity(providerIdentity, { label = "browser verification provider identity", expectedId } = {}) {
|
|
453
|
+
if (!providerIdentity || typeof providerIdentity !== "object") {
|
|
454
|
+
throw providerError(`${label} must be an object.`, { providerIdentity });
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
typeof providerIdentity.id !== "string" ||
|
|
458
|
+
!BROWSER_VERIFICATION_PROVIDER_ID_PATTERN.test(providerIdentity.id)
|
|
459
|
+
) {
|
|
460
|
+
throw providerError(`${label}.id must match [a-z0-9-], start/end alphanumeric, max 64 chars.`, {
|
|
461
|
+
id: providerIdentity?.id,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
if (expectedId !== undefined && providerIdentity.id !== expectedId) {
|
|
465
|
+
throw providerError(`${label}.id must match registry key "${expectedId}".`, {
|
|
466
|
+
id: providerIdentity.id,
|
|
467
|
+
expectedId,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return deepFreeze({ id: providerIdentity.id });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function assertBrowserVerificationProviderRegistration(provider, expectedId, options = {}) {
|
|
474
|
+
const validated = assertBrowserVerificationProvider(provider, options);
|
|
475
|
+
if (validated.id !== expectedId) {
|
|
476
|
+
throw providerError(`Provider id "${validated.id}" does not match requested registry key "${expectedId}".`, {
|
|
477
|
+
id: validated.id,
|
|
478
|
+
expectedId,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return validated;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function createBrowserVerificationProviderRegistry(providers = {}) {
|
|
485
|
+
if (!providers || typeof providers !== "object" || Array.isArray(providers)) {
|
|
486
|
+
throw providerError("Browser verification provider registry source must be an object.", {
|
|
487
|
+
providers,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const registry = {};
|
|
491
|
+
const entries = providers instanceof Map ? [...providers.entries()] : Object.entries(providers);
|
|
492
|
+
for (const [name, provider] of entries) {
|
|
493
|
+
if (typeof name !== "string" || !BROWSER_VERIFICATION_PROVIDER_ID_PATTERN.test(name)) {
|
|
494
|
+
throw providerError(`Invalid browser verification provider ID: ${name}.`, { name });
|
|
495
|
+
}
|
|
496
|
+
if (typeof provider === "function") {
|
|
497
|
+
registry[name] = provider;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
assertBrowserVerificationProviderRegistration(provider, name, { label: `browser verification provider "${name}"` });
|
|
501
|
+
registry[name] = provider;
|
|
502
|
+
}
|
|
503
|
+
return deepFreeze({ ...registry });
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export async function resolveBrowserVerificationProvider(providers, name, input = {}) {
|
|
507
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
508
|
+
throw unavailableError("Browser verification provider name must be a non-empty string.", { name });
|
|
509
|
+
}
|
|
510
|
+
const registry = providers ?? {};
|
|
511
|
+
const entry = registry instanceof Map ? registry.get(name) : registry[name];
|
|
512
|
+
if (!entry) {
|
|
513
|
+
throw unavailableError(`Browser verification provider "${name}" is not registered.`, { name });
|
|
514
|
+
}
|
|
515
|
+
const provider = typeof entry === "function" ? await entry(input) : entry;
|
|
516
|
+
return assertBrowserVerificationProviderRegistration(provider, name, {
|
|
517
|
+
label: `browser verification provider "${name}"`,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
|
|
3
|
+
E_BROWSER_VERIFICATION_CANCELLED,
|
|
4
|
+
E_BROWSER_VERIFICATION_EXECUTION_FAILED,
|
|
5
|
+
E_BROWSER_VERIFICATION_REQUEST_INVALID,
|
|
6
|
+
E_BROWSER_VERIFICATION_RESULT_INVALID,
|
|
7
|
+
E_BROWSER_VERIFICATION_OUTPUT_LIMIT,
|
|
8
|
+
E_BROWSER_VERIFICATION_ORIGIN_DENIED,
|
|
9
|
+
E_BROWSER_VERIFICATION_PROVIDER_INVALID,
|
|
10
|
+
E_BROWSER_VERIFICATION_TIMEOUT,
|
|
11
|
+
E_BROWSER_VERIFICATION_VERSION_UNSUPPORTED,
|
|
12
|
+
} from "../error-codes.js";
|
|
13
|
+
import { normalizeBrowserVerificationRequestOptions } from "./constants.js";
|
|
14
|
+
import { normalizeBrowserVerificationRequest, resolveBrowserVerificationProvider } from "./provider.js";
|
|
15
|
+
import { normalizeBrowserVerificationResult } from "./normalize.js";
|
|
16
|
+
|
|
17
|
+
function error(code, message, details = null) {
|
|
18
|
+
const output = new Error(message);
|
|
19
|
+
output.code = code;
|
|
20
|
+
output.details = details;
|
|
21
|
+
return output;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function safeFailure(providerName) {
|
|
25
|
+
return error(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, `Browser verification provider "${providerName}" failed.`, { provider: providerName });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function bounded(promiseFactory, { controller, deadline, providerName, verificationId }) {
|
|
29
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
30
|
+
if (remaining() <= 0) throw error(E_BROWSER_VERIFICATION_TIMEOUT, `Browser verification provider "${providerName}" timed out.`, { provider: providerName, verificationId });
|
|
31
|
+
let timer;
|
|
32
|
+
try {
|
|
33
|
+
return await new Promise((resolve, reject) => {
|
|
34
|
+
timer = setTimeout(() => reject(error(E_BROWSER_VERIFICATION_TIMEOUT, `Browser verification provider "${providerName}" timed out.`, { provider: providerName, verificationId })), remaining());
|
|
35
|
+
Promise.resolve().then(() => promiseFactory(remaining())).then(resolve, reject);
|
|
36
|
+
});
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function timeoutError(providerName, verificationId) {
|
|
43
|
+
return error(E_BROWSER_VERIFICATION_TIMEOUT, `Browser verification provider "${providerName}" timed out.`, {
|
|
44
|
+
provider: providerName,
|
|
45
|
+
verificationId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runBrowserVerification({
|
|
50
|
+
target, projectPath, taskId, providerName, verificationId, requirement, startUrl, allowedOrigins,
|
|
51
|
+
viewport, steps, assertions, capture, timeoutMs, runtimeContext,
|
|
52
|
+
} = {}) {
|
|
53
|
+
const effectiveTimeoutMs = normalizeBrowserVerificationRequestOptions({ timeoutMs }).timeoutMs;
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const deadline = Date.now() + effectiveTimeoutMs;
|
|
56
|
+
let normalizedRequest;
|
|
57
|
+
try {
|
|
58
|
+
normalizedRequest = normalizeBrowserVerificationRequest({
|
|
59
|
+
target: target ?? projectPath, taskId, verificationId, requirement, startUrl, allowedOrigins,
|
|
60
|
+
viewport, steps, assertions, capture, timeoutMs: effectiveTimeoutMs,
|
|
61
|
+
});
|
|
62
|
+
} catch (failure) {
|
|
63
|
+
throw failure;
|
|
64
|
+
}
|
|
65
|
+
const providers = runtimeContext?.browserVerificationProviders;
|
|
66
|
+
try {
|
|
67
|
+
const provider = await bounded(
|
|
68
|
+
(remaining) => resolveBrowserVerificationProvider(providers, providerName, {
|
|
69
|
+
signal: controller.signal,
|
|
70
|
+
timeoutMs: remaining,
|
|
71
|
+
request: normalizedRequest,
|
|
72
|
+
}),
|
|
73
|
+
{ controller, deadline, providerName, verificationId: normalizedRequest.verificationId },
|
|
74
|
+
);
|
|
75
|
+
const raw = await bounded(
|
|
76
|
+
(remaining) => provider.verify({ ...normalizedRequest, signal: controller.signal, timeoutMs: remaining }),
|
|
77
|
+
{ controller, deadline, providerName, verificationId: normalizedRequest.verificationId },
|
|
78
|
+
);
|
|
79
|
+
if (controller.signal.aborted) throw error(E_BROWSER_VERIFICATION_TIMEOUT, `Browser verification provider "${providerName}" timed out.`, { provider: providerName });
|
|
80
|
+
if (Date.now() >= deadline) {
|
|
81
|
+
controller.abort();
|
|
82
|
+
throw timeoutError(providerName, normalizedRequest.verificationId);
|
|
83
|
+
}
|
|
84
|
+
const remaining = deadline - Date.now();
|
|
85
|
+
if (remaining <= 0) {
|
|
86
|
+
controller.abort();
|
|
87
|
+
throw timeoutError(providerName, normalizedRequest.verificationId);
|
|
88
|
+
}
|
|
89
|
+
const result = normalizeBrowserVerificationResult(raw, {
|
|
90
|
+
provider, verificationId: normalizedRequest.verificationId, taskId: normalizedRequest.taskId,
|
|
91
|
+
requirement: normalizedRequest.requirement, target: normalizedRequest.target,
|
|
92
|
+
expectedAssertions: normalizedRequest.assertions, allowedOrigins: normalizedRequest.allowedOrigins,
|
|
93
|
+
});
|
|
94
|
+
if (Date.now() >= deadline) {
|
|
95
|
+
controller.abort();
|
|
96
|
+
throw timeoutError(providerName, normalizedRequest.verificationId);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
} catch (failure) {
|
|
100
|
+
if (failure?.code === E_BROWSER_VERIFICATION_TIMEOUT) {
|
|
101
|
+
controller.abort();
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
103
|
+
throw failure;
|
|
104
|
+
}
|
|
105
|
+
if ([E_BROWSER_VERIFICATION_REQUEST_INVALID, E_BROWSER_VERIFICATION_RESULT_INVALID,
|
|
106
|
+
E_BROWSER_VERIFICATION_OUTPUT_LIMIT, E_BROWSER_VERIFICATION_ORIGIN_DENIED,
|
|
107
|
+
E_BROWSER_VERIFICATION_CANCELLED, E_BROWSER_VERIFICATION_EXECUTION_FAILED,
|
|
108
|
+
E_BROWSER_VERIFICATION_VERSION_UNSUPPORTED, E_BROWSER_VERIFICATION_PROVIDER_INVALID].includes(failure?.code)) throw failure;
|
|
109
|
+
if (failure?.code === E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE) {
|
|
110
|
+
if (failure.message.includes("not registered")) throw failure;
|
|
111
|
+
throw safeFailure(providerName);
|
|
112
|
+
}
|
|
113
|
+
throw safeFailure(providerName);
|
|
114
|
+
}
|
|
115
|
+
}
|