Skip to main content

ic_asset_router/
config.rs

1use std::{collections::HashMap, time::Duration};
2
3use ic_http_certification::HeaderField;
4
5/// Global cache-control configuration for static and dynamic assets.
6///
7/// Static assets are files embedded via `include_dir` and served directly.
8/// Dynamic assets are generated by route handlers at request time.
9///
10/// These values are used as defaults; per-route overrides can be applied via
11/// handler response headers.
12pub struct CacheControl {
13    /// Cache-Control for static assets embedded via `include_dir`.
14    /// Default: `"public, max-age=31536000, immutable"`
15    pub static_assets: String,
16
17    /// Cache-Control for dynamically generated assets.
18    /// Default: `"public, no-cache, no-store"`
19    pub dynamic_assets: String,
20}
21
22impl Default for CacheControl {
23    fn default() -> Self {
24        Self {
25            static_assets: "public, max-age=31536000, immutable".into(),
26            dynamic_assets: "public, no-cache, no-store".into(),
27        }
28    }
29}
30
31/// TTL-based cache configuration for dynamic assets.
32///
33/// Controls how long dynamically generated assets remain valid before
34/// the library triggers re-generation via an update call.
35#[derive(Default)]
36pub struct CacheConfig {
37    /// Default TTL applied to all dynamic assets that don't have an explicit TTL.
38    /// `None` means dynamic assets are cached indefinitely (backwards-compatible).
39    pub default_ttl: Option<Duration>,
40    /// Per-route TTL overrides. Keys are exact path strings (e.g. `"/posts/1"`).
41    /// A per-route TTL takes precedence over `default_ttl`.
42    pub per_route_ttl: HashMap<String, Duration>,
43}
44
45impl CacheConfig {
46    /// Resolve the effective TTL for a given path.
47    ///
48    /// Priority: per-route TTL > default TTL > None (cache forever).
49    pub fn effective_ttl(&self, path: &str) -> Option<Duration> {
50        self.per_route_ttl.get(path).copied().or(self.default_ttl)
51    }
52}
53
54/// Typed fields for well-known security headers.
55///
56/// Based on the OWASP Secure Headers Project "active" list and Helmet.js defaults.
57/// Each field is `Option<String>` -- `None` means the header is not emitted.
58///
59/// For headers not covered by this struct, use [`AssetConfig::custom_headers`].
60///
61/// `X-XSS-Protection` is intentionally excluded: it is deprecated and its filter
62/// mechanism can be exploited. Modern guidance is to not set it at all.
63pub struct SecurityHeaders {
64    /// `Strict-Transport-Security` -- Forces HTTPS, prevents protocol downgrade (RFC 6797).
65    /// Example: `"max-age=31536000; includeSubDomains"`
66    pub hsts: Option<String>,
67
68    /// `Content-Security-Policy` -- Controls which resources the browser can load.
69    /// Primary defense against XSS. Complex to configure; `None` by default.
70    /// Example: `"default-src 'self'; script-src 'self'"`
71    pub csp: Option<String>,
72
73    /// `X-Content-Type-Options` -- Prevents MIME-type sniffing.
74    /// The only valid value is `"nosniff"`.
75    pub content_type_options: Option<String>,
76
77    /// `X-Frame-Options` -- Controls iframe embedding (legacy; superseded by CSP `frame-ancestors`).
78    /// Values: `"DENY"`, `"SAMEORIGIN"`.
79    pub frame_options: Option<String>,
80
81    /// `Referrer-Policy` -- Controls referrer information sent with requests.
82    /// Values: `"no-referrer"`, `"strict-origin-when-cross-origin"`, etc.
83    pub referrer_policy: Option<String>,
84
85    /// `Permissions-Policy` -- Controls which browser APIs are allowed (camera, geolocation, etc.).
86    /// Replaces deprecated `Feature-Policy`. Still a W3C working draft but widely supported.
87    /// Example: `"camera=(), microphone=(), geolocation=()"`
88    pub permissions_policy: Option<String>,
89
90    /// `Cross-Origin-Embedder-Policy` -- Controls loading of cross-origin resources.
91    /// Values: `"require-corp"`, `"credentialless"`, `"unsafe-none"`.
92    /// Warning: `"require-corp"` breaks Google Fonts, CDN images, analytics scripts.
93    pub coep: Option<String>,
94
95    /// `Cross-Origin-Opener-Policy` -- Process-isolates the document, prevents XS-Leaks.
96    /// Values: `"same-origin"`, `"same-origin-allow-popups"`, `"unsafe-none"`.
97    pub coop: Option<String>,
98
99    /// `Cross-Origin-Resource-Policy` -- Controls which origins can load a resource.
100    /// Mitigates Spectre side-channel attacks.
101    /// Values: `"same-origin"`, `"same-site"`, `"cross-origin"`.
102    pub corp: Option<String>,
103
104    /// `X-DNS-Prefetch-Control` -- Controls DNS prefetching, which can leak browsing intent.
105    /// Values: `"off"`, `"on"`.
106    pub dns_prefetch_control: Option<String>,
107
108    /// `X-Permitted-Cross-Domain-Policies` -- Controls cross-domain policy for Flash/PDF.
109    /// Values: `"none"`, `"master-only"`, `"by-content-type"`, `"all"`.
110    pub permitted_cross_domain_policies: Option<String>,
111}
112
113impl SecurityHeaders {
114    /// Strict preset — mirrors the library's original hardcoded behavior plus
115    /// `Cross-Origin-Resource-Policy`, `X-DNS-Prefetch-Control`, and
116    /// `X-Permitted-Cross-Domain-Policies`.
117    ///
118    /// Suitable for apps that do not load cross-origin resources.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use ic_asset_router::SecurityHeaders;
124    ///
125    /// let headers = SecurityHeaders::strict().to_headers();
126    /// let names: Vec<&str> = headers.iter().map(|(k, _)| k.as_str()).collect();
127    /// assert!(names.contains(&"strict-transport-security"));
128    /// assert!(names.contains(&"x-content-type-options"));
129    /// assert!(names.contains(&"x-frame-options"));
130    /// assert!(names.contains(&"cross-origin-embedder-policy"));
131    /// ```
132    pub fn strict() -> Self {
133        Self {
134            hsts: Some("max-age=31536000; includeSubDomains".into()),
135            csp: None,
136            content_type_options: Some("nosniff".into()),
137            frame_options: Some("DENY".into()),
138            referrer_policy: Some("no-referrer".into()),
139            permissions_policy: Some(
140                "accelerometer=(), camera=(), geolocation=(), gyroscope=(), \
141                 magnetometer=(), microphone=(), payment=(), usb=(), interest-cohort=()"
142                    .into(),
143            ),
144            coep: Some("require-corp".into()),
145            coop: Some("same-origin".into()),
146            corp: Some("same-origin".into()),
147            dns_prefetch_control: Some("off".into()),
148            permitted_cross_domain_policies: Some("none".into()),
149        }
150    }
151
152    /// Permissive preset — allows cross-origin resources, iframe embedding via
153    /// `SAMEORIGIN`, and relaxed referrer policy.
154    ///
155    /// Suitable for SPAs loading external fonts, images, scripts. This is the
156    /// default preset returned by [`SecurityHeaders::default()`].
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use ic_asset_router::SecurityHeaders;
162    ///
163    /// let headers = SecurityHeaders::permissive().to_headers();
164    /// let names: Vec<&str> = headers.iter().map(|(k, _)| k.as_str()).collect();
165    /// // Permissive does not set COEP (which would break external resources)
166    /// assert!(!names.contains(&"cross-origin-embedder-policy"));
167    /// // But still sets HSTS and X-Content-Type-Options
168    /// assert!(names.contains(&"strict-transport-security"));
169    /// assert!(names.contains(&"x-content-type-options"));
170    /// ```
171    pub fn permissive() -> Self {
172        Self {
173            hsts: Some("max-age=31536000; includeSubDomains".into()),
174            csp: None,
175            content_type_options: Some("nosniff".into()),
176            frame_options: Some("SAMEORIGIN".into()),
177            referrer_policy: Some("strict-origin-when-cross-origin".into()),
178            permissions_policy: None,
179            coep: None,
180            coop: Some("same-origin-allow-popups".into()),
181            corp: Some("cross-origin".into()),
182            dns_prefetch_control: None,
183            permitted_cross_domain_policies: Some("none".into()),
184        }
185    }
186
187    /// No security headers — the consumer takes full responsibility.
188    ///
189    /// # Examples
190    ///
191    /// ```
192    /// use ic_asset_router::SecurityHeaders;
193    ///
194    /// let headers = SecurityHeaders::none().to_headers();
195    /// assert!(headers.is_empty());
196    /// ```
197    pub fn none() -> Self {
198        Self {
199            hsts: None,
200            csp: None,
201            content_type_options: None,
202            frame_options: None,
203            referrer_policy: None,
204            permissions_policy: None,
205            coep: None,
206            coop: None,
207            corp: None,
208            dns_prefetch_control: None,
209            permitted_cross_domain_policies: None,
210        }
211    }
212
213    /// Convert non-`None` fields into a list of HTTP header tuples.
214    ///
215    /// Fields that are `None` are omitted entirely (the header is not emitted).
216    pub fn to_headers(&self) -> Vec<HeaderField> {
217        let mut headers = Vec::new();
218
219        if let Some(ref v) = self.hsts {
220            headers.push(("strict-transport-security".to_string(), v.clone()));
221        }
222        if let Some(ref v) = self.csp {
223            headers.push(("content-security-policy".to_string(), v.clone()));
224        }
225        if let Some(ref v) = self.content_type_options {
226            headers.push(("x-content-type-options".to_string(), v.clone()));
227        }
228        if let Some(ref v) = self.frame_options {
229            headers.push(("x-frame-options".to_string(), v.clone()));
230        }
231        if let Some(ref v) = self.referrer_policy {
232            headers.push(("referrer-policy".to_string(), v.clone()));
233        }
234        if let Some(ref v) = self.permissions_policy {
235            headers.push(("permissions-policy".to_string(), v.clone()));
236        }
237        if let Some(ref v) = self.coep {
238            headers.push(("cross-origin-embedder-policy".to_string(), v.clone()));
239        }
240        if let Some(ref v) = self.coop {
241            headers.push(("cross-origin-opener-policy".to_string(), v.clone()));
242        }
243        if let Some(ref v) = self.corp {
244            headers.push(("cross-origin-resource-policy".to_string(), v.clone()));
245        }
246        if let Some(ref v) = self.dns_prefetch_control {
247            headers.push(("x-dns-prefetch-control".to_string(), v.clone()));
248        }
249        if let Some(ref v) = self.permitted_cross_domain_policies {
250            headers.push(("x-permitted-cross-domain-policies".to_string(), v.clone()));
251        }
252
253        headers
254    }
255}
256
257impl Default for SecurityHeaders {
258    /// Returns [`SecurityHeaders::permissive()`] to avoid breaking common use cases
259    /// (external fonts, CDN images, analytics).
260    fn default() -> Self {
261        Self::permissive()
262    }
263}
264
265/// Global configuration for the asset router.
266///
267/// Controls security headers, cache-control, custom headers, and TTL-based
268/// caching applied to all responses.
269///
270/// # Examples
271///
272/// ```
273/// use ic_asset_router::{AssetConfig, SecurityHeaders, CacheControl};
274///
275/// let config = AssetConfig {
276///     security_headers: SecurityHeaders::strict(),
277///     cache_control: CacheControl {
278///         static_assets: "public, max-age=3600".into(),
279///         ..CacheControl::default()
280///     },
281///     ..AssetConfig::default()
282/// };
283/// ```
284#[derive(Default)]
285pub struct AssetConfig {
286    /// Typed security headers. Use a preset ([`SecurityHeaders::strict()`],
287    /// [`SecurityHeaders::permissive()`], [`SecurityHeaders::none()`]) or
288    /// configure individual fields.
289    pub security_headers: SecurityHeaders,
290
291    /// Cache-Control configuration for static and dynamic assets.
292    pub cache_control: CacheControl,
293
294    /// TTL-based cache configuration for dynamic assets.
295    pub cache_config: CacheConfig,
296
297    /// Arbitrary headers appended after security headers.
298    ///
299    /// If a custom header has the same name as a security header, the custom
300    /// header wins (last-write-wins semantics during merging).
301    pub custom_headers: Vec<HeaderField>,
302}
303
304impl AssetConfig {
305    /// Merge security headers and custom headers into a single list, then layer
306    /// `additional_headers` on top.
307    ///
308    /// Merge order (last-write-wins for duplicate header names):
309    /// 1. Security headers (from [`SecurityHeaders`])
310    /// 2. Custom headers (from [`AssetConfig::custom_headers`])
311    /// 3. `additional_headers` (per-route / per-call overrides)
312    ///
313    /// The comparison is case-insensitive on header names.
314    pub fn merged_headers(&self, additional_headers: Vec<HeaderField>) -> Vec<HeaderField> {
315        let mut merged: Vec<HeaderField> = Vec::new();
316
317        // Start with security headers
318        for h in self.security_headers.to_headers() {
319            merged.push(h);
320        }
321
322        // Layer custom_headers -- override any security header with the same name
323        for h in &self.custom_headers {
324            let name_lower = h.0.to_lowercase();
325            merged.retain(|(k, _)| k.to_lowercase() != name_lower);
326            merged.push(h.clone());
327        }
328
329        // Layer additional_headers (per-route overrides)
330        for h in &additional_headers {
331            let name_lower = h.0.to_lowercase();
332            merged.retain(|(k, _)| k.to_lowercase() != name_lower);
333            merged.push(h.clone());
334        }
335
336        merged
337    }
338}
339
340// Test coverage audit (Session 7, Spec 5.5):
341//
342// Covered:
343//   - SecurityHeaders presets: strict (all expected headers), permissive (subset), none (zero)
344//   - Custom header override: custom_headers replaces security header of same name
345//   - Additional headers override both custom and security headers
346//   - Case-insensitive header name matching during merges
347//   - X-XSS-Protection never set by any preset
348//   - Default preset is permissive
349//   - CacheControl defaults and custom values (static and dynamic)
350//   - CacheConfig default (None TTL, empty per-route), per-route TTL overrides default
351//
352// No significant gaps identified for config.rs.
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    // ---- 1.1.15: strict() produces the expected set of headers ----
358
359    #[test]
360    fn strict_produces_expected_headers() {
361        let headers = SecurityHeaders::strict().to_headers();
362        let names: Vec<&str> = headers.iter().map(|(k, _)| k.as_str()).collect();
363
364        assert!(names.contains(&"strict-transport-security"));
365        assert!(names.contains(&"x-content-type-options"));
366        assert!(names.contains(&"x-frame-options"));
367        assert!(names.contains(&"referrer-policy"));
368        assert!(names.contains(&"permissions-policy"));
369        assert!(names.contains(&"cross-origin-embedder-policy"));
370        assert!(names.contains(&"cross-origin-opener-policy"));
371        assert!(names.contains(&"cross-origin-resource-policy"));
372        assert!(names.contains(&"x-dns-prefetch-control"));
373        assert!(names.contains(&"x-permitted-cross-domain-policies"));
374
375        // CSP is None by default even in strict
376        assert!(!names.contains(&"content-security-policy"));
377
378        // Verify specific values
379        let find =
380            |name: &str| -> String { headers.iter().find(|(k, _)| k == name).unwrap().1.clone() };
381
382        assert_eq!(find("x-frame-options"), "DENY");
383        assert_eq!(find("referrer-policy"), "no-referrer");
384        assert_eq!(find("cross-origin-embedder-policy"), "require-corp");
385        assert_eq!(find("cross-origin-opener-policy"), "same-origin");
386        assert_eq!(find("cross-origin-resource-policy"), "same-origin");
387        assert_eq!(find("x-dns-prefetch-control"), "off");
388        assert_eq!(find("x-permitted-cross-domain-policies"), "none");
389    }
390
391    // ---- 1.1.16: permissive() produces the expected set of headers ----
392
393    #[test]
394    fn permissive_produces_expected_headers() {
395        let headers = SecurityHeaders::permissive().to_headers();
396        let names: Vec<&str> = headers.iter().map(|(k, _)| k.as_str()).collect();
397
398        assert!(names.contains(&"strict-transport-security"));
399        assert!(names.contains(&"x-content-type-options"));
400        assert!(names.contains(&"x-frame-options"));
401        assert!(names.contains(&"referrer-policy"));
402        assert!(names.contains(&"cross-origin-opener-policy"));
403        assert!(names.contains(&"cross-origin-resource-policy"));
404        assert!(names.contains(&"x-permitted-cross-domain-policies"));
405
406        // These should NOT be set in permissive
407        assert!(!names.contains(&"permissions-policy"));
408        assert!(!names.contains(&"cross-origin-embedder-policy"));
409        assert!(!names.contains(&"content-security-policy"));
410        assert!(!names.contains(&"x-dns-prefetch-control"));
411
412        let find =
413            |name: &str| -> String { headers.iter().find(|(k, _)| k == name).unwrap().1.clone() };
414
415        assert_eq!(find("x-frame-options"), "SAMEORIGIN");
416        assert_eq!(find("referrer-policy"), "strict-origin-when-cross-origin");
417        assert_eq!(
418            find("cross-origin-opener-policy"),
419            "same-origin-allow-popups"
420        );
421        assert_eq!(find("cross-origin-resource-policy"), "cross-origin");
422    }
423
424    // ---- 1.1.17: none() produces zero headers ----
425
426    #[test]
427    fn none_produces_zero_headers() {
428        let headers = SecurityHeaders::none().to_headers();
429        assert!(headers.is_empty());
430    }
431
432    // ---- 1.1.18: custom headers override security headers of same name ----
433
434    #[test]
435    fn custom_headers_override_security_headers() {
436        let config = AssetConfig {
437            security_headers: SecurityHeaders::strict(),
438            cache_control: CacheControl::default(),
439            cache_config: CacheConfig::default(),
440            custom_headers: vec![("x-frame-options".to_string(), "SAMEORIGIN".to_string())],
441        };
442        let merged = config.merged_headers(vec![]);
443        let frame_opts: Vec<_> = merged
444            .iter()
445            .filter(|(k, _)| k == "x-frame-options")
446            .collect();
447        assert_eq!(frame_opts.len(), 1);
448        assert_eq!(frame_opts[0].1, "SAMEORIGIN");
449    }
450
451    #[test]
452    fn additional_headers_override_custom_and_security() {
453        let config = AssetConfig {
454            security_headers: SecurityHeaders::strict(),
455            cache_control: CacheControl::default(),
456            cache_config: CacheConfig::default(),
457            custom_headers: vec![("x-frame-options".to_string(), "SAMEORIGIN".to_string())],
458        };
459        let merged = config.merged_headers(vec![(
460            "X-Frame-Options".to_string(),
461            "ALLOW-FROM https://example.com".to_string(),
462        )]);
463        let frame_opts: Vec<_> = merged
464            .iter()
465            .filter(|(k, _)| k.to_lowercase() == "x-frame-options")
466            .collect();
467        assert_eq!(frame_opts.len(), 1);
468        assert_eq!(frame_opts[0].1, "ALLOW-FROM https://example.com");
469    }
470
471    // ---- 1.1.19: X-XSS-Protection is never set by any preset ----
472
473    #[test]
474    fn xss_protection_never_set() {
475        for headers in [
476            SecurityHeaders::strict().to_headers(),
477            SecurityHeaders::permissive().to_headers(),
478            SecurityHeaders::none().to_headers(),
479        ] {
480            assert!(
481                headers
482                    .iter()
483                    .all(|(k, _)| k.to_lowercase() != "x-xss-protection"),
484                "X-XSS-Protection should never be set by any preset"
485            );
486        }
487    }
488
489    #[test]
490    fn default_is_permissive() {
491        let default_headers = SecurityHeaders::default().to_headers();
492        let permissive_headers = SecurityHeaders::permissive().to_headers();
493        assert_eq!(default_headers, permissive_headers);
494    }
495
496    // ---- 1.7.7: default CacheControl reproduces current behavior ----
497
498    #[test]
499    fn default_cache_control_reproduces_current_behavior() {
500        let cc = CacheControl::default();
501        assert_eq!(cc.static_assets, "public, max-age=31536000, immutable");
502        assert_eq!(cc.dynamic_assets, "public, no-cache, no-store");
503    }
504
505    // ---- 1.7.8: custom static cache-control value is used ----
506
507    #[test]
508    fn custom_static_cache_control() {
509        let cc = CacheControl {
510            static_assets: "public, max-age=3600".into(),
511            ..CacheControl::default()
512        };
513        assert_eq!(cc.static_assets, "public, max-age=3600");
514        // dynamic_assets unchanged
515        assert_eq!(cc.dynamic_assets, "public, no-cache, no-store");
516    }
517
518    // ---- 1.7.9: custom dynamic cache-control value is used ----
519
520    #[test]
521    fn custom_dynamic_cache_control() {
522        let cc = CacheControl {
523            dynamic_assets: "public, max-age=600".into(),
524            ..CacheControl::default()
525        };
526        assert_eq!(cc.dynamic_assets, "public, max-age=600");
527        // static_assets unchanged
528        assert_eq!(cc.static_assets, "public, max-age=31536000, immutable");
529    }
530
531    #[test]
532    fn asset_config_default_includes_default_cache_control() {
533        let config = AssetConfig::default();
534        assert_eq!(
535            config.cache_control.static_assets,
536            "public, max-age=31536000, immutable"
537        );
538        assert_eq!(
539            config.cache_control.dynamic_assets,
540            "public, no-cache, no-store"
541        );
542    }
543
544    // ---- 4.1.16: CacheConfig::default() has default_ttl: None and empty per_route_ttl ----
545
546    #[test]
547    fn cache_config_default_has_none_and_empty() {
548        let cc = CacheConfig::default();
549        assert!(cc.default_ttl.is_none());
550        assert!(cc.per_route_ttl.is_empty());
551    }
552
553    // ---- 4.1.20: per-route TTL overrides default TTL ----
554
555    #[test]
556    fn per_route_ttl_overrides_default_ttl() {
557        let cc = CacheConfig {
558            default_ttl: Some(Duration::from_secs(3600)),
559            per_route_ttl: HashMap::from([("/posts/1".to_string(), Duration::from_secs(60))]),
560        };
561        // Per-route TTL should win
562        assert_eq!(cc.effective_ttl("/posts/1"), Some(Duration::from_secs(60)));
563        // Path without per-route override falls back to default
564        assert_eq!(cc.effective_ttl("/about"), Some(Duration::from_secs(3600)));
565        // With no default and no per-route, returns None
566        let cc_no_default = CacheConfig {
567            default_ttl: None,
568            per_route_ttl: HashMap::from([("/posts/1".to_string(), Duration::from_secs(60))]),
569        };
570        assert_eq!(
571            cc_no_default.effective_ttl("/posts/1"),
572            Some(Duration::from_secs(60))
573        );
574        assert_eq!(cc_no_default.effective_ttl("/about"), None);
575    }
576
577    // ---- 8.6.4: Config header dedup tests ----
578
579    /// Later header with the same key overrides the earlier one.
580    #[test]
581    fn merged_headers_later_overrides_earlier() {
582        let config = AssetConfig {
583            security_headers: SecurityHeaders::none(),
584            cache_control: CacheControl::default(),
585            cache_config: CacheConfig::default(),
586            custom_headers: vec![
587                ("x-custom".to_string(), "first".to_string()),
588                ("x-custom".to_string(), "second".to_string()),
589            ],
590        };
591        let merged = config.merged_headers(vec![]);
592        let custom: Vec<_> = merged.iter().filter(|(k, _)| k == "x-custom").collect();
593        assert_eq!(custom.len(), 1, "only one x-custom header should remain");
594        assert_eq!(custom[0].1, "second", "later value should win");
595    }
596
597    /// Override is case-insensitive: "Content-Type" overrides "content-type".
598    #[test]
599    fn merged_headers_case_insensitive_override() {
600        let config = AssetConfig {
601            security_headers: SecurityHeaders::none(),
602            cache_control: CacheControl::default(),
603            cache_config: CacheConfig::default(),
604            custom_headers: vec![("content-type".to_string(), "text/plain".to_string())],
605        };
606        // Additional header with different casing overrides custom.
607        let merged = config.merged_headers(vec![(
608            "Content-Type".to_string(),
609            "application/json".to_string(),
610        )]);
611        let ct: Vec<_> = merged
612            .iter()
613            .filter(|(k, _)| k.to_lowercase() == "content-type")
614            .collect();
615        assert_eq!(ct.len(), 1, "only one content-type header should remain");
616        assert_eq!(ct[0].1, "application/json", "additional header should win");
617        // The surviving header preserves the casing from the additional layer.
618        assert_eq!(ct[0].0, "Content-Type");
619    }
620}