@intranefr/superbackend 1.5.3 → 1.6.4

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 (106) hide show
  1. package/cookies.txt +6 -0
  2. package/cookies1.txt +6 -0
  3. package/cookies2.txt +6 -0
  4. package/cookies3.txt +6 -0
  5. package/cookies4.txt +5 -0
  6. package/cookies_old.txt +5 -0
  7. package/cookies_old_test.txt +6 -0
  8. package/cookies_super.txt +5 -0
  9. package/cookies_super_test.txt +6 -0
  10. package/cookies_test.txt +6 -0
  11. package/index.js +7 -0
  12. package/package.json +3 -1
  13. package/plugins/core-waiting-list-migration/README.md +118 -0
  14. package/plugins/core-waiting-list-migration/index.js +438 -0
  15. package/plugins/global-settings-presets/index.js +20 -0
  16. package/plugins/hello-cli/index.js +17 -0
  17. package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
  18. package/plugins/ui-components-seeder/components/suiToast.js +186 -0
  19. package/plugins/ui-components-seeder/index.js +31 -0
  20. package/public/js/admin-ui-components-preview.js +281 -0
  21. package/public/js/admin-ui-components.js +408 -0
  22. package/public/js/llm-provider-model-picker.js +193 -0
  23. package/public/test-iframe-fix.html +63 -0
  24. package/public/test-iframe.html +14 -0
  25. package/src/admin/endpointRegistry.js +68 -0
  26. package/src/controllers/admin.controller.js +25 -5
  27. package/src/controllers/adminDataCleanup.controller.js +45 -0
  28. package/src/controllers/adminLlm.controller.js +0 -8
  29. package/src/controllers/adminLogin.controller.js +269 -0
  30. package/src/controllers/adminPlugins.controller.js +55 -0
  31. package/src/controllers/adminRegistry.controller.js +106 -0
  32. package/src/controllers/adminStats.controller.js +4 -4
  33. package/src/controllers/registry.controller.js +32 -0
  34. package/src/controllers/waitingList.controller.js +52 -74
  35. package/src/middleware/auth.js +71 -1
  36. package/src/middleware/rbac.js +62 -0
  37. package/src/middleware.js +480 -156
  38. package/src/models/GlobalSetting.js +11 -1
  39. package/src/models/UiComponent.js +2 -0
  40. package/src/models/User.js +1 -1
  41. package/src/routes/admin.routes.js +3 -3
  42. package/src/routes/adminAgents.routes.js +2 -2
  43. package/src/routes/adminAssets.routes.js +11 -11
  44. package/src/routes/adminBlog.routes.js +2 -2
  45. package/src/routes/adminBlogAi.routes.js +2 -2
  46. package/src/routes/adminBlogAutomation.routes.js +2 -2
  47. package/src/routes/adminCache.routes.js +2 -2
  48. package/src/routes/adminConsoleManager.routes.js +2 -2
  49. package/src/routes/adminCrons.routes.js +2 -2
  50. package/src/routes/adminDataCleanup.routes.js +26 -0
  51. package/src/routes/adminDbBrowser.routes.js +2 -2
  52. package/src/routes/adminEjsVirtual.routes.js +2 -2
  53. package/src/routes/adminFeatureFlags.routes.js +6 -6
  54. package/src/routes/adminHeadless.routes.js +2 -2
  55. package/src/routes/adminHealthChecks.routes.js +2 -2
  56. package/src/routes/adminI18n.routes.js +2 -2
  57. package/src/routes/adminJsonConfigs.routes.js +8 -8
  58. package/src/routes/adminLlm.routes.js +8 -8
  59. package/src/routes/adminLogin.routes.js +23 -0
  60. package/src/routes/adminMarkdowns.routes.js +3 -9
  61. package/src/routes/adminMigration.routes.js +12 -12
  62. package/src/routes/adminPages.routes.js +2 -2
  63. package/src/routes/adminPlugins.routes.js +15 -0
  64. package/src/routes/adminProxy.routes.js +2 -2
  65. package/src/routes/adminRateLimits.routes.js +8 -8
  66. package/src/routes/adminRbac.routes.js +2 -2
  67. package/src/routes/adminRegistry.routes.js +24 -0
  68. package/src/routes/adminScripts.routes.js +2 -2
  69. package/src/routes/adminSeoConfig.routes.js +10 -10
  70. package/src/routes/adminTelegram.routes.js +2 -2
  71. package/src/routes/adminTerminals.routes.js +2 -2
  72. package/src/routes/adminUiComponents.routes.js +2 -2
  73. package/src/routes/adminUploadNamespaces.routes.js +7 -7
  74. package/src/routes/blogInternal.routes.js +2 -2
  75. package/src/routes/experiments.routes.js +2 -2
  76. package/src/routes/formsAdmin.routes.js +6 -6
  77. package/src/routes/globalSettings.routes.js +8 -8
  78. package/src/routes/internalExperiments.routes.js +2 -2
  79. package/src/routes/notificationAdmin.routes.js +7 -7
  80. package/src/routes/orgAdmin.routes.js +16 -16
  81. package/src/routes/pages.routes.js +3 -3
  82. package/src/routes/registry.routes.js +11 -0
  83. package/src/routes/stripeAdmin.routes.js +12 -12
  84. package/src/routes/userAdmin.routes.js +7 -7
  85. package/src/routes/waitingListAdmin.routes.js +2 -2
  86. package/src/routes/workflows.routes.js +3 -3
  87. package/src/services/dataCleanup.service.js +286 -0
  88. package/src/services/jsonConfigs.service.js +262 -0
  89. package/src/services/plugins.service.js +348 -0
  90. package/src/services/registry.service.js +452 -0
  91. package/src/services/uiComponents.service.js +180 -0
  92. package/src/services/waitingListJson.service.js +401 -0
  93. package/src/utils/rbac/rightsRegistry.js +118 -0
  94. package/test-access.js +63 -0
  95. package/test-iframe-fix.html +63 -0
  96. package/test-iframe.html +14 -0
  97. package/views/admin-403.ejs +92 -0
  98. package/views/admin-dashboard-home.ejs +52 -2
  99. package/views/admin-dashboard.ejs +143 -2
  100. package/views/admin-data-cleanup.ejs +357 -0
  101. package/views/admin-login.ejs +286 -0
  102. package/views/admin-plugins-system.ejs +223 -0
  103. package/views/admin-ui-components.ejs +82 -402
  104. package/views/admin-users.ejs +207 -11
  105. package/views/partials/dashboard/nav-items.ejs +2 -0
  106. package/views/partials/llm-provider-model-picker.ejs +0 -161
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ id: 'global-settings-presets',
3
+ name: 'Global Settings Presets',
4
+ version: '1.0.0',
5
+ description: 'Permissive-contract example plugin that can register useful global setting presets.',
6
+ tags: ['example', 'settings'],
7
+ async install(ctx) {
8
+ const globalSettings = ctx?.services?.globalSettings || null;
9
+ if (!globalSettings) {
10
+ console.log('[global-settings-presets] globalSettings service not found, skipping');
11
+ return;
12
+ }
13
+
14
+ console.log('[global-settings-presets] service detected; add your project defaults here');
15
+ // Example: create common config entries for integrations or environment presets.
16
+ },
17
+ bootstrap() {
18
+ console.log('[global-settings-presets] bootstrap hook ready');
19
+ },
20
+ };
@@ -0,0 +1,17 @@
1
+ module.exports = {
2
+ meta: {
3
+ id: 'hello-cli',
4
+ name: 'Hello CLI',
5
+ version: '1.0.0',
6
+ description: 'Prints a hello message to stdout on bootstrap.',
7
+ tags: ['example', 'cli'],
8
+ },
9
+ hooks: {
10
+ bootstrap() {
11
+ console.log('Hello terminal');
12
+ },
13
+ install() {
14
+ console.log('[hello-cli] install hook executed');
15
+ },
16
+ },
17
+ };
@@ -0,0 +1,212 @@
1
+ module.exports = {
2
+ code: 'sui_alert',
3
+ name: 'SUI Alert',
4
+ html: `<div class="sui-alert-overlay" data-sui-alert-overlay>
5
+ <div class="sui-alert" data-sui-alert>
6
+ <div class="sui-alert-header">
7
+ <span class="sui-alert-title" data-sui-alert-title></span>
8
+ <button class="sui-alert-close" data-sui-alert-close aria-label="Close">&times;</button>
9
+ </div>
10
+ <div class="sui-alert-body" data-sui-alert-body></div>
11
+ <div class="sui-alert-footer">
12
+ <button class="sui-alert-btn sui-alert-btn-primary" data-sui-alert-confirm>OK</button>
13
+ </div>
14
+ </div>
15
+ </div>`,
16
+ css: `.sui-alert-overlay {
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ background: rgba(0, 0, 0, 0.5);
23
+ display: none;
24
+ align-items: center;
25
+ justify-content: center;
26
+ z-index: 9999;
27
+ opacity: 0;
28
+ visibility: hidden;
29
+ transition: opacity 0.3s ease, visibility 0.3s ease;
30
+ }
31
+
32
+ .sui-alert-overlay.sui-alert-show {
33
+ opacity: 1;
34
+ visibility: visible;
35
+ }
36
+
37
+ .sui-alert {
38
+ background: white;
39
+ border-radius: 8px;
40
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
41
+ max-width: 400px;
42
+ width: 90%;
43
+ transform: scale(0.9);
44
+ transition: transform 0.3s ease;
45
+ }
46
+
47
+ .sui-alert-overlay.sui-alert-show .sui-alert {
48
+ transform: scale(1);
49
+ }
50
+
51
+ .sui-alert-header {
52
+ display: flex;
53
+ justify-content: space-between;
54
+ align-items: center;
55
+ padding: 16px 20px;
56
+ border-bottom: 1px solid #e5e7eb;
57
+ }
58
+
59
+ .sui-alert-title {
60
+ font-weight: 600;
61
+ font-size: 16px;
62
+ color: #111827;
63
+ }
64
+
65
+ .sui-alert-close {
66
+ background: none;
67
+ border: none;
68
+ font-size: 20px;
69
+ color: #6b7280;
70
+ cursor: pointer;
71
+ padding: 0;
72
+ width: 24px;
73
+ height: 24px;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ border-radius: 4px;
78
+ transition: background-color 0.2s;
79
+ }
80
+
81
+ .sui-alert-close:hover {
82
+ background-color: #f3f4f6;
83
+ }
84
+
85
+ .sui-alert-body {
86
+ padding: 20px;
87
+ color: #374151;
88
+ font-size: 14px;
89
+ line-height: 1.5;
90
+ }
91
+
92
+ .sui-alert-footer {
93
+ padding: 12px 20px 20px;
94
+ display: flex;
95
+ justify-content: flex-end;
96
+ }
97
+
98
+ .sui-alert-btn {
99
+ padding: 8px 16px;
100
+ border: none;
101
+ border-radius: 6px;
102
+ font-size: 14px;
103
+ font-weight: 500;
104
+ cursor: pointer;
105
+ transition: background-color 0.2s;
106
+ }
107
+
108
+ .sui-alert-btn-primary {
109
+ background-color: #3b82f6;
110
+ color: white;
111
+ }
112
+
113
+ .sui-alert-btn-primary:hover {
114
+ background-color: #2563eb;
115
+ }
116
+
117
+ .sui-alert-overlay.sui-alert-success .sui-alert-title { color: #059669; }
118
+ .sui-alert-overlay.sui-alert-warning .sui-alert-title { color: #d97706; }
119
+ .sui-alert-overlay.sui-alert-error .sui-alert-title { color: #dc2626; }`,
120
+ js: `let currentAlert = null;
121
+ let autoDismissTimer = null;
122
+
123
+ function showAlert(options = {}) {
124
+ const {
125
+ title = 'Alert',
126
+ message = '',
127
+ type = 'info',
128
+ confirmText = 'OK',
129
+ onConfirm = null,
130
+ autoDismiss = 0,
131
+ } = options;
132
+
133
+ if (currentAlert) hideAlert();
134
+
135
+ const overlay = templateRootEl.querySelector('[data-sui-alert-overlay]');
136
+ const titleEl = templateRootEl.querySelector('[data-sui-alert-title]');
137
+ const bodyEl = templateRootEl.querySelector('[data-sui-alert-body]');
138
+ const closeEl = templateRootEl.querySelector('[data-sui-alert-close]');
139
+ const confirmEl = templateRootEl.querySelector('[data-sui-alert-confirm]');
140
+
141
+ titleEl.textContent = title;
142
+ bodyEl.textContent = message;
143
+ confirmEl.textContent = confirmText;
144
+
145
+ overlay.className = 'sui-alert-overlay';
146
+ overlay.classList.add('sui-alert-' + type);
147
+
148
+ function hide() {
149
+ overlay.classList.remove('sui-alert-show');
150
+ setTimeout(() => {
151
+ overlay.style.display = 'none';
152
+ if (autoDismissTimer) {
153
+ clearTimeout(autoDismissTimer);
154
+ autoDismissTimer = null;
155
+ }
156
+ currentAlert = null;
157
+ }, 300);
158
+ }
159
+
160
+ closeEl.onclick = hide;
161
+ confirmEl.onclick = () => {
162
+ if (typeof onConfirm === 'function') onConfirm();
163
+ hide();
164
+ };
165
+
166
+ if (autoDismiss > 0) autoDismissTimer = setTimeout(hide, autoDismiss);
167
+
168
+ overlay.style.display = 'flex';
169
+ currentAlert = overlay;
170
+ requestAnimationFrame(() => overlay.classList.add('sui-alert-show'));
171
+ return hide;
172
+ }
173
+
174
+ function hideAlert() {
175
+ if (!currentAlert) return;
176
+ currentAlert.classList.remove('sui-alert-show');
177
+ currentAlert.style.display = 'none';
178
+ currentAlert = null;
179
+ }
180
+
181
+ return {
182
+ show: showAlert,
183
+ hide: hideAlert,
184
+ info: (title, message, options) => showAlert({ ...options, title, message, type: 'info' }),
185
+ success: (title, message, options) => showAlert({ ...options, title, message, type: 'success' }),
186
+ warning: (title, message, options) => showAlert({ ...options, title, message, type: 'warning' }),
187
+ error: (title, message, options) => showAlert({ ...options, title, message, type: 'error' }),
188
+ };`,
189
+ usageMarkdown: `# SUI Alert Component
190
+
191
+ Non-blocking customizable alert.
192
+
193
+ ## SDK Usage
194
+
195
+ \`\`\`javascript
196
+ await uiCmp.load('sui_alert');
197
+ const alertCmp = await uiCmp.sui_alert.create();
198
+ alertCmp.success('Done', 'Operation completed');
199
+ \`\`\`
200
+
201
+ ## Methods
202
+ - \`show(options)\`
203
+ - \`hide()\`
204
+ - \`info(title, message, options)\`
205
+ - \`success(title, message, options)\`
206
+ - \`warning(title, message, options)\`
207
+ - \`error(title, message, options)\``,
208
+ api: 'show(options), hide(), info(title,message,options), success(title,message,options), warning(title,message,options), error(title,message,options)',
209
+ previewExample: `const i = await uiCmp.create({});\ni.warning("ATTENTION");`,
210
+ version: '1.0.0',
211
+ isActive: true,
212
+ };
@@ -0,0 +1,186 @@
1
+ module.exports = {
2
+ code: 'sui_toast',
3
+ name: 'SUI Toast',
4
+ html: `<div class="sui-toast-container" data-sui-toast-container>
5
+ <div class="sui-toast" data-sui-toast-template>
6
+ <div class="sui-toast-icon" data-sui-toast-icon></div>
7
+ <div class="sui-toast-content">
8
+ <div class="sui-toast-title" data-sui-toast-title></div>
9
+ <div class="sui-toast-message" data-sui-toast-message></div>
10
+ </div>
11
+ <button class="sui-toast-close" data-sui-toast-close aria-label="Close">&times;</button>
12
+ </div>
13
+ </div>`,
14
+ css: `.sui-toast-container {
15
+ position: fixed;
16
+ top: 20px;
17
+ right: 20px;
18
+ z-index: 10000;
19
+ pointer-events: none;
20
+ }
21
+
22
+ .sui-toast {
23
+ background: white;
24
+ border-radius: 8px;
25
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
26
+ padding: 16px;
27
+ margin-bottom: 12px;
28
+ min-width: 300px;
29
+ max-width: 400px;
30
+ display: flex;
31
+ align-items: flex-start;
32
+ gap: 12px;
33
+ pointer-events: auto;
34
+ transform: translateX(100%);
35
+ opacity: 0;
36
+ transition: transform 0.3s ease, opacity 0.3s ease;
37
+ }
38
+
39
+ .sui-toast.sui-toast-show { transform: translateX(0); opacity: 1; }
40
+ .sui-toast.sui-toast-hide { transform: translateX(100%); opacity: 0; }
41
+
42
+ .sui-toast-icon { width: 20px; height: 20px; flex-shrink: 0; margin-top: 2px; }
43
+ .sui-toast-icon::before { content: ''; display: block; width: 100%; height: 100%; border-radius: 50%; }
44
+ .sui-toast.sui-toast-info .sui-toast-icon::before { background-color: #3b82f6; }
45
+ .sui-toast.sui-toast-success .sui-toast-icon::before { background-color: #059669; }
46
+ .sui-toast.sui-toast-warning .sui-toast-icon::before { background-color: #d97706; }
47
+ .sui-toast.sui-toast-error .sui-toast-icon::before { background-color: #dc2626; }
48
+
49
+ .sui-toast-content { flex: 1; min-width: 0; }
50
+ .sui-toast-title { font-weight: 600; font-size: 14px; color: #111827; margin-bottom: 4px; line-height: 1.4; }
51
+ .sui-toast-message { font-size: 13px; color: #6b7280; line-height: 1.4; }
52
+
53
+ .sui-toast-close {
54
+ background: none;
55
+ border: none;
56
+ font-size: 16px;
57
+ color: #9ca3af;
58
+ cursor: pointer;
59
+ padding: 0;
60
+ width: 20px;
61
+ height: 20px;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ border-radius: 4px;
66
+ transition: background-color 0.2s;
67
+ flex-shrink: 0;
68
+ }
69
+ .sui-toast-close:hover { background-color: #f3f4f6; color: #6b7280; }
70
+
71
+ .sui-toast-container.sui-toast-top-left { top: 20px; left: 20px; right: auto; }
72
+ .sui-toast-container.sui-toast-top-right { top: 20px; right: 20px; }
73
+ .sui-toast-container.sui-toast-bottom-left { bottom: 20px; left: 20px; right: auto; top: auto; }
74
+ .sui-toast-container.sui-toast-bottom-right { bottom: 20px; right: 20px; top: auto; }
75
+ .sui-toast-container.sui-toast-top-center { top: 20px; left: 50%; right: auto; transform: translateX(-50%); }
76
+ .sui-toast-container.sui-toast-bottom-center { bottom: 20px; left: 50%; right: auto; top: auto; transform: translateX(-50%); }`,
77
+ js: `let toastContainer = null;
78
+ let toastQueue = [];
79
+ let activeToasts = new Map();
80
+
81
+ function getContainer() {
82
+ if (!toastContainer) {
83
+ toastContainer = templateRootEl.querySelector('[data-sui-toast-container]');
84
+ }
85
+ return toastContainer;
86
+ }
87
+
88
+ function createToast(options = {}) {
89
+ const {
90
+ title = '',
91
+ message = '',
92
+ type = 'info',
93
+ duration = 5000,
94
+ position = 'top-right',
95
+ } = options;
96
+
97
+ const container = getContainer();
98
+ const template = container.querySelector('[data-sui-toast-template]');
99
+ const toastEl = template.cloneNode(true);
100
+ toastEl.removeAttribute('data-sui-toast-template');
101
+
102
+ const titleEl = toastEl.querySelector('[data-sui-toast-title]');
103
+ const messageEl = toastEl.querySelector('[data-sui-toast-message]');
104
+ const closeEl = toastEl.querySelector('[data-sui-toast-close]');
105
+
106
+ titleEl.textContent = title;
107
+ messageEl.textContent = message;
108
+
109
+ toastEl.className = 'sui-toast sui-toast-' + type;
110
+ container.className = 'sui-toast-container sui-toast-' + position;
111
+
112
+ function hide() {
113
+ toastEl.classList.add('sui-toast-hide');
114
+ setTimeout(() => {
115
+ if (container.contains(toastEl)) container.removeChild(toastEl);
116
+ activeToasts.delete(toastEl);
117
+ if (toastQueue.length > 0) {
118
+ const next = toastQueue.shift();
119
+ showToast(next);
120
+ }
121
+ }, 300);
122
+ }
123
+
124
+ closeEl.onclick = hide;
125
+
126
+ let timer = null;
127
+ if (duration > 0) timer = setTimeout(hide, duration);
128
+
129
+ return { element: toastEl, hide, timer };
130
+ }
131
+
132
+ function showToast(options = {}) {
133
+ if (activeToasts.size >= 5) {
134
+ toastQueue.push(options);
135
+ return null;
136
+ }
137
+
138
+ const toast = createToast(options);
139
+ const container = getContainer();
140
+ container.appendChild(toast.element);
141
+ activeToasts.set(toast.element, toast);
142
+
143
+ requestAnimationFrame(() => toast.element.classList.add('sui-toast-show'));
144
+ return toast.hide;
145
+ }
146
+
147
+ function clearAll() {
148
+ activeToasts.forEach((toast) => {
149
+ if (toast.timer) clearTimeout(toast.timer);
150
+ toast.hide();
151
+ });
152
+ toastQueue = [];
153
+ }
154
+
155
+ return {
156
+ show: showToast,
157
+ clear: clearAll,
158
+ info: (title, message, options) => showToast({ ...options, title, message, type: 'info' }),
159
+ success: (title, message, options) => showToast({ ...options, title, message, type: 'success' }),
160
+ warning: (title, message, options) => showToast({ ...options, title, message, type: 'warning' }),
161
+ error: (title, message, options) => showToast({ ...options, title, message, type: 'error' }),
162
+ };`,
163
+ usageMarkdown: `# SUI Toast Component
164
+
165
+ Non-blocking toast notification system.
166
+
167
+ ## SDK Usage
168
+
169
+ \`\`\`javascript
170
+ await uiCmp.load('sui_toast');
171
+ const toastCmp = await uiCmp.sui_toast.create();
172
+ toastCmp.success('Success', 'Toast works', { duration: 2500 });
173
+ \`\`\`
174
+
175
+ ## Methods
176
+ - \`show(options)\`
177
+ - \`clear()\`
178
+ - \`info(title, message, options)\`
179
+ - \`success(title, message, options)\`
180
+ - \`warning(title, message, options)\`
181
+ - \`error(title, message, options)\``,
182
+ api: 'show(options), clear(), info(title,message,options), success(title,message,options), warning(title,message,options), error(title,message,options)',
183
+ previewExample: `const i = await uiCmp.create({});\nawait i.success("All good");`,
184
+ version: '1.0.0',
185
+ isActive: true,
186
+ };
@@ -0,0 +1,31 @@
1
+ const alertComponent = require('./components/suiAlert');
2
+ const toastComponent = require('./components/suiToast');
3
+
4
+ module.exports = {
5
+ meta: {
6
+ id: 'sui-ui-components',
7
+ name: 'SUI - UI Components',
8
+ version: '1.1.0',
9
+ description: 'Simple UI Components - non-blocking alert and toast components',
10
+ tags: ['ui', 'components', 'alert', 'toast', 'sui'],
11
+ },
12
+ hooks: {
13
+ async install(ctx) {
14
+ const service = ctx?.services?.uiComponents || null;
15
+ if (!service) {
16
+ console.log('[sui-ui-components] uiComponents service not found, skipping seeding');
17
+ return;
18
+ }
19
+
20
+ console.log('[sui-ui-components] Installing SUI components...');
21
+
22
+ try {
23
+ await service.upsertComponent(alertComponent);
24
+ await service.upsertComponent(toastComponent);
25
+ console.log('[sui-ui-components] Successfully installed sui_alert and sui_toast components');
26
+ } catch (error) {
27
+ console.error('[sui-ui-components] Failed to install components:', error);
28
+ }
29
+ },
30
+ },
31
+ };