@freshjuice/zest 1.0.0 → 2.0.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.
Files changed (61) hide show
  1. package/README.md +178 -78
  2. package/dist/zest.de.js +692 -305
  3. package/dist/zest.de.js.map +1 -1
  4. package/dist/zest.de.min.js +1 -1
  5. package/dist/zest.en.js +692 -305
  6. package/dist/zest.en.js.map +1 -1
  7. package/dist/zest.en.min.js +1 -1
  8. package/dist/zest.es.js +692 -305
  9. package/dist/zest.es.js.map +1 -1
  10. package/dist/zest.es.min.js +1 -1
  11. package/dist/zest.esm.js +692 -305
  12. package/dist/zest.esm.js.map +1 -1
  13. package/dist/zest.esm.min.js +1 -1
  14. package/dist/zest.fr.js +692 -305
  15. package/dist/zest.fr.js.map +1 -1
  16. package/dist/zest.fr.min.js +1 -1
  17. package/dist/zest.headless.esm.js +2299 -0
  18. package/dist/zest.headless.esm.js.map +1 -0
  19. package/dist/zest.headless.esm.min.js +1 -0
  20. package/dist/zest.it.js +692 -305
  21. package/dist/zest.it.js.map +1 -1
  22. package/dist/zest.it.min.js +1 -1
  23. package/dist/zest.ja.js +692 -305
  24. package/dist/zest.ja.js.map +1 -1
  25. package/dist/zest.ja.min.js +1 -1
  26. package/dist/zest.js +692 -305
  27. package/dist/zest.js.map +1 -1
  28. package/dist/zest.min.js +1 -1
  29. package/dist/zest.nl.js +692 -305
  30. package/dist/zest.nl.js.map +1 -1
  31. package/dist/zest.nl.min.js +1 -1
  32. package/dist/zest.pl.js +692 -305
  33. package/dist/zest.pl.js.map +1 -1
  34. package/dist/zest.pl.min.js +1 -1
  35. package/dist/zest.pt.js +692 -305
  36. package/dist/zest.pt.js.map +1 -1
  37. package/dist/zest.pt.min.js +1 -1
  38. package/dist/zest.ru.js +692 -305
  39. package/dist/zest.ru.js.map +1 -1
  40. package/dist/zest.ru.min.js +1 -1
  41. package/dist/zest.uk.js +692 -305
  42. package/dist/zest.uk.js.map +1 -1
  43. package/dist/zest.uk.min.js +1 -1
  44. package/dist/zest.zh.js +692 -305
  45. package/dist/zest.zh.js.map +1 -1
  46. package/dist/zest.zh.min.js +1 -1
  47. package/package.json +16 -4
  48. package/src/core/cookie-interceptor.js +20 -5
  49. package/src/core/known-trackers.js +41 -14
  50. package/src/core/pattern-matcher.js +20 -5
  51. package/src/core/script-blocker.js +85 -79
  52. package/src/core/security.js +204 -0
  53. package/src/core/storage-interceptor.js +5 -1
  54. package/src/core-lifecycle.js +192 -0
  55. package/src/headless.js +133 -0
  56. package/src/index.js +73 -184
  57. package/src/storage/consent-store.js +32 -8
  58. package/src/ui/banner.js +11 -7
  59. package/src/ui/modal.js +16 -12
  60. package/src/ui/styles.js +25 -4
  61. package/src/ui/widget.js +3 -1
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Security utilities - escaping, validation, and safe parsing helpers
3
+ *
4
+ * These helpers are used across UI components, URL/CSS validation, and
5
+ * consent-cookie parsing to provide defense-in-depth against untrusted
6
+ * config (CMS-driven, i18n-loaded, or attacker-supplied).
7
+ */
8
+
9
+ const HTML_ESCAPE_MAP = {
10
+ '&': '&',
11
+ '<': '&lt;',
12
+ '>': '&gt;',
13
+ '"': '&quot;',
14
+ "'": '&#39;',
15
+ '`': '&#96;'
16
+ };
17
+
18
+ /**
19
+ * Escape a value for safe embedding in HTML text nodes and attribute values.
20
+ * Accepts any value — non-strings are stringified first. null/undefined -> ''.
21
+ */
22
+ export function escapeHTML(value) {
23
+ if (value === null || value === undefined) return '';
24
+ const str = typeof value === 'string' ? value : String(value);
25
+ return str.replace(/[&<>"'`]/g, (ch) => HTML_ESCAPE_MAP[ch]);
26
+ }
27
+
28
+ /**
29
+ * Validate a URL — return the URL if it uses http:/https:/mailto:/tel:,
30
+ * otherwise return null. Blocks javascript:, data:, vbscript:, file:, etc.
31
+ */
32
+ export function safeUrl(url) {
33
+ if (typeof url !== 'string' || url.length === 0) return null;
34
+ const trimmed = url.trim();
35
+ if (trimmed.length === 0) return null;
36
+
37
+ // Relative URLs (no protocol) are safe — treat as same-origin path
38
+ if (/^[/?#]/.test(trimmed)) return trimmed;
39
+
40
+ // Check protocol explicitly, do NOT rely on URL parsing alone —
41
+ // attackers may use whitespace/control characters to confuse parsers.
42
+ const match = trimmed.match(/^([a-z][a-z0-9+.-]*):/i);
43
+ if (!match) {
44
+ // No protocol — treat as relative
45
+ return trimmed;
46
+ }
47
+
48
+ const protocol = match[1].toLowerCase();
49
+ if (protocol === 'http' || protocol === 'https' || protocol === 'mailto' || protocol === 'tel') {
50
+ return trimmed;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * Validate a CSS color value. Accepts #rgb/#rrggbb/#rrggbbaa and a
57
+ * small allowlist of CSS named colors and rgb()/rgba()/hsl()/hsla()
58
+ * functional forms with numeric-only arguments.
59
+ */
60
+ const NAMED_COLORS = new Set([
61
+ 'transparent', 'black', 'white', 'red', 'green', 'blue', 'yellow',
62
+ 'orange', 'purple', 'pink', 'gray', 'grey', 'brown', 'cyan', 'magenta',
63
+ 'silver', 'gold', 'navy', 'teal', 'maroon', 'olive', 'lime', 'aqua',
64
+ 'fuchsia', 'indigo', 'violet', 'crimson', 'coral', 'salmon', 'tomato'
65
+ ]);
66
+
67
+ export function safeColor(color) {
68
+ if (typeof color !== 'string') return null;
69
+ const trimmed = color.trim();
70
+
71
+ if (/^#[0-9a-fA-F]{3,8}$/.test(trimmed)) return trimmed;
72
+ if (NAMED_COLORS.has(trimmed.toLowerCase())) return trimmed;
73
+
74
+ // Functional notations: only digits, dots, commas, %, whitespace between parens
75
+ if (/^(rgb|rgba|hsl|hsla)\(\s*[\d.,%\s/]+\s*\)$/i.test(trimmed)) return trimmed;
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Validate a regex pattern string. Rejects patterns that contain known
82
+ * catastrophic-backtracking shapes (nested quantifiers). Compiles with
83
+ * try/catch.
84
+ *
85
+ * Returns a RegExp on success, null on failure.
86
+ */
87
+ const REDOS_PATTERNS = [
88
+ /(\([^)]*[+*][^)]*\)|\[[^\]]*\]|\\w|\\d|\\s)\s*[+*]/, // nested quantifier
89
+ /\(\?[=!][^)]*[+*][^)]*\)[+*]/, // lookahead with quantifier, then quantifier
90
+ ];
91
+
92
+ export function safeRegExp(pattern, flags) {
93
+ if (pattern instanceof RegExp) return pattern;
94
+ if (typeof pattern !== 'string') return null;
95
+
96
+ // Cap pattern length to limit compiled-regex state
97
+ if (pattern.length > 500) return null;
98
+
99
+ // Heuristic: reject obviously dangerous patterns
100
+ for (const bad of REDOS_PATTERNS) {
101
+ if (bad.test(pattern)) return null;
102
+ }
103
+
104
+ try {
105
+ return new RegExp(pattern, flags);
106
+ } catch (e) {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Sanitize a consent-cookie payload. Only known category keys with
113
+ * boolean values survive; prototype-polluting keys are stripped.
114
+ */
115
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
116
+
117
+ export function sanitizeConsentPayload(raw, knownCategoryIds) {
118
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
119
+
120
+ const result = {
121
+ version: typeof raw.version === 'string' ? raw.version : null,
122
+ timestamp: typeof raw.timestamp === 'number' && Number.isFinite(raw.timestamp) ? raw.timestamp : null,
123
+ categories: {}
124
+ };
125
+
126
+ const cats = raw.categories;
127
+ if (!cats || typeof cats !== 'object' || Array.isArray(cats)) return null;
128
+
129
+ for (const key of knownCategoryIds) {
130
+ if (FORBIDDEN_KEYS.has(key)) continue;
131
+ if (Object.prototype.hasOwnProperty.call(cats, key)) {
132
+ result.categories[key] = cats[key] === true;
133
+ }
134
+ }
135
+
136
+ // essential is always true regardless of stored value
137
+ if (knownCategoryIds.includes('essential')) {
138
+ result.categories.essential = true;
139
+ }
140
+
141
+ return result;
142
+ }
143
+
144
+ /**
145
+ * Invoke a user-supplied callback, swallowing and logging exceptions so
146
+ * a misbehaving callback can't break the consent flow.
147
+ */
148
+ export function safeInvoke(fn, ...args) {
149
+ if (typeof fn !== 'function') return undefined;
150
+ try {
151
+ return fn(...args);
152
+ } catch (e) {
153
+ try {
154
+ console.error('[Zest] User callback threw:', e);
155
+ } catch (_) {
156
+ /* no-op */
157
+ }
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Strip comments and selector-level content from a customStyles string
164
+ * while still allowing property/value declarations scoped under the
165
+ * component selectors the author is targeting. We cannot fully sandbox
166
+ * CSS without a parser, but we can at least neutralise the most
167
+ * dangerous clickjacking vector (rules targeting Zest's own buttons).
168
+ *
169
+ * Returns the sanitized CSS string (possibly empty).
170
+ */
171
+ export function sanitizeCustomStyles(css) {
172
+ if (typeof css !== 'string' || css.length === 0) return '';
173
+
174
+ // Hard cap on size to avoid runaway payloads
175
+ if (css.length > 20000) return '';
176
+
177
+ // Remove CSS comments (can hide payloads)
178
+ let out = css.replace(/\/\*[\s\S]*?\*\//g, '');
179
+
180
+ // Block at-rules that can load external resources or alter behavior
181
+ out = out.replace(/@import\s+[^;]+;?/gi, '');
182
+ out = out.replace(/@charset\s+[^;]+;?/gi, '');
183
+
184
+ // Block url() values pointing outside of data: or https:
185
+ out = out.replace(/url\(\s*(['"]?)([^)'"]+)\1\s*\)/gi, (match, quote, value) => {
186
+ const v = value.trim().toLowerCase();
187
+ if (v.startsWith('https:') || v.startsWith('data:image/') || v.startsWith('/') || v.startsWith('#')) {
188
+ return match;
189
+ }
190
+ return 'url(#)';
191
+ });
192
+
193
+ // Block selectors that target the built-in reject button, which could
194
+ // be used to hide it for clickjacking consent bypass.
195
+ out = out.replace(/\.zest-btn--secondary\s*\{[^}]*\}/gi, '');
196
+ out = out.replace(/\[data-action\s*=\s*["']reject-all["']\]\s*\{[^}]*\}/gi, '');
197
+ out = out.replace(/\[data-action\s*=\s*["']accept-all["']\]\s*\{[^}]*\}/gi, '');
198
+
199
+ // Block expression() (ancient IE) and -moz-binding (ancient FF)
200
+ out = out.replace(/expression\s*\([^)]*\)/gi, '');
201
+ out = out.replace(/-moz-binding\s*:[^;}]*/gi, '');
202
+
203
+ return out;
204
+ }
@@ -4,6 +4,10 @@
4
4
 
5
5
  import { getCategoryForName } from './pattern-matcher.js';
6
6
 
7
+ // Upper bound on queued operations awaiting consent replay — unbounded
8
+ // growth would be a memory-exhaustion DoS vector.
9
+ const MAX_QUEUE_SIZE = 200;
10
+
7
11
  // Store originals
8
12
  let originalLocalStorage = null;
9
13
  let originalSessionStorage = null;
@@ -70,7 +74,7 @@ function createStorageProxy(storage, queue, storageName) {
70
74
 
71
75
  if (checkConsent(category)) {
72
76
  target.setItem(key, value);
73
- } else {
77
+ } else if (queue.length < MAX_QUEUE_SIZE) {
74
78
  queue.push({
75
79
  key,
76
80
  value,
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Core lifecycle - UI-agnostic initialization and consent actions.
3
+ *
4
+ * This module contains everything the main entry (with UI) and the
5
+ * headless entry (no UI) share: interceptor setup, consent load/save,
6
+ * replay, DNT handling, and the events/callbacks fan-out. It intentionally
7
+ * does NOT import anything from `./ui/*` so tree-shakers can drop the UI
8
+ * bundle entirely when only the headless API is used.
9
+ */
10
+
11
+ import { interceptCookies, setConsentChecker as setCookieChecker, replayCookies } from './core/cookie-interceptor.js';
12
+ import { interceptStorage, setConsentChecker as setStorageChecker, replayStorage } from './core/storage-interceptor.js';
13
+ import { startScriptBlocking, setConsentChecker as setScriptChecker, replayScripts } from './core/script-blocker.js';
14
+ import { setPatterns } from './core/pattern-matcher.js';
15
+ import { getCategoryIds } from './core/categories.js';
16
+ import { isDoNotTrackEnabled } from './core/dnt.js';
17
+ import { safeInvoke } from './core/security.js';
18
+
19
+ import { applyConsentSignals } from './integrations/consent-signals.js';
20
+
21
+ import { setConfig } from './config/parser.js';
22
+
23
+ import {
24
+ loadConsent,
25
+ hasConsent,
26
+ updateConsent,
27
+ acceptAll as storeAcceptAll,
28
+ rejectAll as storeRejectAll,
29
+ resetConsent,
30
+ hasConsentDecision
31
+ } from './storage/consent-store.js';
32
+ import { emitReady, emitConsent, emitReject, emitChange } from './storage/events.js';
33
+
34
+ let initialized = false;
35
+ let currentConfig = null;
36
+
37
+ function checkConsent(category) {
38
+ return hasConsent(category);
39
+ }
40
+
41
+ function replayAll(categories) {
42
+ replayCookies(categories);
43
+ replayStorage(categories);
44
+ replayScripts(categories);
45
+ }
46
+
47
+ /**
48
+ * Run the non-UI half of init. Returns a snapshot the caller (UI or
49
+ * headless) can use to decide what to do next.
50
+ */
51
+ export function coreInit(userConfig = {}) {
52
+ if (initialized) {
53
+ return {
54
+ alreadyInitialized: true,
55
+ config: currentConfig,
56
+ consent: loadConsent(),
57
+ hasDecision: hasConsentDecision(),
58
+ dntApplied: false
59
+ };
60
+ }
61
+
62
+ currentConfig = setConfig(userConfig);
63
+
64
+ // Push default-denied state to vendor consent mode APIs BEFORE any
65
+ // third-party script has a chance to fire.
66
+ applyConsentSignals(
67
+ { essential: true, functional: false, analytics: false, marketing: false },
68
+ currentConfig,
69
+ true
70
+ );
71
+
72
+ if (currentConfig.patterns) {
73
+ setPatterns(currentConfig.patterns);
74
+ }
75
+
76
+ setCookieChecker(checkConsent);
77
+ setStorageChecker(checkConsent);
78
+ setScriptChecker(checkConsent);
79
+
80
+ interceptCookies();
81
+ interceptStorage();
82
+ startScriptBlocking(currentConfig.mode, currentConfig.blockedDomains);
83
+
84
+ const consent = loadConsent();
85
+ initialized = true;
86
+
87
+ if (hasConsentDecision()) {
88
+ applyConsentSignals(consent, currentConfig, false);
89
+ }
90
+
91
+ // DNT / GPC handling — if the user signalled opt-out at the browser
92
+ // level and the site opts to respect it, auto-reject before the UI
93
+ // layer ever runs.
94
+ const dntEnabled = isDoNotTrackEnabled();
95
+ let dntApplied = false;
96
+
97
+ if (dntEnabled && currentConfig.respectDNT && currentConfig.dntBehavior !== 'ignore') {
98
+ if (currentConfig.dntBehavior === 'reject' && !hasConsentDecision()) {
99
+ const result = storeRejectAll(currentConfig.expiration);
100
+ dntApplied = true;
101
+ applyConsentSignals(result.current, currentConfig, false);
102
+ emitReject(result.current);
103
+ emitChange(result.current, result.previous);
104
+ safeInvoke(currentConfig.callbacks?.onReject);
105
+ safeInvoke(currentConfig.callbacks?.onChange, result.current);
106
+ }
107
+ }
108
+
109
+ emitReady(consent);
110
+ safeInvoke(currentConfig.callbacks?.onReady, consent);
111
+
112
+ return {
113
+ alreadyInitialized: false,
114
+ config: currentConfig,
115
+ consent,
116
+ hasDecision: hasConsentDecision(),
117
+ dntApplied
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Accept all categories, replay queued items, fire events + callbacks.
123
+ * Returns { current, previous } or null if not yet initialized.
124
+ */
125
+ export function coreAcceptAll() {
126
+ if (!initialized) return null;
127
+ const result = storeAcceptAll(currentConfig.expiration);
128
+ applyConsentSignals(result.current, currentConfig, false);
129
+ replayAll(getCategoryIds());
130
+ emitConsent(result.current, result.previous);
131
+ emitChange(result.current, result.previous);
132
+ safeInvoke(currentConfig.callbacks?.onAccept, result.current);
133
+ safeInvoke(currentConfig.callbacks?.onChange, result.current);
134
+ return result;
135
+ }
136
+
137
+ /**
138
+ * Reject all non-essential categories, fire events + callbacks.
139
+ */
140
+ export function coreRejectAll() {
141
+ if (!initialized) return null;
142
+ const result = storeRejectAll(currentConfig.expiration);
143
+ applyConsentSignals(result.current, currentConfig, false);
144
+ emitReject(result.current);
145
+ emitChange(result.current, result.previous);
146
+ safeInvoke(currentConfig.callbacks?.onReject);
147
+ safeInvoke(currentConfig.callbacks?.onChange, result.current);
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Save custom selections and replay only the newly-allowed categories.
153
+ */
154
+ export function coreUpdateConsent(selections) {
155
+ if (!initialized) return null;
156
+ const result = updateConsent(selections, currentConfig.expiration);
157
+ applyConsentSignals(result.current, currentConfig, false);
158
+
159
+ const newlyAllowed = Object.keys(result.current).filter(
160
+ (cat) => result.current[cat] && !result.previous[cat]
161
+ );
162
+ if (newlyAllowed.length > 0) {
163
+ replayAll(newlyAllowed);
164
+ }
165
+
166
+ const hasNonEssential = Object.entries(selections || {}).some(
167
+ ([cat, val]) => cat !== 'essential' && val
168
+ );
169
+ if (hasNonEssential) {
170
+ emitConsent(result.current, result.previous);
171
+ } else {
172
+ emitReject(result.current);
173
+ }
174
+ emitChange(result.current, result.previous);
175
+ safeInvoke(currentConfig.callbacks?.onChange, result.current);
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Clear the consent cookie. The caller is responsible for any UI reset.
181
+ */
182
+ export function coreReset() {
183
+ resetConsent();
184
+ }
185
+
186
+ export function isInitialized() {
187
+ return initialized;
188
+ }
189
+
190
+ export function getActiveConfig() {
191
+ return currentConfig;
192
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Zest Headless - consent logic only, zero UI.
3
+ *
4
+ * Use this entry when you want to bring your own banner / modal / settings
5
+ * markup and style it with your own CSS. No Shadow DOM is mounted, no
6
+ * inline stylesheet is injected, and nothing is attached to `window`.
7
+ *
8
+ * Everything you need to wire a custom UI is here:
9
+ *
10
+ * import Zest from '@freshjuice/zest/headless';
11
+ *
12
+ * Zest.init({ mode: 'safe', respectDNT: true });
13
+ *
14
+ * if (!Zest.hasConsentDecision()) {
15
+ * myBanner.show();
16
+ * }
17
+ *
18
+ * myAcceptBtn.addEventListener('click', () => Zest.acceptAll());
19
+ * myRejectBtn.addEventListener('click', () => Zest.rejectAll());
20
+ * mySaveBtn.addEventListener('click', () => {
21
+ * Zest.updateConsent({ analytics: true, marketing: false });
22
+ * });
23
+ *
24
+ * Zest.on(Zest.EVENTS.CHANGE, (e) => console.log(e.detail.consent));
25
+ *
26
+ * The headless build does NOT auto-initialize. You must call `init()`
27
+ * yourself, so you control exactly when interceptors come online.
28
+ */
29
+
30
+ import {
31
+ coreInit,
32
+ coreAcceptAll,
33
+ coreRejectAll,
34
+ coreUpdateConsent,
35
+ coreReset,
36
+ isInitialized,
37
+ getActiveConfig
38
+ } from './core-lifecycle.js';
39
+
40
+ import {
41
+ getConsent,
42
+ hasConsent,
43
+ hasConsentDecision,
44
+ getConsentProof
45
+ } from './storage/consent-store.js';
46
+
47
+ import { isDoNotTrackEnabled, getDNTDetails } from './core/dnt.js';
48
+
49
+ import { EVENTS, on, once } from './storage/events.js';
50
+
51
+ function init(userConfig = {}) {
52
+ const snapshot = coreInit(userConfig);
53
+ // Headless returns the snapshot so callers can decide whether to
54
+ // render their banner / settings UI based on hasDecision / dntApplied.
55
+ return snapshot;
56
+ }
57
+
58
+ function acceptAll() {
59
+ if (!isInitialized()) {
60
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
61
+ return null;
62
+ }
63
+ return coreAcceptAll();
64
+ }
65
+
66
+ function rejectAll() {
67
+ if (!isInitialized()) {
68
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
69
+ return null;
70
+ }
71
+ return coreRejectAll();
72
+ }
73
+
74
+ function updateConsent(selections) {
75
+ if (!isInitialized()) {
76
+ console.warn('[Zest] Not initialized. Call Zest.init() first.');
77
+ return null;
78
+ }
79
+ return coreUpdateConsent(selections);
80
+ }
81
+
82
+ function reset() {
83
+ coreReset();
84
+ }
85
+
86
+ const Zest = {
87
+ init,
88
+
89
+ // Consent state
90
+ getConsent,
91
+ hasConsent,
92
+ hasConsentDecision,
93
+ getConsentProof,
94
+
95
+ // Actions
96
+ acceptAll,
97
+ rejectAll,
98
+ updateConsent,
99
+ reset,
100
+
101
+ // DNT introspection
102
+ isDoNotTrackEnabled,
103
+ getDNTDetails,
104
+
105
+ // Events
106
+ on,
107
+ once,
108
+ EVENTS,
109
+
110
+ // Config introspection
111
+ getConfig: getActiveConfig
112
+ };
113
+
114
+ export default Zest;
115
+
116
+ // Named exports for tree-shake friendly consumers who only need a slice.
117
+ export {
118
+ init,
119
+ acceptAll,
120
+ rejectAll,
121
+ updateConsent,
122
+ reset,
123
+ getConsent,
124
+ hasConsent,
125
+ hasConsentDecision,
126
+ getConsentProof,
127
+ isDoNotTrackEnabled,
128
+ getDNTDetails,
129
+ on,
130
+ once,
131
+ EVENTS,
132
+ getActiveConfig as getConfig
133
+ };