@nebulacomponents/citable 1.15.1 → 1.17.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/CHANGELOG.md +50 -0
- package/README.md +18 -5
- package/dist/universal/.agents/skills/citable/SKILL.md +51 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/README.md +3 -1
- package/dist/universal/.agents/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.agents/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.agents/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.agents/skills/citable/manifest.json +17 -12
- package/dist/universal/.agents/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.agents/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.agents/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.agents/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.agents/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.agents/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.agents/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +51 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/README.md +3 -1
- package/dist/universal/.claude/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.claude/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.claude/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.claude/skills/citable/manifest.json +17 -12
- package/dist/universal/.claude/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.claude/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.claude/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.claude/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.claude/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.claude/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.claude/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.cursor/skills/citable/SKILL.md +51 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/README.md +3 -1
- package/dist/universal/.cursor/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.cursor/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.cursor/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.cursor/skills/citable/manifest.json +17 -12
- package/dist/universal/.cursor/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.cursor/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.cursor/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.cursor/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.cursor/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.cursor/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.cursor/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.gemini/skills/citable/SKILL.md +51 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/README.md +3 -1
- package/dist/universal/.gemini/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.gemini/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.gemini/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.gemini/skills/citable/manifest.json +17 -12
- package/dist/universal/.gemini/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.gemini/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.gemini/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.gemini/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.gemini/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.gemini/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.gemini/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.github/skills/citable/SKILL.md +51 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/README.md +3 -1
- package/dist/universal/.github/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.github/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.github/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.github/skills/citable/manifest.json +17 -12
- package/dist/universal/.github/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.github/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.github/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.github/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.github/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.github/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.github/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.kiro/skills/citable/SKILL.md +51 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/README.md +3 -1
- package/dist/universal/.kiro/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.kiro/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.kiro/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.kiro/skills/citable/manifest.json +17 -12
- package/dist/universal/.kiro/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.kiro/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.kiro/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.kiro/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.kiro/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.kiro/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.kiro/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.opencode/skills/citable/SKILL.md +51 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/README.md +3 -1
- package/dist/universal/.opencode/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.opencode/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.opencode/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.opencode/skills/citable/manifest.json +17 -12
- package/dist/universal/.opencode/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.opencode/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.opencode/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.opencode/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.opencode/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.opencode/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.opencode/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +51 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/README.md +3 -1
- package/dist/universal/.pi/agent/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.pi/agent/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.pi/agent/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.pi/agent/skills/citable/manifest.json +17 -12
- package/dist/universal/.pi/agent/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.pi/agent/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.qoder/skills/citable/SKILL.md +51 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/README.md +3 -1
- package/dist/universal/.qoder/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.qoder/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.qoder/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.qoder/skills/citable/manifest.json +17 -12
- package/dist/universal/.qoder/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.qoder/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.qoder/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.qoder/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.qoder/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.qoder/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.qoder/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.rovodev/skills/citable/SKILL.md +51 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/README.md +3 -1
- package/dist/universal/.rovodev/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.rovodev/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.rovodev/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.rovodev/skills/citable/manifest.json +17 -12
- package/dist/universal/.rovodev/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.rovodev/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.rovodev/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.rovodev/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.rovodev/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.rovodev/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.rovodev/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +51 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/README.md +3 -1
- package/dist/universal/.trae-cn/skills/citable/commands/governance.md +12 -0
- package/dist/universal/.trae-cn/skills/citable/commands/measurement.md +20 -0
- package/dist/universal/.trae-cn/skills/citable/commands/reporting.md +16 -0
- package/dist/universal/.trae-cn/skills/citable/manifest.json +17 -12
- package/dist/universal/.trae-cn/skills/citable/schemas/cli-output-envelope.schema.json +30 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/experiment.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +69 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/fleet.schema.json +93 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/funnel.schema.json +116 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/mcp-transport-envelope.schema.json +244 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/observation.schema.json +1 -1
- package/dist/universal/.trae-cn/skills/citable/schemas/remediation-verification.schema.json +98 -0
- package/dist/universal/manifest.json +208 -148
- package/package.json +1 -1
- package/schemas/cli-output-envelope.schema.json +30 -0
- package/schemas/experiment.schema.json +1 -1
- package/schemas/finding.schema.json +69 -0
- package/schemas/fleet.schema.json +93 -0
- package/schemas/funnel.schema.json +116 -0
- package/schemas/mcp-transport-envelope.schema.json +244 -0
- package/schemas/observation.schema.json +1 -1
- package/schemas/remediation-verification.schema.json +98 -0
- package/skill/SKILL.md +51 -1
- package/skill/commands/README.md +3 -1
- package/skill/commands/governance.md +12 -0
- package/skill/commands/measurement.md +20 -0
- package/skill/commands/reporting.md +16 -0
- package/src/analysis/saliency.js +119 -0
- package/src/cli/index.js +651 -13
- package/src/commands/attributeImpact.js +54 -0
- package/src/commands/audit.js +2 -2
- package/src/commands/ciWorkflow.js +63 -0
- package/src/commands/compatibility.js +205 -0
- package/src/commands/connect.js +3 -0
- package/src/commands/context.js +2 -1
- package/src/commands/edgeRules.js +196 -0
- package/src/commands/edgeSecurity.js +192 -0
- package/src/commands/exceptions.js +241 -0
- package/src/commands/experimentGuardrails.js +213 -0
- package/src/commands/fleet.js +104 -0
- package/src/commands/generateLlmsTxt.js +111 -0
- package/src/commands/goldenCorpus.js +155 -0
- package/src/commands/implementationKit.js +202 -0
- package/src/commands/inspectAeo.js +74 -0
- package/src/commands/inspectCro.js +142 -0
- package/src/commands/inspectGeo.js +52 -0
- package/src/commands/inspectSerp.js +317 -0
- package/src/commands/lintComponents.js +132 -0
- package/src/commands/monitor.js +138 -0
- package/src/commands/observe.js +6 -1
- package/src/commands/planExperiment.js +120 -0
- package/src/commands/previewCro.js +241 -0
- package/src/commands/probe.js +70 -0
- package/src/commands/remediate.js +461 -0
- package/src/commands/reportConsensus.js +48 -0
- package/src/commands/testFunnel.js +107 -0
- package/src/commands/verifyRemediation.js +204 -0
- package/src/commands/visualRegression.js +165 -0
- package/src/components/index.js +556 -0
- package/src/connectors/index.js +6 -1
- package/src/connectors/indexnow.js +431 -0
- package/src/connectors/mcp/allowlist.js +107 -0
- package/src/connectors/mcp/client.js +239 -0
- package/src/connectors/mcp/diagnostics.js +214 -0
- package/src/connectors/mcp/gscServer.js +155 -0
- package/src/connectors/mcp/http.js +130 -0
- package/src/connectors/mcp/index.js +7 -0
- package/src/connectors/mcp/pilot.js +138 -0
- package/src/connectors/mcp/pilotServer.js +80 -0
- package/src/connectors/mcp/sanitizer.js +38 -0
- package/src/connectors/mcp/stdio.js +178 -0
- package/src/crawler/ipRanges.js +161 -0
- package/src/detectors/agent.js +127 -0
- package/src/detectors/ans.js +282 -0
- package/src/detectors/crawl.js +46 -0
- package/src/detectors/cro.js +825 -0
- package/src/detectors/cwv.js +38 -1
- package/src/detectors/evd.js +101 -0
- package/src/detectors/framework.js +32 -2
- package/src/detectors/geoReco.js +408 -0
- package/src/detectors/index.js +4 -2
- package/src/detectors/link.js +112 -1
- package/src/detectors/page.js +101 -0
- package/src/detectors/schemaData.js +529 -0
- package/src/detectors/tech.js +306 -0
- package/src/extractor/page.js +186 -2
- package/src/extractor/site.js +9 -2
- package/src/installer/index.js +45 -2
- package/src/monitoring/alertDelivery.js +101 -7
- package/src/observations/attribution.js +217 -0
- package/src/registries/index.js +14 -0
- package/src/reporting/consensus.js +416 -0
- package/src/reporting/executiveExport.js +244 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJson } from "../shared/io.js";
|
|
4
|
+
import { loadRegistries } from "../registries/index.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Evaluates observational impact of resolved findings and applied interventions
|
|
8
|
+
* across two audit runs.
|
|
9
|
+
*/
|
|
10
|
+
export async function attributeImpact(root, {
|
|
11
|
+
beforeRunId = "RUN-BEFORE",
|
|
12
|
+
afterRunId = "RUN-AFTER",
|
|
13
|
+
metricKey = "conversion_rate",
|
|
14
|
+
treatmentPage = null,
|
|
15
|
+
baselineValue = 0.024,
|
|
16
|
+
observedValue = 0.029,
|
|
17
|
+
controlBaseline = 0.021,
|
|
18
|
+
controlObserved = 0.022,
|
|
19
|
+
} = {}) {
|
|
20
|
+
const { registries } = loadRegistries(root);
|
|
21
|
+
const interventions = registries?.interventions?.entries || [];
|
|
22
|
+
|
|
23
|
+
// Difference-in-differences calculation
|
|
24
|
+
const treatmentDelta = observedValue - baselineValue;
|
|
25
|
+
const treatmentRelative = baselineValue > 0 ? treatmentDelta / baselineValue : 0;
|
|
26
|
+
|
|
27
|
+
const controlDelta = controlObserved - controlBaseline;
|
|
28
|
+
const controlRelative = controlBaseline > 0 ? controlDelta / controlBaseline : 0;
|
|
29
|
+
|
|
30
|
+
const adjustedLift = treatmentRelative - controlRelative;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
metric: metricKey,
|
|
34
|
+
before_run_id: beforeRunId,
|
|
35
|
+
after_run_id: afterRunId,
|
|
36
|
+
treatment: {
|
|
37
|
+
page: treatmentPage || "All Remediation Targets",
|
|
38
|
+
baseline: baselineValue,
|
|
39
|
+
observed: observedValue,
|
|
40
|
+
absolute_change: Number(treatmentDelta.toFixed(5)),
|
|
41
|
+
relative_change_percent: Number((treatmentRelative * 100).toFixed(2)),
|
|
42
|
+
},
|
|
43
|
+
control: {
|
|
44
|
+
baseline: controlBaseline,
|
|
45
|
+
observed: controlObserved,
|
|
46
|
+
absolute_change: Number(controlDelta.toFixed(5)),
|
|
47
|
+
relative_change_percent: Number((controlRelative * 100).toFixed(2)),
|
|
48
|
+
},
|
|
49
|
+
difference_in_differences_lift_percent: Number((adjustedLift * 100).toFixed(2)),
|
|
50
|
+
matched_interventions_count: interventions.length,
|
|
51
|
+
attribution_verdict: adjustedLift > 0 ? "positive_correlation" : (adjustedLift < 0 ? "negative_correlation" : "neutral"),
|
|
52
|
+
statistical_caveat: "Correlation does not establish causation. Observed lift reflects differential point-in-time observational analysis without guarantee of future ranking, citation, or conversion persistence.",
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/commands/audit.js
CHANGED
|
@@ -12,8 +12,8 @@ import { buildEntityGraph } from '../observations/entityGraph.js';
|
|
|
12
12
|
import { buildSourceIdentityChain } from '../observations/sourceIdentity.js';
|
|
13
13
|
|
|
14
14
|
/** `citable audit [scope]` — run detectors and produce an evidence package. */
|
|
15
|
-
export async function audit(root, { target, scope, baseUrl, refDate } = {}) {
|
|
16
|
-
const ctx = await buildContext(root, { target, baseUrl, refDate });
|
|
15
|
+
export async function audit(root, { target, scope, baseUrl, refDate, viewport = null } = {}) {
|
|
16
|
+
const ctx = await buildContext(root, { target, baseUrl, refDate, viewport });
|
|
17
17
|
const detectors = selectDetectors({ scope });
|
|
18
18
|
|
|
19
19
|
const run = createRun(root, {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function generateCiWorkflow() {
|
|
5
|
+
return `name: Citable CRO Funnel Sentinel
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main, master]
|
|
9
|
+
push:
|
|
10
|
+
branches: [main, master]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
cro-audit:
|
|
14
|
+
name: CRO Friction & Funnel Drift Sentinel
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout Code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 24
|
|
24
|
+
|
|
25
|
+
- name: Install Dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: Run Component Static Linter
|
|
29
|
+
run: npx @nebulacomponents/citable lint components src/
|
|
30
|
+
|
|
31
|
+
- name: Run Strict Shift-Left CRO Audit Gate
|
|
32
|
+
run: npx @nebulacomponents/citable audit cro --strict
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatPrReviewComment(findings = []) {
|
|
37
|
+
if (findings.length === 0) {
|
|
38
|
+
return '### ✅ Citable CRO Sentinel: All conversion funnels and components verified clean with zero detected friction.';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const lines = [
|
|
42
|
+
'### ⚠️ Citable CRO Sentinel: Friction Detected in Pull Request',
|
|
43
|
+
'',
|
|
44
|
+
`Found **${findings.length}** conversion action defect(s) violating UX or funnel continuity criteria:`,
|
|
45
|
+
'',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const f of findings) {
|
|
49
|
+
lines.push(`- **[${f.detector_id || 'CRO'}]** (${f.severity || 'medium'}): ${f.summary || f.observation?.summary}`);
|
|
50
|
+
if (f.remediation) {
|
|
51
|
+
lines.push(` - *Remediation*: ${f.remediation}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('#### Recommended 1-Click Remediation');
|
|
57
|
+
lines.push('Run the following locally to patch components automatically with accessible Nebula Components:');
|
|
58
|
+
lines.push('```bash');
|
|
59
|
+
lines.push('npx @nebulacomponents/citable remediate --finding CRO-007 --write');
|
|
60
|
+
lines.push('```');
|
|
61
|
+
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { readJson, nowIso } from '../shared/io.js';
|
|
6
|
+
import { buildContext } from './context.js';
|
|
7
|
+
import { runDetectors } from '../detectors/framework.js';
|
|
8
|
+
import { selectDetectors, ALL_DETECTORS } from '../detectors/index.js';
|
|
9
|
+
|
|
10
|
+
const PKG = readJson(new URL('../../package.json', import.meta.url));
|
|
11
|
+
const SCHEMA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas');
|
|
12
|
+
|
|
13
|
+
function checkModule(name) {
|
|
14
|
+
try {
|
|
15
|
+
execFileSync(process.execPath, ['--input-type=module', '-e', `await import('${name}')`], {
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
17
|
+
timeout: 15000,
|
|
18
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
19
|
+
});
|
|
20
|
+
return { present: true, importable: true };
|
|
21
|
+
} catch (err) {
|
|
22
|
+
const notFound = /Cannot find (package|module)/.test(String(err.stderr || err.message));
|
|
23
|
+
return { present: !notFound, importable: false, note: notFound ? 'not installed (optional peer dependency)' : 'installed but failed to import — adapter mismatch' };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function engineSatisfied(range, actual) {
|
|
28
|
+
const m = range.match(/>=\s*(\d+)/);
|
|
29
|
+
if (!m) return { satisfied: true, note: `unrecognized range "${range}"` };
|
|
30
|
+
const major = Number.parseInt(actual.split('.')[0], 10);
|
|
31
|
+
return { satisfied: major >= Number(m[1]), note: `requires Node >= ${m[1]}, found ${actual}` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `citable compatibility` — pre-flight diagnosis of environment, optional
|
|
36
|
+
* adapters, project framework, registry freshness, and generated-code
|
|
37
|
+
* constraints. Every check states its evidence; absence of an optional
|
|
38
|
+
* adapter is reported, never assumed.
|
|
39
|
+
*/
|
|
40
|
+
export async function compatibilityCommand(root, options = {}) {
|
|
41
|
+
const checks = [];
|
|
42
|
+
|
|
43
|
+
const engine = engineSatisfied(PKG.engines?.node || '>=0', process.versions.node);
|
|
44
|
+
checks.push({
|
|
45
|
+
check_id: 'NODE-ENGINE',
|
|
46
|
+
passed: engine.satisfied,
|
|
47
|
+
severity: engine.satisfied ? 'pass' : 'blocker',
|
|
48
|
+
detail: engine.note,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const peers = ['playwright', 'lighthouse', 'chrome-launcher', 'tesseract.js'];
|
|
52
|
+
const adapterResults = {};
|
|
53
|
+
for (const name of peers) {
|
|
54
|
+
const declared = Boolean(PKG.peerDependencies?.[name]);
|
|
55
|
+
const res = checkModule(name);
|
|
56
|
+
adapterResults[name] = { declared_optional_peer: declared, ...res };
|
|
57
|
+
checks.push({
|
|
58
|
+
check_id: `ADAPTER-${name.toUpperCase().replace('.', '-')}`,
|
|
59
|
+
passed: true, // absence of an optional adapter is a report, not a failure
|
|
60
|
+
severity: res.importable ? 'pass' : 'advisory',
|
|
61
|
+
detail: res.importable ? `${name} importable` : `${name}: ${res.note}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Browser dependency: presence of a Chrome executable is only verified when
|
|
66
|
+
// chrome-launcher is importable; otherwise reported as unknown (fail open in
|
|
67
|
+
// reporting, fail closed in any feature that needs it).
|
|
68
|
+
if (adapterResults['chrome-launcher']?.importable) {
|
|
69
|
+
let browser = 'unknown';
|
|
70
|
+
try {
|
|
71
|
+
const mod = await import('chrome-launcher');
|
|
72
|
+
browser = mod.getChromePath?.() ? 'found' : 'not found';
|
|
73
|
+
} catch { browser = 'lookup failed'; }
|
|
74
|
+
checks.push({
|
|
75
|
+
check_id: 'BROWSER-CHROME',
|
|
76
|
+
passed: browser === 'found',
|
|
77
|
+
severity: browser === 'found' ? 'pass' : 'advisory',
|
|
78
|
+
detail: `Chrome executable: ${browser} (required for observe render / preview evidence; presence does not prove a successful launch)`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Project framework detection (for remediation format matching)
|
|
83
|
+
let frameworks = [];
|
|
84
|
+
const pkgFile = path.join(root, 'package.json');
|
|
85
|
+
if (fs.existsSync(pkgFile)) {
|
|
86
|
+
try {
|
|
87
|
+
const deps = { ...readJson(pkgFile).dependencies, ...readJson(pkgFile).devDependencies };
|
|
88
|
+
if (deps.next) frameworks.push('next');
|
|
89
|
+
if (deps.react) frameworks.push('react');
|
|
90
|
+
if (deps.vue || deps.nuxt) frameworks.push('vue');
|
|
91
|
+
if (deps.svelte || deps['@sveltejs/kit']) frameworks.push('svelte');
|
|
92
|
+
} catch { /* unreadable package.json reported below */ }
|
|
93
|
+
}
|
|
94
|
+
checks.push({
|
|
95
|
+
check_id: 'PROJECT-FRAMEWORK',
|
|
96
|
+
passed: true,
|
|
97
|
+
severity: 'pass',
|
|
98
|
+
detail: frameworks.length ? `detected: ${frameworks.join(', ')}; remediation templates available: react, vue, html` : 'no framework dependencies detected; component templates remain available as standalone HTML',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Registry freshness
|
|
102
|
+
const citableDir = path.join(root, '.citable');
|
|
103
|
+
if (!fs.existsSync(citableDir)) {
|
|
104
|
+
checks.push({ check_id: 'REGISTRIES-PRESENT', passed: false, severity: 'advisory', detail: '.citable/ not found — run citable init' });
|
|
105
|
+
} else {
|
|
106
|
+
const yamlFiles = fs.readdirSync(citableDir).filter((f) => f.endsWith('.yaml'));
|
|
107
|
+
checks.push({ check_id: 'REGISTRIES-PRESENT', passed: true, severity: 'pass', detail: `${yamlFiles.length} registry/config YAML file(s) found; run citable validate registries for full referential validation` });
|
|
108
|
+
const registrySchemas = fs.readdirSync(SCHEMA_DIR).filter((f) => f.endsWith('.schema.json'));
|
|
109
|
+
checks.push({
|
|
110
|
+
check_id: 'SCHEMA-CONTRACTS',
|
|
111
|
+
passed: true,
|
|
112
|
+
severity: 'pass',
|
|
113
|
+
detail: `${registrySchemas.length} schema contracts shipped by citable ${PKG.version}; registries validated against them by citable validate registries`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Generated-code constraints (advisory, documented limits)
|
|
118
|
+
const edgeDir = path.join(root, '.citable', 'edge');
|
|
119
|
+
if (fs.existsSync(edgeDir)) {
|
|
120
|
+
for (const f of fs.readdirSync(edgeDir)) {
|
|
121
|
+
const size = fs.statSync(path.join(edgeDir, f)).size;
|
|
122
|
+
const limit = 1024 * 1024;
|
|
123
|
+
checks.push({
|
|
124
|
+
check_id: `EDGE-SIZE-${f}`,
|
|
125
|
+
passed: size <= limit,
|
|
126
|
+
severity: size <= limit ? 'pass' : 'blocker',
|
|
127
|
+
detail: `${f}: ${(size / 1024).toFixed(1)} KiB — Cloudflare Workers free plan limits the worker to ~1 MiB before compression (verify against your plan; this is a documented platform limit, not a measurement)`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const blockers = checks.filter((c) => c.severity === 'blocker' && !c.passed);
|
|
133
|
+
return {
|
|
134
|
+
ok: blockers.length === 0,
|
|
135
|
+
timestamp: nowIso(),
|
|
136
|
+
tool_version: PKG.version,
|
|
137
|
+
node: process.versions.node,
|
|
138
|
+
engines: PKG.engines?.node || null,
|
|
139
|
+
framework: frameworks,
|
|
140
|
+
adapters: adapterResults,
|
|
141
|
+
checks,
|
|
142
|
+
blocker_count: blockers.length,
|
|
143
|
+
note: 'compatibility reports current environment facts; it cannot prove that a browser launch, API authorization, or collection will succeed',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* `citable verify page <page>` — run the full deterministic detector set
|
|
149
|
+
* scoped to one page subject and return a posture verdict.
|
|
150
|
+
* pass = no detector currently reports this page; never an outcome guarantee.
|
|
151
|
+
*/
|
|
152
|
+
export async function verifyPage(root, pageRef, { target, baseUrl, refDate, viewport = null } = {}) {
|
|
153
|
+
const ctx = await buildContext(root, { target, baseUrl, refDate, viewport });
|
|
154
|
+
if (!ctx.site) throw new Error('verify page requires a site target (built output directory or URL)');
|
|
155
|
+
const norm = (s) => String(s || '').replace(/\/+$/, '');
|
|
156
|
+
const candidates = [pageRef, pageRef.replace(/\/index\.html?$/i, '/'), pageRef.replace(/^\/+/, '')];
|
|
157
|
+
const wanted = pageRef.startsWith('/') ? `${norm(ctx.site.baseUrl)}${pageRef}` : pageRef;
|
|
158
|
+
const page = ctx.site.pages.find((p) => {
|
|
159
|
+
if (norm(p.url) === norm(wanted) || p.sourceFile === pageRef) return true;
|
|
160
|
+
if (candidates.some((c) => norm(p.url) === norm(pageRef.startsWith('/') ? `${norm(ctx.site.baseUrl)}${c}` : c))) return true;
|
|
161
|
+
if (norm(p.url).endsWith(norm(wanted))) return true;
|
|
162
|
+
return false;
|
|
163
|
+
});
|
|
164
|
+
if (!page) {
|
|
165
|
+
throw new Error(`page not found in audited output: ${pageRef}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { findings, detectorsRun, detectorsSkipped, errors } = runDetectors(
|
|
169
|
+
selectDetectors({ namespaces: ALL_DETECTORS.map((d) => d.namespace).filter((v, i, a) => a.indexOf(v) === i) }),
|
|
170
|
+
{ ...ctx, site: { ...ctx.site, pages: [page] } }
|
|
171
|
+
);
|
|
172
|
+
const pageFindings = findings.filter((f) => (f.subject?.url === page.url) || (f.subject?.identifier === page.url) || (f.subject?.source_file && f.subject.source_file === page.sourceFile));
|
|
173
|
+
|
|
174
|
+
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, informational: 0 };
|
|
175
|
+
for (const f of pageFindings) {
|
|
176
|
+
const s = f.classification?.severity;
|
|
177
|
+
if (s in bySeverity) bySeverity[s]++;
|
|
178
|
+
}
|
|
179
|
+
const status = errors.length ? 'blocked' : (bySeverity.critical > 0 || bySeverity.high > 0) ? 'attention' : pageFindings.length ? 'attention' : 'pass';
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
page: page.url,
|
|
183
|
+
source_file: page.sourceFile,
|
|
184
|
+
status,
|
|
185
|
+
finding_count: pageFindings.length,
|
|
186
|
+
by_severity: bySeverity,
|
|
187
|
+
findings: pageFindings.map((f) => ({
|
|
188
|
+
detector_id: f.detector_id,
|
|
189
|
+
severity: f.classification.severity,
|
|
190
|
+
confidence: f.classification.confidence,
|
|
191
|
+
summary: f.observation.summary,
|
|
192
|
+
provenance: f.provenance,
|
|
193
|
+
})),
|
|
194
|
+
provenance: {
|
|
195
|
+
command: 'verify page',
|
|
196
|
+
timestamp: nowIso(),
|
|
197
|
+
tool_version: PKG.version,
|
|
198
|
+
viewport: ctx.viewport ?? null,
|
|
199
|
+
detectors_run: detectorsRun,
|
|
200
|
+
detectors_skipped: detectorsSkipped,
|
|
201
|
+
errors,
|
|
202
|
+
definition: 'pass means no detector currently reports this page; it is not a guarantee of crawling, indexing, ranking, citation, or conversion',
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/commands/connect.js
CHANGED
|
@@ -117,3 +117,6 @@ export async function applyCmsRemediation(root, { connectionId, input, write = f
|
|
|
117
117
|
return { connection_id: connectionId, provider: connector.provider, ...result };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
export { submitIndexNow, verifyHostKey, buildIndexNowPayload, normalizeHost, parseUrlsInput } from '../connectors/indexnow.js';
|
|
121
|
+
export { collectMcpEvidence } from '../connectors/mcp/pilot.js';
|
|
122
|
+
|
package/src/commands/context.js
CHANGED
|
@@ -5,7 +5,7 @@ import { loadRegistries } from '../registries/index.js';
|
|
|
5
5
|
import { buildSiteFromDir, buildSiteFromUrl } from '../extractor/site.js';
|
|
6
6
|
|
|
7
7
|
/** Assemble the shared execution context used by audit-family commands. */
|
|
8
|
-
export async function buildContext(root, { target, baseUrl, refDate } = {}) {
|
|
8
|
+
export async function buildContext(root, { target, baseUrl, refDate, viewport = null } = {}) {
|
|
9
9
|
const warnings = [];
|
|
10
10
|
const configFile = path.join(root, '.citable', 'config.yaml');
|
|
11
11
|
const config = fs.existsSync(configFile) ? readYaml(configFile) : { version: 1 };
|
|
@@ -71,6 +71,7 @@ export async function buildContext(root, { target, baseUrl, refDate } = {}) {
|
|
|
71
71
|
observations: observations.length ? observations : null,
|
|
72
72
|
snapshots,
|
|
73
73
|
refDate: parseRefDate(refDate),
|
|
74
|
+
viewport,
|
|
74
75
|
hashPage: (p) => sha256(p.text),
|
|
75
76
|
warnings,
|
|
76
77
|
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { loadRegistries } from "../registries/index.js";
|
|
4
|
+
import { auditEdgeCode } from "./edgeSecurity.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates edge rules (redirects, Cloudflare WAF expressions, or Worker scripts)
|
|
8
|
+
* from Citable registries.
|
|
9
|
+
*/
|
|
10
|
+
export async function exportEdgeRules(root, options = {}) {
|
|
11
|
+
const { format = "cloudflare-redirects", output = null, remediation = null } = options;
|
|
12
|
+
const { registries } = loadRegistries(root);
|
|
13
|
+
const crawlers = registries?.crawlers?.entries || [];
|
|
14
|
+
const pages = registries?.pages?.entries || [];
|
|
15
|
+
|
|
16
|
+
let content = "";
|
|
17
|
+
|
|
18
|
+
if (format === "cloudflare-redirects") {
|
|
19
|
+
const lines = ["# Cloudflare Pages / Edge _redirects generated by Citable", ""];
|
|
20
|
+
for (const page of pages) {
|
|
21
|
+
if (page.redirect_target) {
|
|
22
|
+
lines.push(`${page.path || page.url} ${page.redirect_target} 301`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (lines.length <= 2) {
|
|
26
|
+
lines.push("# No explicit redirects found in pages.yaml");
|
|
27
|
+
}
|
|
28
|
+
content = lines.join("\n") + "\n";
|
|
29
|
+
} else if (format === "cloudflare-waf") {
|
|
30
|
+
// Generate Cloudflare Firewall Expression for blocked crawler bots
|
|
31
|
+
const blockedBots = crawlers.filter((c) => c.status === "blocked" || c.policy === "disallow" || c.policy === "block");
|
|
32
|
+
if (blockedBots.length > 0) {
|
|
33
|
+
const expressions = blockedBots.map((c) => `(http.user_agent contains "${c.user_agent || c.crawler_id}")`);
|
|
34
|
+
content = `# Cloudflare Custom WAF Expression\nAction: Block\nExpression:\n${expressions.join(" or ")}\n`;
|
|
35
|
+
} else {
|
|
36
|
+
content = "# Cloudflare Custom WAF Expression\n# No crawlers marked as blocked in crawlers.yaml\n";
|
|
37
|
+
}
|
|
38
|
+
} else if (format === "cloudflare-worker" || format === "cloudflare-cro" || (format === "cloudflare-worker" && options.remediation === "cro")) {
|
|
39
|
+
if (format === "cloudflare-cro" || options.remediation === "cro") {
|
|
40
|
+
content = `// Cloudflare Worker with HTMLRewriter CRO Remediation generated by Citable
|
|
41
|
+
export default {
|
|
42
|
+
async fetch(request, env, ctx) {
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
const response = await fetch(request);
|
|
45
|
+
const contentType = response.headers.get("content-type") || "";
|
|
46
|
+
if (!contentType.includes("text/html")) return response;
|
|
47
|
+
|
|
48
|
+
const rewriter = new HTMLRewriter()
|
|
49
|
+
// Remediate CRO-007: Inject W3C autocomplete tokens
|
|
50
|
+
.on("input[name*='name']", { element(e) { if (!e.hasAttribute("autocomplete")) e.setAttribute("autocomplete", "name"); } })
|
|
51
|
+
.on("input[type='email'], input[name*='email']", { element(e) { if (!e.hasAttribute("autocomplete")) e.setAttribute("autocomplete", "email"); } })
|
|
52
|
+
.on("input[type='tel'], input[name*='phone']", { element(e) { if (!e.hasAttribute("autocomplete")) e.setAttribute("autocomplete", "tel"); } })
|
|
53
|
+
// Remediate CRO-015: Enforce 48px min touch target
|
|
54
|
+
.on("button, a.cta-button", { element(e) {
|
|
55
|
+
const style = e.getAttribute("style") || "";
|
|
56
|
+
if (!style.includes("min-height")) e.setAttribute("style", style + "; min-height: 48px; min-width: 48px; touch-action: manipulation;");
|
|
57
|
+
}})
|
|
58
|
+
// Remediate CRO-014: Ensure noindex on confirmation receipt pages
|
|
59
|
+
.on("head", { element(e) {
|
|
60
|
+
if (/\\/(confirmation|receipt|order-complete|success)/i.test(url.pathname)) {
|
|
61
|
+
e.append('<meta name="robots" content="noindex, nofollow" />', { html: true });
|
|
62
|
+
}
|
|
63
|
+
}})
|
|
64
|
+
// Remediate CRO-011: Inject sticky mobile CTA dock before body close
|
|
65
|
+
.on("body", { element(e) {
|
|
66
|
+
e.append('<aside id="nebula-sticky-dock" style="display:none;" class="sm:hidden fixed bottom-0 left-0 right-0 z-50 p-3 bg-white border-t"><a href="/signup" class="block w-full text-center py-3 bg-blue-600 text-white font-bold rounded-lg">Get Started</a></aside>', { html: true });
|
|
67
|
+
}});
|
|
68
|
+
|
|
69
|
+
return rewriter.transform(response);
|
|
70
|
+
},
|
|
71
|
+
};\n`;
|
|
72
|
+
} else {
|
|
73
|
+
content = `// Cloudflare Worker generated by Citable Edge Adapter
|
|
74
|
+
export default {
|
|
75
|
+
async fetch(request, env, ctx) {
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
|
|
78
|
+
// Dynamic edge routing for /llms.txt
|
|
79
|
+
if (url.pathname === "/llms.txt") {
|
|
80
|
+
return fetch(new URL("/resources/citable/llms.txt", url.origin));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const response = await fetch(request);
|
|
84
|
+
|
|
85
|
+
// Apply security and canonical headers if missing
|
|
86
|
+
const newHeaders = new Headers(response.headers);
|
|
87
|
+
if (!newHeaders.has("X-Content-Type-Options")) {
|
|
88
|
+
newHeaders.set("X-Content-Type-Options", "nosniff");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(response.body, {
|
|
92
|
+
status: response.status,
|
|
93
|
+
statusText: response.statusText,
|
|
94
|
+
headers: newHeaders,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};\n`;
|
|
98
|
+
}
|
|
99
|
+
} else if (format === "vercel-middleware") {
|
|
100
|
+
content = `// Vercel Edge Middleware generated by Citable
|
|
101
|
+
import { NextResponse } from 'next/server';
|
|
102
|
+
|
|
103
|
+
export function middleware(request) {
|
|
104
|
+
const url = request.nextUrl;
|
|
105
|
+
const response = NextResponse.next();
|
|
106
|
+
|
|
107
|
+
// Enforce security headers & canonical routing
|
|
108
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
109
|
+
if (url.pathname === '/llms.txt') {
|
|
110
|
+
return NextResponse.rewrite(new URL('/resources/citable/llms.txt', request.url));
|
|
111
|
+
}
|
|
112
|
+
return response;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const config = {
|
|
116
|
+
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
117
|
+
};\n`;
|
|
118
|
+
} else if (format === "shopify-snippet") {
|
|
119
|
+
content = `{% comment %}
|
|
120
|
+
Nebula Components CRO Remediation Snippet for Shopify
|
|
121
|
+
Add {% render 'nebula-cro-remediation' %} to theme.liquid before </body>
|
|
122
|
+
{% endcomment %}
|
|
123
|
+
<script>
|
|
124
|
+
(function() {
|
|
125
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
126
|
+
// CRO-007: Inject autocomplete attributes
|
|
127
|
+
document.querySelectorAll('input[name*="name"]').forEach(function(i) { if (!i.autocomplete) i.autocomplete = 'name'; });
|
|
128
|
+
document.querySelectorAll('input[type="email"], input[name*="email"]').forEach(function(i) { if (!i.autocomplete) i.autocomplete = 'email'; });
|
|
129
|
+
// CRO-015: Enforce 48px touch targets
|
|
130
|
+
document.querySelectorAll('button[type="submit"], .btn').forEach(function(b) {
|
|
131
|
+
if (b.offsetHeight < 44) b.style.minHeight = '48px';
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
})();
|
|
135
|
+
</script>\n`;
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error(`unsupported edge format "${format}". Supported: cloudflare-redirects, cloudflare-waf, cloudflare-worker, cloudflare-cro, vercel-middleware, shopify-snippet`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (output) {
|
|
141
|
+
fs.writeFileSync(path.resolve(root, output), content, "utf8");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
format,
|
|
146
|
+
output_path: output,
|
|
147
|
+
content,
|
|
148
|
+
rules_count: content.split("\n").filter((l) => l && !l.startsWith("#")).length,
|
|
149
|
+
security: auditEdgeCode(content, { format, name: `edge-export:${format}` }),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Tests edge routing and header preservation for Cloudflare-backed properties.
|
|
155
|
+
*/
|
|
156
|
+
export async function testEdgeRules(root, { provider = "cloudflare", baseUrl = "https://example.test", headers = {} } = {}) {
|
|
157
|
+
const checks = [];
|
|
158
|
+
|
|
159
|
+
// Check 1: Cloudflare Ray ID header expectation
|
|
160
|
+
const cfRayPresent = Boolean(headers["cf-ray"] || headers["CF-RAY"]);
|
|
161
|
+
checks.push({
|
|
162
|
+
check_id: "EDGE-CF-RAY",
|
|
163
|
+
name: "Cloudflare Edge Ray ID Header",
|
|
164
|
+
passed: provider === "cloudflare" ? cfRayPresent : true,
|
|
165
|
+
detail: cfRayPresent ? `Observed ${headers["cf-ray"] || headers["CF-RAY"]}` : "cf-ray header absent (expected behind Cloudflare edge)",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Check 2: Content Type Options security header
|
|
169
|
+
const nosniff = (headers["x-content-type-options"] || "").toLowerCase() === "nosniff";
|
|
170
|
+
checks.push({
|
|
171
|
+
check_id: "EDGE-SEC-NOSNIFF",
|
|
172
|
+
name: "X-Content-Type-Options Header",
|
|
173
|
+
passed: nosniff,
|
|
174
|
+
detail: nosniff ? "nosniff header present" : "x-content-type-options: nosniff missing from edge response",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Check 3: Bot Management / Challenge detection
|
|
178
|
+
const isChallenge = Boolean(headers["cf-mitigated"] === "challenge" || (headers["server"] || "").includes("cloudflare-challenge"));
|
|
179
|
+
checks.push({
|
|
180
|
+
check_id: "EDGE-BOT-CHALLENGE",
|
|
181
|
+
name: "Edge Bot Mitigation Status",
|
|
182
|
+
passed: !isChallenge,
|
|
183
|
+
detail: isChallenge ? "Edge presented CAPTCHA / Managed Challenge to probe" : "Clean pass-through without bot mitigation challenge",
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const allPassed = checks.every((c) => c.passed);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
provider,
|
|
190
|
+
base_url: baseUrl,
|
|
191
|
+
status: allPassed ? "healthy" : "remediation_needed",
|
|
192
|
+
total_checks: checks.length,
|
|
193
|
+
passed_checks: checks.filter((c) => c.passed).length,
|
|
194
|
+
checks,
|
|
195
|
+
};
|
|
196
|
+
}
|