@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,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E_BROWSER_VERIFICATION_ORIGIN_DENIED,
|
|
3
|
+
E_BROWSER_VERIFICATION_OUTPUT_LIMIT,
|
|
4
|
+
E_BROWSER_VERIFICATION_RESULT_INVALID,
|
|
5
|
+
} from "../error-codes.js";
|
|
6
|
+
import { deepFreeze, normalizePortableText } from "../portable-context.js";
|
|
7
|
+
import {
|
|
8
|
+
BROWSER_VERIFICATION_ASSERTION_STATUSES,
|
|
9
|
+
BROWSER_VERIFICATION_BLOCKED_RESULT_FIELDS,
|
|
10
|
+
BROWSER_VERIFICATION_LIMITS,
|
|
11
|
+
BROWSER_VERIFICATION_RESULT_FIELDS,
|
|
12
|
+
BROWSER_VERIFICATION_TRUST,
|
|
13
|
+
} from "./constants.js";
|
|
14
|
+
|
|
15
|
+
function resultError(message, details = null, code = E_BROWSER_VERIFICATION_RESULT_INVALID) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
error.code = code;
|
|
18
|
+
error.details = details;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function limitError(message, details) {
|
|
23
|
+
return resultError(message, details, E_BROWSER_VERIFICATION_OUTPUT_LIMIT);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function portable(label, value, maxLength, optional = false) {
|
|
27
|
+
try {
|
|
28
|
+
return normalizePortableText(value, { label, maxLength, optional });
|
|
29
|
+
} catch {
|
|
30
|
+
throw resultError(`${label} is invalid.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function assertSafeObservationText(label, value) {
|
|
35
|
+
if (/(?:authorization\s*:\s*bearer|cookie\s*:|(?:token|secret|password)=|-----begin|file:\/\/|(?:^|\/)Users\/|(?:^|\/)home\/)/i.test(value)) {
|
|
36
|
+
throw resultError(`${label} contains sensitive or non-portable content.`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function strictSnapshot(value, label = "browser verification result", seen = new Set()) {
|
|
42
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
43
|
+
if (typeof value === "number") {
|
|
44
|
+
if (!Number.isFinite(value)) throw resultError(`${label} contains a non-finite number.`);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== "object") throw resultError(`${label} contains an unsupported value.`);
|
|
48
|
+
if (seen.has(value)) throw resultError(`${label} contains a circular reference.`);
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
seen.add(value);
|
|
51
|
+
try {
|
|
52
|
+
const output = [];
|
|
53
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
54
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) throw resultError(`${label} contains a sparse array.`);
|
|
55
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
56
|
+
if (!descriptor || descriptor.get || descriptor.set) throw resultError(`${label}[${index}] is an accessor.`);
|
|
57
|
+
output.push(strictSnapshot(value[index], `${label}[${index}]`, seen));
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
} finally {
|
|
61
|
+
seen.delete(value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (value instanceof Date || value instanceof Map || value instanceof Set || value instanceof Promise
|
|
65
|
+
|| Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
66
|
+
throw resultError(`${label} contains a non-plain object.`);
|
|
67
|
+
}
|
|
68
|
+
seen.add(value);
|
|
69
|
+
try {
|
|
70
|
+
const output = {};
|
|
71
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
72
|
+
if (typeof key !== "string") throw resultError(`${label} contains a symbol key.`);
|
|
73
|
+
if (key === "toJSON") throw resultError(`${label} contains a custom toJSON method.`);
|
|
74
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
75
|
+
if (!descriptor || descriptor.get || descriptor.set) throw resultError(`${label}.${key} is an accessor.`);
|
|
76
|
+
output[key] = strictSnapshot(value[key], `${label}.${key}`, seen);
|
|
77
|
+
}
|
|
78
|
+
return output;
|
|
79
|
+
} finally {
|
|
80
|
+
seen.delete(value);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function rejectAuthorityFields(raw) {
|
|
85
|
+
for (const field of BROWSER_VERIFICATION_BLOCKED_RESULT_FIELDS) {
|
|
86
|
+
if (Object.prototype.hasOwnProperty.call(raw, field)) {
|
|
87
|
+
throw resultError("browser verification result contains a reserved authority field.", { field });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rejectUnknownNestedFields(value, allowed, label) {
|
|
93
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
94
|
+
if (typeof key !== "string" || !allowed.includes(key)) {
|
|
95
|
+
throw resultError(`${label} contains an unsupported field.`, { field: typeof key === "string" ? key : String(key) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function snapshotInput(raw) {
|
|
101
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw resultError("browser verification result must be an object.");
|
|
102
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
103
|
+
if (typeof key !== "string") throw resultError("browser verification result contains a symbol key.");
|
|
104
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, key);
|
|
105
|
+
if (!descriptor || descriptor.get || descriptor.set) throw resultError("browser verification result contains an accessor.", { field: key });
|
|
106
|
+
}
|
|
107
|
+
rejectAuthorityFields(raw);
|
|
108
|
+
return strictSnapshot(raw);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inspectResultShape(raw) {
|
|
112
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw resultError("browser verification result must be an object.");
|
|
113
|
+
for (const key of Reflect.ownKeys(raw)) {
|
|
114
|
+
if (typeof key !== "string") throw resultError("browser verification result contains a symbol key.");
|
|
115
|
+
const descriptor = Object.getOwnPropertyDescriptor(raw, key);
|
|
116
|
+
if (!descriptor || descriptor.get || descriptor.set) throw resultError("browser verification result contains an accessor.");
|
|
117
|
+
}
|
|
118
|
+
rejectAuthorityFields(raw);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertOrigin(value, label, allowedOrigins) {
|
|
122
|
+
if (typeof value !== "string" || value.length > BROWSER_VERIFICATION_LIMITS.maxUrlChars) {
|
|
123
|
+
throw resultError(`${label} is invalid.`);
|
|
124
|
+
}
|
|
125
|
+
let url;
|
|
126
|
+
try { url = new URL(value); } catch { throw resultError(`${label} is invalid.`); }
|
|
127
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
128
|
+
throw resultError(`${label} is invalid.`);
|
|
129
|
+
}
|
|
130
|
+
const origin = `${url.protocol}//${url.hostname.toLowerCase()}${url.port ? `:${url.port}` : ""}`;
|
|
131
|
+
if (!allowedOrigins.includes(origin)) {
|
|
132
|
+
throw resultError(`${label} origin is not allowed.`, { label }, E_BROWSER_VERIFICATION_ORIGIN_DENIED);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeAssertions(rawAssertions, expected) {
|
|
138
|
+
if (!Array.isArray(rawAssertions) || rawAssertions.length !== expected.length) {
|
|
139
|
+
throw resultError("browser verification result assertions must match the requested assertion set.");
|
|
140
|
+
}
|
|
141
|
+
const ids = new Set();
|
|
142
|
+
return rawAssertions.map((raw, index) => {
|
|
143
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw resultError(`assertions[${index}] is invalid.`);
|
|
144
|
+
rejectUnknownNestedFields(raw, ["id", "kind", "status", "actual", "message"], `assertions[${index}]`);
|
|
145
|
+
const expectedAssertion = expected[index];
|
|
146
|
+
const id = portable(`assertions[${index}].id`, raw.id, BROWSER_VERIFICATION_LIMITS.maxAssertionIdChars);
|
|
147
|
+
if (id !== expectedAssertion.id || raw.kind !== expectedAssertion.kind || ids.has(id)) {
|
|
148
|
+
throw resultError("browser verification result assertions do not match the requested assertion set.");
|
|
149
|
+
}
|
|
150
|
+
ids.add(id);
|
|
151
|
+
if (!BROWSER_VERIFICATION_ASSERTION_STATUSES.includes(raw.status)) throw resultError(`assertions[${index}].status is invalid.`);
|
|
152
|
+
const output = { id, kind: raw.kind, status: raw.status };
|
|
153
|
+
if (raw.actual !== undefined) output.actual = assertSafeObservationText(`assertions[${index}].actual`, portable(`assertions[${index}].actual`, raw.actual, BROWSER_VERIFICATION_LIMITS.maxActualChars));
|
|
154
|
+
if (raw.message !== undefined) output.message = assertSafeObservationText(`assertions[${index}].message`, portable(`assertions[${index}].message`, raw.message, BROWSER_VERIFICATION_LIMITS.maxDiagnosticChars));
|
|
155
|
+
return deepFreeze(output);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function deriveStatus(assertions) {
|
|
160
|
+
if (assertions.some(({ status }) => status === "FAIL")) return "FAIL";
|
|
161
|
+
if (assertions.some(({ status }) => status === "BLOCKED")) return "BLOCKED";
|
|
162
|
+
return "PASS";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizeArtifacts(raw) {
|
|
166
|
+
if (raw === undefined) return [];
|
|
167
|
+
if (!Array.isArray(raw)) throw resultError("artifacts must be an array.");
|
|
168
|
+
if (raw.length > BROWSER_VERIFICATION_LIMITS.maxArtifacts) throw limitError("Too many artifacts.", { count: raw.length });
|
|
169
|
+
return raw.map((artifact, index) => {
|
|
170
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) throw resultError(`artifacts[${index}] is invalid.`);
|
|
171
|
+
rejectUnknownNestedFields(artifact, ["kind", "mimeType", "byteLength", "sha256", "ref"], `artifacts[${index}]`);
|
|
172
|
+
const ref = artifact.ref;
|
|
173
|
+
const isWindowsDrivePath = /^[a-z]:[\\/]/i.test(ref);
|
|
174
|
+
const isUncPath = /^(?:\\\\|\/\/)/.test(ref);
|
|
175
|
+
const isFileUrl = /^file:/i.test(ref);
|
|
176
|
+
if (artifact.kind !== "SCREENSHOT" || !["image/png", "image/jpeg"].includes(artifact.mimeType)
|
|
177
|
+
|| !Number.isInteger(artifact.byteLength) || artifact.byteLength <= 0 || artifact.byteLength > BROWSER_VERIFICATION_LIMITS.maxArtifactBytes
|
|
178
|
+
|| typeof artifact.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(artifact.sha256)
|
|
179
|
+
|| typeof artifact.ref !== "string" || artifact.ref.length > BROWSER_VERIFICATION_LIMITS.maxArtifactRefChars
|
|
180
|
+
|| artifact.ref.startsWith("/") || isWindowsDrivePath || isUncPath || isFileUrl
|
|
181
|
+
|| /^(?:https?:\/\/|ftp:\/\/)/i.test(artifact.ref) && (() => {
|
|
182
|
+
try { return Boolean(new URL(artifact.ref).username || new URL(artifact.ref).password); } catch { return true; }
|
|
183
|
+
})()
|
|
184
|
+
|| /(?:token|secret|password|authorization|cookie)=/i.test(artifact.ref)) {
|
|
185
|
+
throw resultError(`artifacts[${index}] is invalid.`);
|
|
186
|
+
}
|
|
187
|
+
return deepFreeze({ ...artifact });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function assertResultSize(value) {
|
|
192
|
+
const serialized = JSON.stringify(value);
|
|
193
|
+
if (serialized.length > BROWSER_VERIFICATION_LIMITS.maxResultChars) {
|
|
194
|
+
throw limitError("Browser verification result exceeds its size limit.", {
|
|
195
|
+
chars: serialized.length,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function normalizeBrowserVerificationResult(raw, {
|
|
201
|
+
provider, verificationId, taskId, requirement, target, expectedAssertions, allowedOrigins = [],
|
|
202
|
+
} = {}) {
|
|
203
|
+
inspectResultShape(raw);
|
|
204
|
+
const snapshot = snapshotInput(raw);
|
|
205
|
+
if (!snapshot || Array.isArray(snapshot)) throw resultError("browser verification result must be an object.");
|
|
206
|
+
for (const field of Object.keys(snapshot)) {
|
|
207
|
+
if (!BROWSER_VERIFICATION_RESULT_FIELDS.includes(field)) throw resultError("browser verification result contains an unsupported field.", { field });
|
|
208
|
+
}
|
|
209
|
+
const expected = Array.isArray(expectedAssertions) ? expectedAssertions : [];
|
|
210
|
+
const assertions = normalizeAssertions(snapshot.assertions, expected);
|
|
211
|
+
if (snapshot.status !== undefined && snapshot.status !== deriveStatus(assertions)) throw resultError("Provider status does not match derived status.");
|
|
212
|
+
const normalized = {
|
|
213
|
+
taskId: portable("taskId", taskId, BROWSER_VERIFICATION_LIMITS.maxVerificationIdChars),
|
|
214
|
+
verificationId: portable("verificationId", verificationId, BROWSER_VERIFICATION_LIMITS.maxVerificationIdChars),
|
|
215
|
+
requirement: portable("requirement", requirement, BROWSER_VERIFICATION_LIMITS.maxRequirementChars),
|
|
216
|
+
target: portable("target", target, BROWSER_VERIFICATION_LIMITS.maxUrlChars),
|
|
217
|
+
provider: { id: provider?.id ?? provider },
|
|
218
|
+
status: deriveStatus(assertions),
|
|
219
|
+
assertions: Object.freeze(assertions),
|
|
220
|
+
diagnostics: Object.freeze((() => {
|
|
221
|
+
if (snapshot.diagnostics === undefined) return [];
|
|
222
|
+
if (!Array.isArray(snapshot.diagnostics)) throw resultError("diagnostics must be an array.");
|
|
223
|
+
if (snapshot.diagnostics.length > BROWSER_VERIFICATION_LIMITS.maxDiagnostics) throw limitError("Too many diagnostics.", { count: snapshot.diagnostics.length });
|
|
224
|
+
return snapshot.diagnostics.map((value, index) => assertSafeObservationText(`diagnostics[${index}]`, portable(`diagnostics[${index}]`, value, BROWSER_VERIFICATION_LIMITS.maxDiagnosticChars)));
|
|
225
|
+
})()),
|
|
226
|
+
snapshots: Object.freeze((() => {
|
|
227
|
+
if (snapshot.snapshots === undefined) return [];
|
|
228
|
+
if (!Array.isArray(snapshot.snapshots)) throw resultError("snapshots must be an array.");
|
|
229
|
+
if (snapshot.snapshots.length > BROWSER_VERIFICATION_LIMITS.maxSnapshots) throw limitError("Too many snapshots.", { count: snapshot.snapshots.length });
|
|
230
|
+
return snapshot.snapshots.map((value, index) => {
|
|
231
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw resultError(`snapshots[${index}] is invalid.`);
|
|
232
|
+
rejectUnknownNestedFields(value, ["kind", "text"], `snapshots[${index}]`);
|
|
233
|
+
if (value.kind !== "ACCESSIBILITY") throw resultError(`snapshots[${index}] is invalid.`);
|
|
234
|
+
return deepFreeze({ kind: "ACCESSIBILITY", text: assertSafeObservationText(`snapshots[${index}].text`, portable(`snapshots[${index}].text`, value.text, BROWSER_VERIFICATION_LIMITS.maxSnapshotChars)) });
|
|
235
|
+
});
|
|
236
|
+
})()),
|
|
237
|
+
artifacts: Object.freeze(normalizeArtifacts(snapshot.artifacts)),
|
|
238
|
+
...BROWSER_VERIFICATION_TRUST,
|
|
239
|
+
};
|
|
240
|
+
if (snapshot.finalUrl !== undefined) normalized.finalUrl = assertOrigin(snapshot.finalUrl, "finalUrl", allowedOrigins);
|
|
241
|
+
else throw resultError("finalUrl is required when browser execution occurs.");
|
|
242
|
+
if (snapshot.navigations !== undefined) {
|
|
243
|
+
if (!Array.isArray(snapshot.navigations)) throw resultError("navigations must be an array.");
|
|
244
|
+
if (snapshot.navigations.length > BROWSER_VERIFICATION_LIMITS.maxNavigations) throw limitError("Too many navigations.", { count: snapshot.navigations.length });
|
|
245
|
+
normalized.navigations = Object.freeze(snapshot.navigations.map((navigation, index) => {
|
|
246
|
+
if (!navigation || typeof navigation !== "object" || Array.isArray(navigation)) throw resultError(`navigations[${index}] is invalid.`);
|
|
247
|
+
rejectUnknownNestedFields(navigation, ["url", "kind"], `navigations[${index}]`);
|
|
248
|
+
if (!navigation || !["NAVIGATE", "REDIRECT"].includes(navigation.kind)) throw resultError(`navigations[${index}] is invalid.`);
|
|
249
|
+
return deepFreeze({ url: assertOrigin(navigation.url, `navigations[${index}].url`, allowedOrigins), kind: navigation.kind });
|
|
250
|
+
}));
|
|
251
|
+
} else normalized.navigations = Object.freeze([]);
|
|
252
|
+
assertResultSize(normalized);
|
|
253
|
+
return deepFreeze(normalized);
|
|
254
|
+
}
|