domma-cms 0.55.1 → 0.69.3
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.
- package/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -115,6 +115,350 @@ const PRESETS = [
|
|
|
115
115
|
delete: {enabled: false, access: 'admin'}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
,
|
|
119
|
+
// ---- Tools promoted out of plugins/ (0.67.0) --------------------------
|
|
120
|
+
// The slugs are what setupPlugin() created from `schema.slug`, NOT the
|
|
121
|
+
// directories the schemas used to live in (`user-notes`, `todos`). Keeping
|
|
122
|
+
// them exactly preserves every existing entry; changing one would seed an
|
|
123
|
+
// empty collection beside a site's real one.
|
|
124
|
+
//
|
|
125
|
+
// seedAll() rewrites schema.json on every boot, so `storage` is declared
|
|
126
|
+
// exactly as the plugins declared it - a site that had these in Mongo must
|
|
127
|
+
// not silently switch to files and lose sight of its rows. Sites with no
|
|
128
|
+
// connections.json fall back to the file adapter as they always did.
|
|
129
|
+
{
|
|
130
|
+
"slug": "notes-notes",
|
|
131
|
+
"title": "Notes",
|
|
132
|
+
"description": "Personal notes, one row per note, scoped to its author.",
|
|
133
|
+
"preset": true,
|
|
134
|
+
"fields": [
|
|
135
|
+
{
|
|
136
|
+
"name": "title",
|
|
137
|
+
"label": "Title",
|
|
138
|
+
"type": "text",
|
|
139
|
+
"required": false
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "content",
|
|
143
|
+
"label": "Content",
|
|
144
|
+
"type": "textarea",
|
|
145
|
+
"required": false
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "categories",
|
|
149
|
+
"label": "Categories",
|
|
150
|
+
"type": "text",
|
|
151
|
+
"required": false
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"name": "userId",
|
|
155
|
+
"label": "User ID",
|
|
156
|
+
"type": "text",
|
|
157
|
+
"required": false
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
"api": {
|
|
161
|
+
"create": {
|
|
162
|
+
"enabled": true,
|
|
163
|
+
"access": "admin"
|
|
164
|
+
},
|
|
165
|
+
"read": {
|
|
166
|
+
"enabled": true,
|
|
167
|
+
"access": "admin"
|
|
168
|
+
},
|
|
169
|
+
"update": {
|
|
170
|
+
"enabled": false,
|
|
171
|
+
"access": "admin"
|
|
172
|
+
},
|
|
173
|
+
"delete": {
|
|
174
|
+
"enabled": false,
|
|
175
|
+
"access": "admin"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
"storage": {
|
|
179
|
+
"adapter": "mongodb",
|
|
180
|
+
"connection": "default"
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"slug": "todo-items",
|
|
185
|
+
"title": "Todo Items",
|
|
186
|
+
"description": "Personal tasks, one row per task, scoped to its owner.",
|
|
187
|
+
"preset": true,
|
|
188
|
+
"fields": [
|
|
189
|
+
{
|
|
190
|
+
"name": "text",
|
|
191
|
+
"label": "Text",
|
|
192
|
+
"type": "text",
|
|
193
|
+
"required": true
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "status",
|
|
197
|
+
"label": "Status",
|
|
198
|
+
"type": "text",
|
|
199
|
+
"required": false
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "priority",
|
|
203
|
+
"label": "Priority",
|
|
204
|
+
"type": "text",
|
|
205
|
+
"required": false
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "dueAt",
|
|
209
|
+
"label": "Due At",
|
|
210
|
+
"type": "text",
|
|
211
|
+
"required": false
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "userId",
|
|
215
|
+
"label": "User ID",
|
|
216
|
+
"type": "text",
|
|
217
|
+
"required": false
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
"api": {
|
|
221
|
+
"create": {
|
|
222
|
+
"enabled": true,
|
|
223
|
+
"access": "admin"
|
|
224
|
+
},
|
|
225
|
+
"read": {
|
|
226
|
+
"enabled": true,
|
|
227
|
+
"access": "admin"
|
|
228
|
+
},
|
|
229
|
+
"update": {
|
|
230
|
+
"enabled": false,
|
|
231
|
+
"access": "admin"
|
|
232
|
+
},
|
|
233
|
+
"delete": {
|
|
234
|
+
"enabled": false,
|
|
235
|
+
"access": "admin"
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"storage": {
|
|
239
|
+
"adapter": "mongodb",
|
|
240
|
+
"connection": "default"
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
// ---- Contacts, promoted out of plugins/ (0.67.0) ----------------------
|
|
244
|
+
// Slugs are contacts-contacts and contacts-groups - NOT the directories the
|
|
245
|
+
// schemas lived in (user-contacts, user-contact-groups). Taken verbatim from
|
|
246
|
+
// those files so none of the 25 contact fields is lost in transcription, and
|
|
247
|
+
// `storage` stays `file` exactly as it was.
|
|
248
|
+
{
|
|
249
|
+
"slug": "contacts-contacts",
|
|
250
|
+
"title": "Contacts",
|
|
251
|
+
"description": "Contacts managed by the Contacts plugin.",
|
|
252
|
+
"fields": [
|
|
253
|
+
{
|
|
254
|
+
"name": "name",
|
|
255
|
+
"label": "Name",
|
|
256
|
+
"type": "text",
|
|
257
|
+
"required": true
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"name": "email",
|
|
261
|
+
"label": "Email",
|
|
262
|
+
"type": "text",
|
|
263
|
+
"required": false
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "phone",
|
|
267
|
+
"label": "Phone",
|
|
268
|
+
"type": "text",
|
|
269
|
+
"required": false
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
"name": "groups",
|
|
273
|
+
"label": "Groups",
|
|
274
|
+
"type": "text",
|
|
275
|
+
"required": false
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"name": "notes",
|
|
279
|
+
"label": "Notes",
|
|
280
|
+
"type": "textarea",
|
|
281
|
+
"required": false
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"name": "favourite",
|
|
285
|
+
"label": "Favourite",
|
|
286
|
+
"type": "text",
|
|
287
|
+
"required": false
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"name": "company",
|
|
291
|
+
"label": "Company",
|
|
292
|
+
"type": "text",
|
|
293
|
+
"required": false
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"name": "jobTitle",
|
|
297
|
+
"label": "Job Title",
|
|
298
|
+
"type": "text",
|
|
299
|
+
"required": false
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"name": "website",
|
|
303
|
+
"label": "Website",
|
|
304
|
+
"type": "text",
|
|
305
|
+
"required": false
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"name": "vatNumber",
|
|
309
|
+
"label": "VAT Number",
|
|
310
|
+
"type": "text",
|
|
311
|
+
"required": false
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"name": "companyNumber",
|
|
315
|
+
"label": "Company Number",
|
|
316
|
+
"type": "text",
|
|
317
|
+
"required": false
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"name": "reference",
|
|
321
|
+
"label": "Their Reference",
|
|
322
|
+
"type": "text",
|
|
323
|
+
"required": false
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "billingEmail",
|
|
327
|
+
"label": "Billing Email",
|
|
328
|
+
"type": "text",
|
|
329
|
+
"required": false
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"name": "paymentTerms",
|
|
333
|
+
"label": "Payment Terms (days)",
|
|
334
|
+
"type": "number",
|
|
335
|
+
"required": false
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "currency",
|
|
339
|
+
"label": "Preferred Currency",
|
|
340
|
+
"type": "text",
|
|
341
|
+
"required": false
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "addresses",
|
|
345
|
+
"label": "Addresses",
|
|
346
|
+
"type": "json",
|
|
347
|
+
"required": false
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"name": "emails",
|
|
351
|
+
"label": "Additional Emails",
|
|
352
|
+
"type": "json",
|
|
353
|
+
"required": false
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "phones",
|
|
357
|
+
"label": "Additional Phones",
|
|
358
|
+
"type": "json",
|
|
359
|
+
"required": false
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "accountName",
|
|
363
|
+
"label": "Account Name",
|
|
364
|
+
"type": "text",
|
|
365
|
+
"required": false
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "bankName",
|
|
369
|
+
"label": "Bank",
|
|
370
|
+
"type": "text",
|
|
371
|
+
"required": false
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "sortCode",
|
|
375
|
+
"label": "Sort Code",
|
|
376
|
+
"type": "text",
|
|
377
|
+
"required": false
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"name": "accountNumber",
|
|
381
|
+
"label": "Account Number",
|
|
382
|
+
"type": "text",
|
|
383
|
+
"required": false
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "iban",
|
|
387
|
+
"label": "IBAN",
|
|
388
|
+
"type": "text",
|
|
389
|
+
"required": false
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"name": "bic",
|
|
393
|
+
"label": "BIC / SWIFT",
|
|
394
|
+
"type": "text",
|
|
395
|
+
"required": false
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"name": "userId",
|
|
399
|
+
"label": "User ID",
|
|
400
|
+
"type": "text",
|
|
401
|
+
"required": false
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
"api": {
|
|
405
|
+
"create": {
|
|
406
|
+
"enabled": true,
|
|
407
|
+
"access": "admin"
|
|
408
|
+
},
|
|
409
|
+
"read": {
|
|
410
|
+
"enabled": true,
|
|
411
|
+
"access": "admin"
|
|
412
|
+
},
|
|
413
|
+
"update": {
|
|
414
|
+
"enabled": false,
|
|
415
|
+
"access": "admin"
|
|
416
|
+
},
|
|
417
|
+
"delete": {
|
|
418
|
+
"enabled": false,
|
|
419
|
+
"access": "admin"
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
"storage": {
|
|
423
|
+
"adapter": "file"
|
|
424
|
+
},
|
|
425
|
+
"preset": true
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"slug": "contacts-groups",
|
|
429
|
+
"title": "Contact Groups",
|
|
430
|
+
"description": "Groups for the Contacts plugin.",
|
|
431
|
+
"fields": [
|
|
432
|
+
{
|
|
433
|
+
"name": "name",
|
|
434
|
+
"label": "Group Name",
|
|
435
|
+
"type": "text",
|
|
436
|
+
"required": true
|
|
437
|
+
}
|
|
438
|
+
],
|
|
439
|
+
"api": {
|
|
440
|
+
"create": {
|
|
441
|
+
"enabled": true,
|
|
442
|
+
"access": "admin"
|
|
443
|
+
},
|
|
444
|
+
"read": {
|
|
445
|
+
"enabled": true,
|
|
446
|
+
"access": "admin"
|
|
447
|
+
},
|
|
448
|
+
"update": {
|
|
449
|
+
"enabled": false,
|
|
450
|
+
"access": "admin"
|
|
451
|
+
},
|
|
452
|
+
"delete": {
|
|
453
|
+
"enabled": false,
|
|
454
|
+
"access": "admin"
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
"storage": {
|
|
458
|
+
"adapter": "file"
|
|
459
|
+
},
|
|
460
|
+
"preset": true
|
|
461
|
+
}
|
|
118
462
|
];
|
|
119
463
|
|
|
120
464
|
/** Slugs exported for use in adapterRegistry and the delete guard. */
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Promoted-plugin migration - one-shot retirement of plugins that became core.
|
|
3
|
+
*
|
|
4
|
+
* A plugin that is promoted into the CMS leaves two traces on an existing
|
|
5
|
+
* install:
|
|
6
|
+
*
|
|
7
|
+
* 1. its `plugins.json` entry, which would otherwise offer a toggle for a
|
|
8
|
+
* feature that is no longer optional, and
|
|
9
|
+
* 2. `plugins/<slug>/` itself - the updater replaces the plugins it ships and
|
|
10
|
+
* adds new ones, but never removes one that has gone away. Left in place,
|
|
11
|
+
* the loader would register a SECOND copy of a feature the CMS now has of
|
|
12
|
+
* its own, which for a shortcode means two handlers racing for the same
|
|
13
|
+
* tag.
|
|
14
|
+
*
|
|
15
|
+
* This is the generic form of `search-migration.js`, which did the same job for
|
|
16
|
+
* `site-search` alone. Each promotion adds a row to PROMOTED rather than another
|
|
17
|
+
* near-identical file.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: with no entry and no directory there is nothing to do, which is
|
|
20
|
+
* the state of every fresh install.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'fs';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Plugins that are now part of the CMS.
|
|
27
|
+
*
|
|
28
|
+
* `author` is checked against the directory's manifest before anything is
|
|
29
|
+
* deleted. A directory of that name carrying somebody else's work is their
|
|
30
|
+
* plugin, not our retired one, and is left exactly where it is.
|
|
31
|
+
*
|
|
32
|
+
* `defaults` is what the plugin's own config.js shipped. Settings matching it
|
|
33
|
+
* are dropped silently; anything else is reported, because a site that tuned a
|
|
34
|
+
* setting deserves to be told where it went rather than have it vanish.
|
|
35
|
+
*
|
|
36
|
+
* `lift(settings, enabled)` moves those settings somewhere they still matter.
|
|
37
|
+
* A row without one is a plugin whose settings core does not need. It runs
|
|
38
|
+
* BEFORE the entry is deleted, so a throw leaves the entry in place and the
|
|
39
|
+
* next start tries again rather than losing the values.
|
|
40
|
+
*/
|
|
41
|
+
const PROMOTED = [
|
|
42
|
+
{
|
|
43
|
+
slug: 'demo-viewer',
|
|
44
|
+
author: 'Domma',
|
|
45
|
+
defaults: {basePath: '/public/demos', defaultHeight: '500px'},
|
|
46
|
+
note: 'the [demo] shortcode is built in'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
slug: 'notes',
|
|
50
|
+
author: 'Darryl Waterhouse',
|
|
51
|
+
defaults: {scope: 'user', maxPerUser: 1000, storage: {adapter: 'mongodb', connection: 'default'}},
|
|
52
|
+
note: 'Notes is a built-in Tool; its entries are untouched in notes-notes'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
slug: 'todo',
|
|
56
|
+
author: 'Darryl Waterhouse',
|
|
57
|
+
defaults: {scope: 'user', defaultPriority: 'medium', maxPerUser: 500, storage: {adapter: 'mongodb', connection: 'default'}},
|
|
58
|
+
note: 'Todo is a built-in Tool; its entries are untouched in todo-items'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
slug: 'contacts',
|
|
62
|
+
author: 'Darryl Waterhouse',
|
|
63
|
+
defaults: {scope: 'user'},
|
|
64
|
+
note: 'Contacts is a built-in Tool; contacts-contacts and contacts-groups are untouched'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
slug: 'analytics',
|
|
68
|
+
author: 'Darryl Waterhouse',
|
|
69
|
+
note: 'analytics is built in; counters moved to content/analytics/',
|
|
70
|
+
lift: liftAnalyticsCounters
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
slug: 'theme-switcher',
|
|
74
|
+
author: 'Domma',
|
|
75
|
+
note: 'the switcher is a theme setting now',
|
|
76
|
+
lift: liftThemeSwitcher
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Move the analytics counters out from under plugins/.
|
|
82
|
+
*
|
|
83
|
+
* This is the only lift that moves data a site cannot regenerate: lifetime.json,
|
|
84
|
+
* daily.json and journeys.json are real traffic history, tens to hundreds of
|
|
85
|
+
* kilobytes per site. The directory they sit in is about to be deleted, so the
|
|
86
|
+
* copy has to happen first and has to be verified.
|
|
87
|
+
*
|
|
88
|
+
* Copy, re-read, compare, and only then let the caller proceed. A mismatch
|
|
89
|
+
* throws, which abandons the whole row - the plugin directory stays, the entry
|
|
90
|
+
* stays, and the next start tries again. A site keeping a duplicate 96K for one
|
|
91
|
+
* release is not a problem; losing the original is.
|
|
92
|
+
*
|
|
93
|
+
* Files already present at the destination are left alone: a second run must not
|
|
94
|
+
* overwrite counters that have moved on since.
|
|
95
|
+
*
|
|
96
|
+
* @returns {string|null} what changed, for the log
|
|
97
|
+
*/
|
|
98
|
+
function liftAnalyticsCounters() {
|
|
99
|
+
const SRC = path.resolve('plugins', 'analytics', 'data');
|
|
100
|
+
const DEST = path.resolve('content', 'analytics');
|
|
101
|
+
if (!fs.existsSync(SRC)) return null;
|
|
102
|
+
|
|
103
|
+
fs.mkdirSync(DEST, {recursive: true});
|
|
104
|
+
const copied = [];
|
|
105
|
+
|
|
106
|
+
for (const name of ['lifetime.json', 'daily.json', 'journeys.json', 'stats.json']) {
|
|
107
|
+
const from = path.join(SRC, name);
|
|
108
|
+
const to = path.join(DEST, name);
|
|
109
|
+
if (!fs.existsSync(from) || fs.existsSync(to)) continue;
|
|
110
|
+
|
|
111
|
+
const raw = fs.readFileSync(from);
|
|
112
|
+
fs.writeFileSync(to, raw);
|
|
113
|
+
|
|
114
|
+
// Verified by comparing what is now on disk, not by trusting the write.
|
|
115
|
+
const back = fs.readFileSync(to);
|
|
116
|
+
if (!back.equals(raw)) {
|
|
117
|
+
throw new Error(`counters failed to copy intact (${name})`);
|
|
118
|
+
}
|
|
119
|
+
copied.push(`${name} (${raw.length} bytes)`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return copied.length
|
|
123
|
+
? `copied counters to content/analytics/: ${copied.join(', ')}`
|
|
124
|
+
: null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Move the theme switcher's placement into config/theme.json.
|
|
129
|
+
*
|
|
130
|
+
* `enabled` carries across deliberately: a site showing the disc keeps showing
|
|
131
|
+
* it, and one that never turned the plugin on does not suddenly acquire a
|
|
132
|
+
* visitor-facing theme picker.
|
|
133
|
+
*
|
|
134
|
+
* theme.json is read and written directly rather than through saveThemeConfig(),
|
|
135
|
+
* which validates and recompiles. Migrations run before the app is up, and a
|
|
136
|
+
* placement value is not worth pulling that machinery into startup. Written only
|
|
137
|
+
* when theme.json already exists - migrateThemeConfig() runs first and creates
|
|
138
|
+
* it, so an absent file means something is wrong enough not to paper over.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} settings
|
|
141
|
+
* @param {boolean} enabled
|
|
142
|
+
* @returns {string|null} what changed, for the log
|
|
143
|
+
*/
|
|
144
|
+
function liftThemeSwitcher(settings, enabled) {
|
|
145
|
+
const THEME_CONFIG = path.resolve('config', 'theme.json');
|
|
146
|
+
if (!fs.existsSync(THEME_CONFIG)) return null;
|
|
147
|
+
|
|
148
|
+
let cfg;
|
|
149
|
+
try {
|
|
150
|
+
cfg = JSON.parse(fs.readFileSync(THEME_CONFIG, 'utf8'));
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (cfg.switcher && cfg.switcher.enabled !== undefined) {
|
|
155
|
+
return null; // already lifted, or configured since
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const POSITIONS = ['right-center', 'left-center', 'bottom-right', 'bottom-left',
|
|
159
|
+
'top-left', 'top-right'];
|
|
160
|
+
const num = (v, fallback) =>
|
|
161
|
+
(Number.isFinite(Number(v)) && Number(v) >= 0 && Number(v) <= 400) ? Number(v) : fallback;
|
|
162
|
+
|
|
163
|
+
cfg.switcher = {
|
|
164
|
+
enabled,
|
|
165
|
+
position: POSITIONS.includes(settings.position) ? settings.position : 'right-center',
|
|
166
|
+
offsetX: num(settings.offsetX, 32),
|
|
167
|
+
offsetY: num(settings.offsetY, 32),
|
|
168
|
+
// The plugin's `themes` list was never read by its own markup, which
|
|
169
|
+
// hardcoded 26 dots. Starting empty means "every theme", which is what
|
|
170
|
+
// the markup effectively promised and never delivered.
|
|
171
|
+
themes: []
|
|
172
|
+
};
|
|
173
|
+
fs.writeFileSync(THEME_CONFIG, JSON.stringify(cfg, null, 2) + '\n');
|
|
174
|
+
return `lifted placement into theme.json (enabled: ${enabled})`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run the migration.
|
|
179
|
+
*
|
|
180
|
+
* Paths are resolved per call rather than at import, so the same process can run
|
|
181
|
+
* this against more than one install - which the tests do.
|
|
182
|
+
*
|
|
183
|
+
* @param {object[]} [table] - override for testing
|
|
184
|
+
* @returns {string[]} what changed, for the startup log (empty = nothing to do)
|
|
185
|
+
*/
|
|
186
|
+
export function migratePromotedPlugins(table = PROMOTED) {
|
|
187
|
+
const PLUGINS_CONFIG = path.resolve('config', 'plugins.json');
|
|
188
|
+
const changes = [];
|
|
189
|
+
|
|
190
|
+
let plugins = null;
|
|
191
|
+
try {
|
|
192
|
+
plugins = JSON.parse(fs.readFileSync(PLUGINS_CONFIG, 'utf8'));
|
|
193
|
+
} catch {
|
|
194
|
+
// No plugins.json, or a malformed one. Neither is this migration's
|
|
195
|
+
// business - a broken file is reported loudly elsewhere.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let dirty = false;
|
|
199
|
+
|
|
200
|
+
for (const {slug, author, defaults = {}, note, lift} of table) {
|
|
201
|
+
const stored = plugins?.[slug];
|
|
202
|
+
|
|
203
|
+
// 1. The entry, and any settings it carried.
|
|
204
|
+
if (plugins && stored) {
|
|
205
|
+
const settings = stored.settings || {};
|
|
206
|
+
|
|
207
|
+
if (lift) {
|
|
208
|
+
// A lift that throws must abandon THIS row entirely - entry and
|
|
209
|
+
// directory both. Deleting either after a failed lift is how a
|
|
210
|
+
// site loses the thing the lift was moving.
|
|
211
|
+
let lifted;
|
|
212
|
+
try {
|
|
213
|
+
lifted = lift(settings, stored.enabled === true);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
changes.push(`${slug}: NOT retired - ${err.message}`);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (lifted) changes.push(`${slug}: ${lifted}`);
|
|
219
|
+
} else {
|
|
220
|
+
const changed = Object.entries(settings)
|
|
221
|
+
.filter(([k, v]) => JSON.stringify(v) !== JSON.stringify(defaults[k]));
|
|
222
|
+
if (changed.length) {
|
|
223
|
+
changes.push(
|
|
224
|
+
`${slug}: dropped non-default settings (${changed.map(([k]) => k).join(', ')}) - ${note}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
delete plugins[slug];
|
|
230
|
+
dirty = true;
|
|
231
|
+
changes.push(`${slug}: removed the plugins.json entry`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 2. The directory - but only if it is the one we shipped.
|
|
235
|
+
const dir = path.resolve('plugins', slug);
|
|
236
|
+
if (!fs.existsSync(dir)) continue;
|
|
237
|
+
|
|
238
|
+
let ours = false;
|
|
239
|
+
try {
|
|
240
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'plugin.json'), 'utf8'));
|
|
241
|
+
ours = manifest.name === slug && manifest.author === author;
|
|
242
|
+
} catch { /* no readable manifest - leave it alone */ }
|
|
243
|
+
|
|
244
|
+
if (ours) {
|
|
245
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
246
|
+
changes.push(`${slug}: removed plugins/${slug}/`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (dirty) {
|
|
251
|
+
fs.writeFileSync(PLUGINS_CONFIG, JSON.stringify(plugins, null, 2) + '\n');
|
|
252
|
+
}
|
|
253
|
+
return changes;
|
|
254
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public asset CORS
|
|
3
|
+
*
|
|
4
|
+
* A module script is fetched in CORS mode, so `import … from
|
|
5
|
+
* 'https://this-site/public/whatever/index.js'` on another origin fails without
|
|
6
|
+
* Access-Control-Allow-Origin. A site that deliberately wants some of its own
|
|
7
|
+
* assets hot-linkable - engine source, demo code, a shared bundle - needs that
|
|
8
|
+
* header on those paths and nowhere else.
|
|
9
|
+
*
|
|
10
|
+
* `@fastify/cors` is registered once with `config/server.json`'s `cors` block and
|
|
11
|
+
* decides per ORIGIN, not per path, so it cannot express "open reads under this
|
|
12
|
+
* one prefix". Hence `cors.publicPrefixes`.
|
|
13
|
+
*
|
|
14
|
+
* Reads only. This never widens a write, and it never applies to a path that was
|
|
15
|
+
* not explicitly listed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read and sanitise the configured prefixes.
|
|
20
|
+
*
|
|
21
|
+
* A prefix must be an absolute path. Anything else is dropped rather than
|
|
22
|
+
* corrected: a typo that silently matched everything would be the one mistake
|
|
23
|
+
* here with real consequences.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} [corsConfig] - the `cors` block from config/server.json
|
|
26
|
+
* @returns {string[]}
|
|
27
|
+
*/
|
|
28
|
+
export function normalisePublicPrefixes(corsConfig) {
|
|
29
|
+
const raw = corsConfig?.publicPrefixes;
|
|
30
|
+
if (!Array.isArray(raw)) return [];
|
|
31
|
+
return raw.filter(p => typeof p === 'string' && p.startsWith('/') && p.length > 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Does this request qualify for the open-read header?
|
|
36
|
+
*
|
|
37
|
+
* @param {string} method
|
|
38
|
+
* @param {string} url - request URL, path first (query is ignored by startsWith)
|
|
39
|
+
* @param {string[]} prefixes - from normalisePublicPrefixes()
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isPublicAssetRead(method, url, prefixes) {
|
|
43
|
+
if (method !== 'GET' && method !== 'HEAD') return false;
|
|
44
|
+
if (!prefixes.length || typeof url !== 'string') return false;
|
|
45
|
+
return prefixes.some(p => url.startsWith(p));
|
|
46
|
+
}
|