Update to fiber 3

Change-Id: I18a7b3ed55aef5cae67db6a613ec14e0f8a9281e
diff --git a/config/config.go b/config/config.go
index 9347994..e024c76 100644
--- a/config/config.go
+++ b/config/config.go
@@ -109,7 +109,7 @@
 	ServiceURL   string        `yaml:"serviceURL,omitempty"`
 	CookieName   string        `yaml:"cookieName,omitempty"`
 	BasePath     string        `yaml:"basePath,omitempty"`     // restricts config file loading to this directory tree
-	AllowOrigins string        `yaml:"allowOrigins,omitempty"` // comma-separated list of allowed CORS origins
+	AllowOrigins []string      `yaml:"allowOrigins,omitempty"`
 	Port         int           `yaml:"port,omitempty"`
 	LogLevel     string        `yaml:"loglevel,omitempty"`
 	RateLimit    int           `yaml:"rateLimit,omitempty"` // max requests per minute per IP (0 = use default 100)
@@ -117,6 +117,28 @@
 	Lists        []MappingList `yaml:"lists,omitempty"`
 }
 
+// UnmarshalYAML rejects the deprecated comma-separated string format for
+// allowOrigins and requires a YAML list instead.
+func (m *MappingConfig) UnmarshalYAML(value *yaml.Node) error {
+	if value.Kind == yaml.MappingNode {
+		for i := 0; i < len(value.Content)-1; i += 2 {
+			if value.Content[i].Value == "allowOrigins" && value.Content[i+1].Kind == yaml.ScalarNode {
+				return fmt.Errorf(
+					"allowOrigins must be a YAML list, not a comma-separated string; update your config:\n" +
+						"  allowOrigins:\n" +
+						"    - \"https://example.com\"")
+			}
+		}
+	}
+	type plain MappingConfig
+	var p plain
+	if err := value.Decode(&p); err != nil {
+		return err
+	}
+	*m = MappingConfig(p)
+	return nil
+}
+
 // AllowedBasePath restricts file loading to a specific directory tree.
 // When set, all file paths must resolve to a location at or below this
 // directory (or under the system temp directory). Defaults to the CWD at
@@ -206,6 +228,8 @@
 				seenIDs[list.ID] = true
 			}
 			allLists = append(allLists, globalConfig.Lists...)
+		} else if strings.Contains(err.Error(), "allowOrigins must be") {
+			return nil, fmt.Errorf("failed to parse config file '%s': %w", configFile, err)
 		} else {
 			// Fall back to old format (direct list)
 			var lists []MappingList
@@ -309,8 +333,8 @@
 
 	// AllowOrigins defaults to the Server value. This avoids duplicating
 	// the server URL string and keeps CORS in sync with the deployment.
-	if config.AllowOrigins == "" {
-		config.AllowOrigins = config.Server
+	if len(config.AllowOrigins) == 0 {
+		config.AllowOrigins = []string{config.Server}
 	}
 	config.AllowOrigins = normalizeOrigins(config.AllowOrigins)
 
@@ -322,21 +346,24 @@
 	}
 }
 
-// normalizeOrigins takes a comma-separated list of origin URLs and strips
-// any path components, returning only scheme + host (+ port when present).
-// The CORS middleware requires bare origins without paths; URLs like
+// normalizeOrigins strips path components from origin URLs, returning only
+// scheme + host (+ port when present). The CORS middleware requires bare
+// origins without paths; URLs like
 // "https://example.com/instance/test" are pruned to "https://example.com".
-func normalizeOrigins(raw string) string {
-	parts := strings.Split(raw, ",")
-	for i, part := range parts {
-		part = strings.TrimSpace(part)
-		if u, err := url.Parse(part); err == nil && u.Host != "" {
-			parts[i] = u.Scheme + "://" + u.Host
+func normalizeOrigins(origins []string) []string {
+	result := make([]string, 0, len(origins))
+	for _, origin := range origins {
+		origin = strings.TrimSpace(origin)
+		if origin == "" {
+			continue
+		}
+		if u, err := url.Parse(origin); err == nil && u.Host != "" {
+			result = append(result, u.Scheme+"://"+u.Host)
 		} else {
-			parts[i] = strings.TrimRight(part, "/")
+			result = append(result, strings.TrimRight(origin, "/"))
 		}
 	}
-	return strings.Join(parts, ",")
+	return result
 }
 
 // ApplyEnvOverrides overrides configuration fields from environment variables.
@@ -344,14 +371,13 @@
 // Non-empty environment values override any previously loaded config values.
 func ApplyEnvOverrides(config *MappingConfig) {
 	envMappings := map[string]*string{
-		"KORAL_MAPPER_SERVER":        &config.Server,
-		"KORAL_MAPPER_SDK":           &config.SDK,
-		"KORAL_MAPPER_STYLESHEET":    &config.Stylesheet,
-		"KORAL_MAPPER_SERVICE_URL":   &config.ServiceURL,
-		"KORAL_MAPPER_COOKIE_NAME":   &config.CookieName,
-		"KORAL_MAPPER_LOG_LEVEL":     &config.LogLevel,
-		"KORAL_MAPPER_BASE_PATH":     &config.BasePath,
-		"KORAL_MAPPER_ALLOW_ORIGINS": &config.AllowOrigins,
+		"KORAL_MAPPER_SERVER":      &config.Server,
+		"KORAL_MAPPER_SDK":         &config.SDK,
+		"KORAL_MAPPER_STYLESHEET":  &config.Stylesheet,
+		"KORAL_MAPPER_SERVICE_URL": &config.ServiceURL,
+		"KORAL_MAPPER_COOKIE_NAME": &config.CookieName,
+		"KORAL_MAPPER_LOG_LEVEL":   &config.LogLevel,
+		"KORAL_MAPPER_BASE_PATH":   &config.BasePath,
 	}
 
 	for envKey, field := range envMappings {
@@ -360,6 +386,10 @@
 		}
 	}
 
+	if val := os.Getenv("KORAL_MAPPER_ALLOW_ORIGINS"); val != "" {
+		config.AllowOrigins = strings.Split(val, ",")
+	}
+
 	if val := os.Getenv("KORAL_MAPPER_PORT"); val != "" {
 		if port, err := strconv.Atoi(val); err == nil {
 			config.Port = port