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
diff --git a/config/config_test.go b/config/config_test.go
index a7d121f..d8c7789 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1191,31 +1191,31 @@
 
 	tests := []struct {
 		name          string
-		listRewrites *bool
+		listRewrites  *bool
 		globalDefault bool
 		expected      bool
 	}{
 		{
 			name:          "nil per-list, global false",
-			listRewrites: nil,
+			listRewrites:  nil,
 			globalDefault: false,
 			expected:      false,
 		},
 		{
 			name:          "nil per-list, global true",
-			listRewrites: nil,
+			listRewrites:  nil,
 			globalDefault: true,
 			expected:      true,
 		},
 		{
 			name:          "per-list true, global false",
-			listRewrites: &trueVal,
+			listRewrites:  &trueVal,
 			globalDefault: false,
 			expected:      true,
 		},
 		{
 			name:          "per-list false, global true",
-			listRewrites: &falseVal,
+			listRewrites:  &falseVal,
 			globalDefault: true,
 			expected:      false,
 		},
@@ -1425,7 +1425,7 @@
 	cfg := &MappingConfig{}
 	ApplyDefaults(cfg)
 	// AllowOrigins should derive from the Server default (trailing slash stripped)
-	assert.Equal(t, "https://korap.ids-mannheim.de", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://korap.ids-mannheim.de"}, cfg.AllowOrigins,
 		"default AllowOrigins should derive from defaultServer")
 }
 
@@ -1434,23 +1434,25 @@
 		Server: "https://custom.example.com/",
 	}
 	ApplyDefaults(cfg)
-	assert.Equal(t, "https://custom.example.com", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://custom.example.com"}, cfg.AllowOrigins,
 		"AllowOrigins should derive from the configured Server (trailing slash stripped)")
 }
 
 func TestAllowOriginsExplicitNotOverriddenByServer(t *testing.T) {
 	cfg := &MappingConfig{
 		Server:       "https://custom.example.com/",
-		AllowOrigins: "https://explicit-origin.example.com",
+		AllowOrigins: []string{"https://explicit-origin.example.com"},
 	}
 	ApplyDefaults(cfg)
-	assert.Equal(t, "https://explicit-origin.example.com", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://explicit-origin.example.com"}, cfg.AllowOrigins,
 		"explicit AllowOrigins should not be overridden by Server default")
 }
 
 func TestAllowOriginsFromYAML(t *testing.T) {
 	content := `
-allowOrigins: "https://custom.example.com,https://other.example.com"
+allowOrigins:
+  - "https://custom.example.com"
+  - "https://other.example.com"
 lists:
   - id: test-mapper
     mappings:
@@ -1466,15 +1468,37 @@
 
 	cfg, err := LoadFromSources(tmpfile.Name(), nil)
 	require.NoError(t, err)
-	assert.Equal(t, "https://custom.example.com,https://other.example.com",
+	assert.Equal(t, []string{"https://custom.example.com", "https://other.example.com"},
 		cfg.AllowOrigins)
 }
 
+func TestAllowOriginsStringFormatRejected(t *testing.T) {
+	content := `
+allowOrigins: "https://custom.example.com,https://other.example.com"
+lists:
+  - id: test-mapper
+    mappings:
+      - "[A] <> [B]"
+`
+	tmpfile, err := os.CreateTemp("", "config-cors-reject-*.yaml")
+	require.NoError(t, err)
+	defer os.Remove(tmpfile.Name())
+
+	_, err = tmpfile.WriteString(content)
+	require.NoError(t, err)
+	require.NoError(t, tmpfile.Close())
+
+	_, err = LoadFromSources(tmpfile.Name(), nil)
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "allowOrigins must be a YAML list")
+}
+
 func TestAllowOriginsEnvOverride(t *testing.T) {
 	t.Setenv("KORAL_MAPPER_ALLOW_ORIGINS", "https://env-origin.example.com")
 
 	content := `
-allowOrigins: "https://yaml-origin.example.com"
+allowOrigins:
+  - "https://yaml-origin.example.com"
 lists:
   - id: test-mapper
     mappings:
@@ -1490,7 +1514,7 @@
 
 	cfg, err := LoadFromSources(tmpfile.Name(), nil)
 	require.NoError(t, err)
-	assert.Equal(t, "https://env-origin.example.com", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://env-origin.example.com"}, cfg.AllowOrigins,
 		"KORAL_MAPPER_ALLOW_ORIGINS env var should override YAML value")
 }
 
@@ -1499,16 +1523,16 @@
 		Server: "https://korap.ids-mannheim.de/instance/test",
 	}
 	ApplyDefaults(cfg)
-	assert.Equal(t, "https://korap.ids-mannheim.de", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://korap.ids-mannheim.de"}, cfg.AllowOrigins,
 		"AllowOrigins should be pruned to host-level origin when Server contains a path")
 }
 
 func TestAllowOriginsExplicitWithPathsPruned(t *testing.T) {
 	cfg := &MappingConfig{
-		AllowOrigins: "https://korap.ids-mannheim.de/instance/test,https://other.example.com/app",
+		AllowOrigins: []string{"https://korap.ids-mannheim.de/instance/test", "https://other.example.com/app"},
 	}
 	ApplyDefaults(cfg)
-	assert.Equal(t, "https://korap.ids-mannheim.de,https://other.example.com", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://korap.ids-mannheim.de", "https://other.example.com"}, cfg.AllowOrigins,
 		"explicit AllowOrigins entries should be pruned to host-level origins")
 }
 
@@ -1517,7 +1541,7 @@
 		Server: "https://korap.ids-mannheim.de:8080/instance/test",
 	}
 	ApplyDefaults(cfg)
-	assert.Equal(t, "https://korap.ids-mannheim.de:8080", cfg.AllowOrigins,
+	assert.Equal(t, []string{"https://korap.ids-mannheim.de:8080"}, cfg.AllowOrigins,
 		"AllowOrigins should preserve port but strip path")
 }