@cogenta/schema 0.2.0 → 0.4.1
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/dist/define-collection.d.ts.map +1 -1
- package/dist/define-collection.js +53 -3
- package/dist/define-collection.js.map +1 -1
- package/dist/generate-types.d.ts.map +1 -1
- package/dist/generate-types.js +6 -1
- package/dist/generate-types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/media-usage.d.ts +56 -0
- package/dist/media-usage.d.ts.map +1 -0
- package/dist/media-usage.js +89 -0
- package/dist/media-usage.js.map +1 -0
- package/dist/rich-text.d.ts +28 -2
- package/dist/rich-text.d.ts.map +1 -1
- package/dist/rich-text.js +26 -3
- package/dist/rich-text.js.map +1 -1
- package/dist/routing/index.d.ts +5 -1
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +2 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/not-found-log.d.ts +78 -0
- package/dist/routing/not-found-log.d.ts.map +1 -0
- package/dist/routing/not-found-log.js +153 -0
- package/dist/routing/not-found-log.js.map +1 -0
- package/dist/routing/redirect-patterns.d.ts +61 -0
- package/dist/routing/redirect-patterns.d.ts.map +1 -0
- package/dist/routing/redirect-patterns.js +173 -0
- package/dist/routing/redirect-patterns.js.map +1 -0
- package/dist/routing/redirects.d.ts +27 -4
- package/dist/routing/redirects.d.ts.map +1 -1
- package/dist/routing/redirects.js +160 -48
- package/dist/routing/redirects.js.map +1 -1
- package/dist/scheduling/index.d.ts +2 -0
- package/dist/scheduling/index.d.ts.map +1 -1
- package/dist/scheduling/index.js +1 -0
- package/dist/scheduling/index.js.map +1 -1
- package/dist/scheduling/registry.d.ts +147 -0
- package/dist/scheduling/registry.d.ts.map +1 -0
- package/dist/scheduling/registry.js +318 -0
- package/dist/scheduling/registry.js.map +1 -0
- package/dist/search/extract.d.ts +24 -0
- package/dist/search/extract.d.ts.map +1 -1
- package/dist/search/extract.js +63 -8
- package/dist/search/extract.js.map +1 -1
- package/dist/search/index.d.ts +2 -1
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -9
- package/dist/search/index.js.map +1 -1
- package/dist/store/admin-theme-store.d.ts +26 -0
- package/dist/store/admin-theme-store.d.ts.map +1 -0
- package/dist/store/admin-theme-store.js +81 -0
- package/dist/store/admin-theme-store.js.map +1 -0
- package/dist/store/admin-theme-tables.d.ts +24 -0
- package/dist/store/admin-theme-tables.d.ts.map +1 -0
- package/dist/store/admin-theme-tables.js +41 -0
- package/dist/store/admin-theme-tables.js.map +1 -0
- package/dist/store/admin-theme-templates.d.ts +135 -0
- package/dist/store/admin-theme-templates.d.ts.map +1 -0
- package/dist/store/admin-theme-templates.js +217 -0
- package/dist/store/admin-theme-templates.js.map +1 -0
- package/dist/store/columns.d.ts.map +1 -1
- package/dist/store/columns.js +8 -2
- package/dist/store/columns.js.map +1 -1
- package/dist/store/diff.d.ts +40 -0
- package/dist/store/diff.d.ts.map +1 -1
- package/dist/store/diff.js +148 -0
- package/dist/store/diff.js.map +1 -1
- package/dist/store/index.d.ts +47 -4
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +26 -2
- package/dist/store/index.js.map +1 -1
- package/dist/store/maintenance-store.d.ts +34 -0
- package/dist/store/maintenance-store.d.ts.map +1 -0
- package/dist/store/maintenance-store.js +92 -0
- package/dist/store/maintenance-store.js.map +1 -0
- package/dist/store/menu-store.d.ts +146 -0
- package/dist/store/menu-store.d.ts.map +1 -0
- package/dist/store/menu-store.js +565 -0
- package/dist/store/menu-store.js.map +1 -0
- package/dist/store/menu-tables.d.ts +38 -0
- package/dist/store/menu-tables.d.ts.map +1 -0
- package/dist/store/menu-tables.js +117 -0
- package/dist/store/menu-tables.js.map +1 -0
- package/dist/store/naming.d.ts +1 -1
- package/dist/store/naming.d.ts.map +1 -1
- package/dist/store/naming.js +2 -0
- package/dist/store/naming.js.map +1 -1
- package/dist/store/pattern-store.d.ts +75 -0
- package/dist/store/pattern-store.d.ts.map +1 -0
- package/dist/store/pattern-store.js +118 -0
- package/dist/store/pattern-store.js.map +1 -0
- package/dist/store/pattern-tables.d.ts +29 -0
- package/dist/store/pattern-tables.d.ts.map +1 -0
- package/dist/store/pattern-tables.js +53 -0
- package/dist/store/pattern-tables.js.map +1 -0
- package/dist/store/read-only.d.ts +3 -3
- package/dist/store/read-only.d.ts.map +1 -1
- package/dist/store/read-only.js +4 -3
- package/dist/store/read-only.js.map +1 -1
- package/dist/store/redirect-tracking.d.ts +50 -0
- package/dist/store/redirect-tracking.d.ts.map +1 -0
- package/dist/store/redirect-tracking.js +91 -0
- package/dist/store/redirect-tracking.js.map +1 -0
- package/dist/store/review-transitions.d.ts +34 -0
- package/dist/store/review-transitions.d.ts.map +1 -0
- package/dist/store/review-transitions.js +51 -0
- package/dist/store/review-transitions.js.map +1 -0
- package/dist/store/role-permission-export.d.ts +22 -0
- package/dist/store/role-permission-export.d.ts.map +1 -0
- package/dist/store/role-permission-export.js +0 -0
- package/dist/store/role-permission-export.js.map +1 -0
- package/dist/store/role-permission-overlay.d.ts +27 -0
- package/dist/store/role-permission-overlay.d.ts.map +1 -0
- package/dist/store/role-permission-overlay.js +0 -0
- package/dist/store/role-permission-overlay.js.map +1 -0
- package/dist/store/role-permission-store.d.ts +72 -0
- package/dist/store/role-permission-store.d.ts.map +1 -0
- package/dist/store/role-permission-store.js +148 -0
- package/dist/store/role-permission-store.js.map +1 -0
- package/dist/store/role-permission-tables.d.ts +27 -0
- package/dist/store/role-permission-tables.d.ts.map +1 -0
- package/dist/store/role-permission-tables.js +52 -0
- package/dist/store/role-permission-tables.js.map +1 -0
- package/dist/store/scheduled-publish-enqueue.d.ts +37 -0
- package/dist/store/scheduled-publish-enqueue.d.ts.map +1 -0
- package/dist/store/scheduled-publish-enqueue.js +51 -0
- package/dist/store/scheduled-publish-enqueue.js.map +1 -0
- package/dist/store/scheduled-publish-failures.d.ts +36 -0
- package/dist/store/scheduled-publish-failures.d.ts.map +1 -0
- package/dist/store/scheduled-publish-failures.js +67 -0
- package/dist/store/scheduled-publish-failures.js.map +1 -0
- package/dist/store/schema-2-1-migration.d.ts +29 -0
- package/dist/store/schema-2-1-migration.d.ts.map +1 -0
- package/dist/store/schema-2-1-migration.js +71 -0
- package/dist/store/schema-2-1-migration.js.map +1 -0
- package/dist/store/search-console-store.d.ts +44 -0
- package/dist/store/search-console-store.d.ts.map +1 -0
- package/dist/store/search-console-store.js +90 -0
- package/dist/store/search-console-store.js.map +1 -0
- package/dist/store/search-console-tables.d.ts +14 -0
- package/dist/store/search-console-tables.d.ts.map +1 -0
- package/dist/store/search-console-tables.js +29 -0
- package/dist/store/search-console-tables.js.map +1 -0
- package/dist/store/search-indexing.d.ts +36 -0
- package/dist/store/search-indexing.d.ts.map +1 -1
- package/dist/store/search-indexing.js +33 -0
- package/dist/store/search-indexing.js.map +1 -1
- package/dist/store/site-settings-registry.d.ts +164 -0
- package/dist/store/site-settings-registry.d.ts.map +1 -0
- package/dist/store/site-settings-registry.js +1160 -0
- package/dist/store/site-settings-registry.js.map +1 -0
- package/dist/store/site-settings-store.d.ts +42 -0
- package/dist/store/site-settings-store.d.ts.map +1 -0
- package/dist/store/site-settings-store.js +97 -0
- package/dist/store/site-settings-store.js.map +1 -0
- package/dist/store/site-settings-tables.d.ts +29 -0
- package/dist/store/site-settings-tables.d.ts.map +1 -0
- package/dist/store/site-settings-tables.js +48 -0
- package/dist/store/site-settings-tables.js.map +1 -0
- package/dist/store/store.d.ts +57 -2
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +278 -37
- package/dist/store/store.js.map +1 -1
- package/dist/store/tables.d.ts.map +1 -1
- package/dist/store/tables.js +11 -0
- package/dist/store/tables.js.map +1 -1
- package/dist/store/taxonomy-store.d.ts.map +1 -1
- package/dist/store/taxonomy-store.js +90 -39
- package/dist/store/taxonomy-store.js.map +1 -1
- package/dist/store/taxonomy-usage.d.ts +47 -0
- package/dist/store/taxonomy-usage.d.ts.map +1 -0
- package/dist/store/taxonomy-usage.js +101 -0
- package/dist/store/taxonomy-usage.js.map +1 -0
- package/dist/store/theme-store.d.ts +73 -0
- package/dist/store/theme-store.d.ts.map +1 -0
- package/dist/store/theme-store.js +193 -0
- package/dist/store/theme-store.js.map +1 -0
- package/dist/store/types.d.ts +36 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/values.d.ts.map +1 -1
- package/dist/store/values.js +28 -1
- package/dist/store/values.js.map +1 -1
- package/dist/system-fields.d.ts +8 -1
- package/dist/system-fields.d.ts.map +1 -1
- package/dist/system-fields.js +14 -1
- package/dist/system-fields.js.map +1 -1
- package/dist/types.d.ts +54 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +21 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The declarative registry fiche 23 task 1 asks for: "un magasin clé/valeur
|
|
4
|
+
* typé, avec un schéma Zod déclarant chaque réglage : clé, type, valeur par
|
|
5
|
+
* défaut, portée (site ou locale), permission requise. Un réglage inconnu est
|
|
6
|
+
* refusé — pas un dépotoir de clés libres."
|
|
7
|
+
*
|
|
8
|
+
* Adding a setting is one entry here — `SiteSettingsStore` validates,
|
|
9
|
+
* defaults and scopes every key generically from this list (never a special
|
|
10
|
+
* case per key), and the admin's settings screen renders a field from each
|
|
11
|
+
* entry's `group`/`order`/`uiType` the same way (`site-settings-field.tsx`).
|
|
12
|
+
* That is what makes the fiche's own acceptance test true: "ajouter un
|
|
13
|
+
* réglage nouveau = une ligne de déclaration, et il apparaît à l'écran sans
|
|
14
|
+
* code d'interface supplémentaire."
|
|
15
|
+
*
|
|
16
|
+
* Every key here is **editorial** (ADR-0025's third category) — a rédacteur
|
|
17
|
+
* changes it from the admin, and it never duplicates a field
|
|
18
|
+
* `cogenta.config.mjs` already owns. `general.defaultLocale` and
|
|
19
|
+
* `reading.notFoundPath`, for instance, stay in the config file on purpose
|
|
20
|
+
* (section 8 of the fiche recommends migrating nothing that already exists);
|
|
21
|
+
* the settings screen shows them too, but read-only, mirrored from
|
|
22
|
+
* `SchemaDocument`/`Site.site` rather than declared here.
|
|
23
|
+
*/
|
|
24
|
+
/** How the admin renders a value — never a free-form widget choice per key. */
|
|
25
|
+
export const SITE_SETTING_UI_TYPES = [
|
|
26
|
+
'string',
|
|
27
|
+
'text',
|
|
28
|
+
'email',
|
|
29
|
+
'boolean',
|
|
30
|
+
'path',
|
|
31
|
+
'number',
|
|
32
|
+
'timeZone',
|
|
33
|
+
'dateStyle',
|
|
34
|
+
'timeStyle',
|
|
35
|
+
/**
|
|
36
|
+
* A closed choice, rendered as a `<select>`. Fiche 34 task 4's first user:
|
|
37
|
+
* `commerce.priceDisplay` (`ttc`/`ht`) — free text would let an operator
|
|
38
|
+
* type a third value the totals engine does not know what to do with.
|
|
39
|
+
*/
|
|
40
|
+
'select',
|
|
41
|
+
/**
|
|
42
|
+
* A single optional media reference, rendered as `MediaPicker` (fiche L21
|
|
43
|
+
* task 8's first and only user: `branding.customLogoMediaId`). The value
|
|
44
|
+
* stored is a media id, or `''` for "unset" — a plain string, like every
|
|
45
|
+
* other value this registry holds; nothing here talks to `MediaStore`
|
|
46
|
+
* directly, exactly the way a `path` setting names a route without
|
|
47
|
+
* checking the route exists.
|
|
48
|
+
*/
|
|
49
|
+
'media',
|
|
50
|
+
/**
|
|
51
|
+
* A short ordered list of `{label, url}` pairs (L25 D2's first and only
|
|
52
|
+
* user: `general.socialLinks`), rendered as a textarea of one
|
|
53
|
+
* `Label | https://url` line per entry, converted to/from JSON by the
|
|
54
|
+
* field component itself — the same "closest shape in the closed set"
|
|
55
|
+
* reasoning `seo.collectionTitleTemplates` already documents for a `text`
|
|
56
|
+
* bypass, except this one *is* a real, generic editor (a JSON object map
|
|
57
|
+
* has no natural line-per-entry form; an ordered list of two short strings
|
|
58
|
+
* does) rather than a placeholder for a bespoke screen.
|
|
59
|
+
*/
|
|
60
|
+
'linkList',
|
|
61
|
+
];
|
|
62
|
+
export const SITE_SETTING_GROUPS = [
|
|
63
|
+
'general',
|
|
64
|
+
'reading',
|
|
65
|
+
'discussion',
|
|
66
|
+
'media',
|
|
67
|
+
'privacy',
|
|
68
|
+
/**
|
|
69
|
+
* The shop-wide settings of fiche 34 task 4 — currency, tax-inclusive vs.
|
|
70
|
+
* exclusive display, countries served, minimum order, default backorder
|
|
71
|
+
* policy, and the invoice mentions of task 5. `commerce.tosPagePath` and
|
|
72
|
+
* `commerce.returnPolicyPagePath` are, deliberately, **paths** here —
|
|
73
|
+
* exactly the way `reading.homePath`/`privacy.policyPath` already point at
|
|
74
|
+
* a real content entry rather than storing legal text as a settings field
|
|
75
|
+
* (fiche 34 § pièges: "les CGV doivent être une page publique").
|
|
76
|
+
*/
|
|
77
|
+
'commerce',
|
|
78
|
+
/**
|
|
79
|
+
* Cogenta's own credit and white-label override (fiche L21 task 8) —
|
|
80
|
+
* whether the public footer and the admin shell name Cogenta at all, and
|
|
81
|
+
* what replaces it when they don't. Editorial on purpose: a reseller
|
|
82
|
+
* turns this off from the admin the same way any other setting here
|
|
83
|
+
* changes, with no redeploy and no code touched.
|
|
84
|
+
*/
|
|
85
|
+
'branding',
|
|
86
|
+
/**
|
|
87
|
+
* Search-engine visibility (fiche 21 task 3): title/description templates,
|
|
88
|
+
* per-collection sitemap hints, and social-card defaults. Previously
|
|
89
|
+
* `seo.tsx` was read-only "by design" — that was a scope choice of a
|
|
90
|
+
* previous lot, never an ADR, and this group is what makes it editable
|
|
91
|
+
* without inventing a second settings store: exactly the same registry
|
|
92
|
+
* that already backs `SettingsRoute`, just with its own screen
|
|
93
|
+
* (`SeoRoute`) rather than a tab there, the same way `commerce` gets its
|
|
94
|
+
* own screen instead of a `SettingsRoute` tab.
|
|
95
|
+
*/
|
|
96
|
+
'seo',
|
|
97
|
+
/**
|
|
98
|
+
* OpenTelemetry tracing and structured-log collection (fiche L22 task 5).
|
|
99
|
+
* Editorial on purpose, unlike the OTLP export destination (which stays in
|
|
100
|
+
* `cogenta.config.mjs`'s `observability` section, because it can carry a
|
|
101
|
+
* bearer-token header — rule R7): whether the local recent-events buffer
|
|
102
|
+
* runs at all, and how verbose it is, are exactly the kind of switch an
|
|
103
|
+
* admin should be able to flip from the "Exploitation" screen with no
|
|
104
|
+
* redeploy, the same way `discussion.enabled` already works.
|
|
105
|
+
*/
|
|
106
|
+
'observability',
|
|
107
|
+
/**
|
|
108
|
+
* L22 task 4 — which collections the writing assistant's vector index
|
|
109
|
+
* actually contains. Its own screen (the redesigned `AssistantRoute`),
|
|
110
|
+
* not a `SettingsRoute` tab, for the same reason `commerce`/`seo` get
|
|
111
|
+
* their own: the one key this group holds (`assistant.indexedCollections`)
|
|
112
|
+
* is a per-collection map, not a single scalar a generic form field can
|
|
113
|
+
* render — exactly `seo.sitemapCollectionSettings`'s own shape.
|
|
114
|
+
*/
|
|
115
|
+
'assistant',
|
|
116
|
+
/**
|
|
117
|
+
* The update system (L22 task 9): whether `cogenta serve`'s own
|
|
118
|
+
* `updates-auto-check` scheduled task may apply an npm update on its own,
|
|
119
|
+
* and how far it may go. Lives here, not in `cogenta.config.mjs`, for the
|
|
120
|
+
* same reason `discussion.moderationRequired` does — it is a live
|
|
121
|
+
* operational choice an admin flips from the screen at 3am because a
|
|
122
|
+
* package just misbehaved, not something that should need a git commit
|
|
123
|
+
* and a redeploy to change. Read by the scheduled task on every tick
|
|
124
|
+
* (`updates.ts` in `cogenta serve`'s wiring), never cached at startup.
|
|
125
|
+
*/
|
|
126
|
+
'updates',
|
|
127
|
+
/**
|
|
128
|
+
* Admin nav reordering/hiding (fiche 22 tâche 8, part 3) — which sidebar
|
|
129
|
+
* sections and entries a signed-in actor sees, and in what order.
|
|
130
|
+
* Deliberately **site-wide**, not per-browser: `sidebarCollapsed` and
|
|
131
|
+
* `groupOpen` (`app-shell.tsx`) stay in `localStorage` on purpose (each
|
|
132
|
+
* person's own "collapsed today" is not something anyone else needs to
|
|
133
|
+
* see), but "this site sells nothing, so hide Boutique for everyone" is an
|
|
134
|
+
* editorial decision about the site, no different from every other setting
|
|
135
|
+
* in this registry — it has to survive a different browser, a different
|
|
136
|
+
* person signing in, and a fresh install pulling the same database.
|
|
137
|
+
*/
|
|
138
|
+
'navigation',
|
|
139
|
+
/**
|
|
140
|
+
* Fiche 59 — the one non-secret, operator-set fact a channel's step-by-step
|
|
141
|
+
* guide needs to name the bot by handle instead of "the Cogenta bot": each
|
|
142
|
+
* channel's bot display name/handle, free text, never a credential. Bot
|
|
143
|
+
* *tokens* stay environment-only (R7) and never enter this table — this
|
|
144
|
+
* group exists specifically so a guide can say "open a chat with
|
|
145
|
+
* @my_cogenta_bot" without ever storing anything a leaked table could turn
|
|
146
|
+
* into a working credential.
|
|
147
|
+
*/
|
|
148
|
+
'channels',
|
|
149
|
+
];
|
|
150
|
+
export const SITE_SETTING_SCOPES = ['site', 'locale'];
|
|
151
|
+
const nonEmpty200 = z.string().max(200);
|
|
152
|
+
const freeText = z.string().max(4000);
|
|
153
|
+
const emailOrEmpty = z.union([z.literal(''), z.email()]);
|
|
154
|
+
const pathOrEmpty = z
|
|
155
|
+
.string()
|
|
156
|
+
.max(200)
|
|
157
|
+
.refine((value) => value === '' || value.startsWith('/'), {
|
|
158
|
+
error: 'A path must be empty (unset) or start with "/".',
|
|
159
|
+
});
|
|
160
|
+
/** A path or an absolute URL, or empty — `seo.defaultSocialImageUrl`'s own shape (fiche 21 task 3). */
|
|
161
|
+
const urlOrPathOrEmpty = z
|
|
162
|
+
.string()
|
|
163
|
+
.max(500)
|
|
164
|
+
.refine((value) => value === '' || value.startsWith('/') || /^https?:\/\//u.test(value), {
|
|
165
|
+
error: 'Must be empty, a site-relative path starting with "/", or an absolute http(s) URL.',
|
|
166
|
+
});
|
|
167
|
+
/**
|
|
168
|
+
* `%title%`/`%site%` composition (`@cogenta/seo`'s `applyTitleTemplate`).
|
|
169
|
+
* Required to actually contain `%title%` when set — a template that drops it
|
|
170
|
+
* silently replaces every title on the site with the same static string,
|
|
171
|
+
* which is never what "add a suffix" meant.
|
|
172
|
+
*/
|
|
173
|
+
const titleTemplateOrEmpty = z
|
|
174
|
+
.string()
|
|
175
|
+
.max(200)
|
|
176
|
+
.refine((value) => value === '' || value.includes('%title%'), {
|
|
177
|
+
error: 'A title template must include the %title% token, or be left empty.',
|
|
178
|
+
});
|
|
179
|
+
/** `seo.collectionTitleTemplates` — `titleTemplateOrEmpty`, one per collection name (fiche 21 task 3). */
|
|
180
|
+
const collectionTitleTemplates = z.record(z.string(), titleTemplateOrEmpty);
|
|
181
|
+
const metaDescriptionOrEmpty = z.string().max(500);
|
|
182
|
+
/**
|
|
183
|
+
* A Google/Bing site-verification token, or empty (fiche 50 task 2). Neither
|
|
184
|
+
* provider documents a fixed format — Google's runs to roughly 44 base64-ish
|
|
185
|
+
* characters, Bing's is a GUID — so this only rules out whitespace and quote
|
|
186
|
+
* characters rather than pinning a shape that would break the moment either
|
|
187
|
+
* provider changes its own generator. The value is rendered into a
|
|
188
|
+
* `<meta content="…">` attribute, HTML-escaped there (`escapeHtmlAttribute`)
|
|
189
|
+
* exactly like every other user-supplied attribute this codebase emits, so
|
|
190
|
+
* this schema's job is plausibility, not the actual injection guard.
|
|
191
|
+
*/
|
|
192
|
+
const siteVerificationTokenOrEmpty = z
|
|
193
|
+
.string()
|
|
194
|
+
.max(255)
|
|
195
|
+
.regex(/^[^\s<>"']*$/u, {
|
|
196
|
+
error: 'Must be empty, or the verification token as the provider gave it — no spaces or quotes.',
|
|
197
|
+
});
|
|
198
|
+
/**
|
|
199
|
+
* IndexNow's own key format (fiche 50 task 3) — mirrors `@cogenta/seo`'s
|
|
200
|
+
* `KEY_PATTERN` (`indexnow.ts`) verbatim. Duplicated rather than imported:
|
|
201
|
+
* `@cogenta/schema` cannot depend on `@cogenta/seo`, which depends on it the
|
|
202
|
+
* other way round.
|
|
203
|
+
*/
|
|
204
|
+
const indexNowKeyOrEmpty = z
|
|
205
|
+
.string()
|
|
206
|
+
.max(128)
|
|
207
|
+
.regex(/^$|^[a-fA-F0-9]{8,128}$/u, {
|
|
208
|
+
error: 'Must be empty, or 8 to 128 hexadecimal characters — generate one with crypto.randomUUID().replaceAll("-", "").',
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* A channel bot's display name/handle (fiche 59) — free text, deliberately
|
|
212
|
+
* unconstrained in shape: `@my_bot` (Telegram), an app name (Slack), or
|
|
213
|
+
* `MyBot#1234` (Discord) are all valid, and this field's only job is
|
|
214
|
+
* plausibility (no newline, a sane length), never a format check tied to one
|
|
215
|
+
* platform. Never a credential — see this key's own registry entry.
|
|
216
|
+
*/
|
|
217
|
+
const botNameOrEmpty = z
|
|
218
|
+
.string()
|
|
219
|
+
.max(80)
|
|
220
|
+
.refine((value) => !/[\r\n]/u.test(value), {
|
|
221
|
+
error: 'A bot name is a single line — remove any line break.',
|
|
222
|
+
});
|
|
223
|
+
/** A Twitter/X `@handle` for `twitter:site`, or empty. */
|
|
224
|
+
const twitterHandleOrEmpty = z
|
|
225
|
+
.string()
|
|
226
|
+
.max(16)
|
|
227
|
+
.refine((value) => value === '' || /^@[A-Za-z0-9_]{1,15}$/u.test(value), {
|
|
228
|
+
error: 'Must be empty, or a handle starting with "@" (letters, digits, underscore, max 15 characters).',
|
|
229
|
+
});
|
|
230
|
+
const changeFrequencyOrEmpty = z.enum([
|
|
231
|
+
'',
|
|
232
|
+
'always',
|
|
233
|
+
'hourly',
|
|
234
|
+
'daily',
|
|
235
|
+
'weekly',
|
|
236
|
+
'monthly',
|
|
237
|
+
'yearly',
|
|
238
|
+
'never',
|
|
239
|
+
]);
|
|
240
|
+
/**
|
|
241
|
+
* A comma-separated list of bare tokens — a nav group id (`content`,
|
|
242
|
+
* `commerce`, …) or the `to` path of a nav item (`/collections`,
|
|
243
|
+
* `/commerce/products`, …). The same shape `content.newEntryDefaultBlocks`
|
|
244
|
+
* already uses for "an ordered subset of a vocabulary this package cannot
|
|
245
|
+
* import" (fiche 22 tâche 8, part 3): `@cogenta/schema` has no dependency on
|
|
246
|
+
* `@cogenta/admin`'s `nav-items.ts` any more than it does on
|
|
247
|
+
* `@cogenta/blocks`'s block vocabulary, so this only shapes the string —
|
|
248
|
+
* `@cogenta/admin`'s own `nav-layout.ts` is what filters out a token that
|
|
249
|
+
* does not name a real group or item. Empty means "no override": the
|
|
250
|
+
* admin's own shipped order and visibility apply untouched.
|
|
251
|
+
*/
|
|
252
|
+
const navTokenList = z
|
|
253
|
+
.string()
|
|
254
|
+
.max(4000)
|
|
255
|
+
.regex(/^$|^[a-zA-Z0-9/_-]+(\s*,\s*[a-zA-Z0-9/_-]+)*$/u, {
|
|
256
|
+
error: 'A comma-separated list of navigation section or entry ids.',
|
|
257
|
+
});
|
|
258
|
+
/** `seo.sitemapCollectionSettings` — one override per collection name (fiche 21 task 3, mirrors `@cogenta/seo`'s `SitemapCollectionOverride`). */
|
|
259
|
+
const sitemapCollectionSettings = z.record(z.string(), z.object({
|
|
260
|
+
included: z.boolean(),
|
|
261
|
+
changefreq: changeFrequencyOrEmpty,
|
|
262
|
+
// '' means "no hint" — kept as a string in storage so an empty text
|
|
263
|
+
// input round-trips without becoming `NaN` on the way back in.
|
|
264
|
+
priority: z.union([z.literal(''), z.number().min(0).max(1)]),
|
|
265
|
+
}));
|
|
266
|
+
/**
|
|
267
|
+
* `general.socialLinks` (L25 D2) — the site's own social profile links, in
|
|
268
|
+
* the order a footer shows them. Twelve is the footer's own layout budget
|
|
269
|
+
* (`renderSocialLinks`, `@cogenta/theme-kit`): past a dozen icons a footer
|
|
270
|
+
* stops reading as a footer, and there is no legitimate reason a real site
|
|
271
|
+
* needs more distinct platforms than that. `url` must be `http(s)`: a footer
|
|
272
|
+
* `<a>` is not a place for a `javascript:`/`mailto:` surprise, and a real
|
|
273
|
+
* social profile is never anything else.
|
|
274
|
+
*/
|
|
275
|
+
const socialLinkEntry = z.object({
|
|
276
|
+
label: z.string().min(1).max(40),
|
|
277
|
+
url: z
|
|
278
|
+
.string()
|
|
279
|
+
.max(500)
|
|
280
|
+
.refine((value) => /^https?:\/\//u.test(value), {
|
|
281
|
+
error: 'Must be an http(s) URL.',
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
const socialLinksSetting = z.array(socialLinkEntry).max(12);
|
|
285
|
+
/**
|
|
286
|
+
* `assistant.indexedCollections` (L22 task 4) — one boolean per collection
|
|
287
|
+
* name, `false` meaning "excluded from the vector index". A collection
|
|
288
|
+
* absent from the map is included: the opt-out shape is what keeps a site
|
|
289
|
+
* that has never touched this setting indexing exactly as it always has.
|
|
290
|
+
*/
|
|
291
|
+
const indexedCollectionsSettings = z.record(z.string(), z.boolean());
|
|
292
|
+
/**
|
|
293
|
+
* A real IANA time zone name, or empty for "not set". `Intl.DateTimeFormat`
|
|
294
|
+
* throws `RangeError` on anything it does not recognise — the same check
|
|
295
|
+
* every JS runtime already performs, reused rather than a hand-rolled list of
|
|
296
|
+
* zone names to keep in sync with the platform's own tzdata.
|
|
297
|
+
*/
|
|
298
|
+
function isValidTimeZone(value) {
|
|
299
|
+
if (value === '')
|
|
300
|
+
return true;
|
|
301
|
+
try {
|
|
302
|
+
new Intl.DateTimeFormat(undefined, { timeZone: value });
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const timeZoneOrEmpty = z
|
|
310
|
+
.string()
|
|
311
|
+
.max(64)
|
|
312
|
+
.refine(isValidTimeZone, { error: 'Not a recognised IANA time zone name (e.g. "Europe/Paris").' });
|
|
313
|
+
const dateTimeStyle = z.enum(['full', 'long', 'medium', 'short']);
|
|
314
|
+
const ADMIN_ONLY = ['admin'];
|
|
315
|
+
export const SITE_SETTINGS_REGISTRY = [
|
|
316
|
+
// General
|
|
317
|
+
{
|
|
318
|
+
key: 'general.title',
|
|
319
|
+
group: 'general',
|
|
320
|
+
order: 0,
|
|
321
|
+
uiType: 'string',
|
|
322
|
+
scope: 'site',
|
|
323
|
+
schema: nonEmpty200,
|
|
324
|
+
defaultValue: '',
|
|
325
|
+
writeRoles: ADMIN_ONLY,
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: 'general.tagline',
|
|
329
|
+
group: 'general',
|
|
330
|
+
order: 1,
|
|
331
|
+
uiType: 'string',
|
|
332
|
+
scope: 'locale',
|
|
333
|
+
schema: nonEmpty200,
|
|
334
|
+
defaultValue: '',
|
|
335
|
+
writeRoles: ADMIN_ONLY,
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
key: 'general.adminEmail',
|
|
339
|
+
group: 'general',
|
|
340
|
+
order: 2,
|
|
341
|
+
uiType: 'email',
|
|
342
|
+
scope: 'site',
|
|
343
|
+
schema: emailOrEmpty,
|
|
344
|
+
defaultValue: '',
|
|
345
|
+
writeRoles: ADMIN_ONLY,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
key: 'general.timeZone',
|
|
349
|
+
group: 'general',
|
|
350
|
+
order: 3,
|
|
351
|
+
uiType: 'timeZone',
|
|
352
|
+
scope: 'site',
|
|
353
|
+
schema: timeZoneOrEmpty,
|
|
354
|
+
defaultValue: '',
|
|
355
|
+
writeRoles: ADMIN_ONLY,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
key: 'general.dateStyle',
|
|
359
|
+
group: 'general',
|
|
360
|
+
order: 4,
|
|
361
|
+
uiType: 'dateStyle',
|
|
362
|
+
scope: 'site',
|
|
363
|
+
schema: dateTimeStyle,
|
|
364
|
+
defaultValue: 'medium',
|
|
365
|
+
writeRoles: ADMIN_ONLY,
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
key: 'general.timeStyle',
|
|
369
|
+
group: 'general',
|
|
370
|
+
order: 5,
|
|
371
|
+
uiType: 'timeStyle',
|
|
372
|
+
scope: 'site',
|
|
373
|
+
schema: dateTimeStyle,
|
|
374
|
+
defaultValue: 'short',
|
|
375
|
+
writeRoles: ADMIN_ONLY,
|
|
376
|
+
},
|
|
377
|
+
/**
|
|
378
|
+
* L21 task 5's "blocs de départ" — the admin's new-entry flow prefills a
|
|
379
|
+
* fresh `blocks` field with these, instead of the empty array it wrote
|
|
380
|
+
* before this setting existed, so an MCP call or a person opening a new
|
|
381
|
+
* page never has to know every block type just to get something sane on
|
|
382
|
+
* the page. Kept out of contract A on purpose (`defineCollection` gains no
|
|
383
|
+
* `defaultBlocks` option — that would be a frozen-schema change, and the
|
|
384
|
+
* lot's own note says to avoid it): this is a pure admin default, not
|
|
385
|
+
* something a headless client's own `POST` ever sees or is bound by.
|
|
386
|
+
*
|
|
387
|
+
* A comma-separated list of contract B block type names rather than a new
|
|
388
|
+
* `uiType` — the same shape `commerce.countriesServed` already uses for a
|
|
389
|
+
* short closed-ish list — because `@cogenta/schema` cannot import
|
|
390
|
+
* `@cogenta/blocks` (the dependency runs the other way) to validate
|
|
391
|
+
* against the real vocabulary here; the admin's own copy
|
|
392
|
+
* (`blocks/vocabulary.ts`) does that filtering when it reads this value,
|
|
393
|
+
* silently dropping a name it does not recognise rather than crashing the
|
|
394
|
+
* new-entry screen over a typo in a setting.
|
|
395
|
+
*/
|
|
396
|
+
{
|
|
397
|
+
key: 'content.newEntryDefaultBlocks',
|
|
398
|
+
group: 'general',
|
|
399
|
+
order: 6,
|
|
400
|
+
uiType: 'string',
|
|
401
|
+
scope: 'site',
|
|
402
|
+
schema: z
|
|
403
|
+
.string()
|
|
404
|
+
.max(500)
|
|
405
|
+
.regex(/^$|^[a-zA-Z][a-zA-Z0-9]*(\s*,\s*[a-zA-Z][a-zA-Z0-9]*)*$/u, {
|
|
406
|
+
error: 'A comma-separated list of block type names, e.g. "prose". Empty means no starting blocks.',
|
|
407
|
+
}),
|
|
408
|
+
defaultValue: 'prose',
|
|
409
|
+
writeRoles: ADMIN_ONLY,
|
|
410
|
+
},
|
|
411
|
+
/**
|
|
412
|
+
* L25 D2 — the site's own social profiles, read by `resolveChromeExtras`
|
|
413
|
+
* (`@cogenta/cli`) and rendered by `renderSocialLinks`
|
|
414
|
+
* (`@cogenta/theme-kit`) in every built-in theme's footer. Site-scoped,
|
|
415
|
+
* unlike `general.tagline`: a Mastodon handle does not change per locale
|
|
416
|
+
* the way an editorial tagline does.
|
|
417
|
+
*/
|
|
418
|
+
{
|
|
419
|
+
key: 'general.socialLinks',
|
|
420
|
+
group: 'general',
|
|
421
|
+
order: 7,
|
|
422
|
+
uiType: 'linkList',
|
|
423
|
+
scope: 'site',
|
|
424
|
+
schema: socialLinksSetting,
|
|
425
|
+
defaultValue: [],
|
|
426
|
+
writeRoles: ADMIN_ONLY,
|
|
427
|
+
},
|
|
428
|
+
/**
|
|
429
|
+
* L25 D2 — a short line an editor writes once (a legal mention, an
|
|
430
|
+
* "about" sentence, an address) that a theme's footer shows as its own
|
|
431
|
+
* short "about" column (`ChromeInput.footerNote`, contract D `theme@1.4`).
|
|
432
|
+
* Locale-scoped like `general.tagline` right above it: this is prose an
|
|
433
|
+
* editor writes, not a fixed fact like a social handle.
|
|
434
|
+
*/
|
|
435
|
+
{
|
|
436
|
+
key: 'general.footerNote',
|
|
437
|
+
group: 'general',
|
|
438
|
+
order: 8,
|
|
439
|
+
uiType: 'text',
|
|
440
|
+
scope: 'locale',
|
|
441
|
+
schema: z.string().max(400),
|
|
442
|
+
defaultValue: '',
|
|
443
|
+
writeRoles: ADMIN_ONLY,
|
|
444
|
+
},
|
|
445
|
+
// Reading
|
|
446
|
+
{
|
|
447
|
+
key: 'reading.homePath',
|
|
448
|
+
group: 'reading',
|
|
449
|
+
order: 0,
|
|
450
|
+
uiType: 'path',
|
|
451
|
+
scope: 'site',
|
|
452
|
+
schema: pathOrEmpty,
|
|
453
|
+
defaultValue: '',
|
|
454
|
+
writeRoles: ADMIN_ONLY,
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
key: 'reading.postsPerPage',
|
|
458
|
+
group: 'reading',
|
|
459
|
+
order: 1,
|
|
460
|
+
uiType: 'number',
|
|
461
|
+
scope: 'site',
|
|
462
|
+
schema: z.number().int().min(1).max(100),
|
|
463
|
+
defaultValue: 10,
|
|
464
|
+
writeRoles: ADMIN_ONLY,
|
|
465
|
+
},
|
|
466
|
+
// Discussion (fiche 15 task 5, ADR-0025) — the site-wide defaults every
|
|
467
|
+
// collection and every entry inherit from unless it has its own override.
|
|
468
|
+
// Per-collection and per-entry overrides do NOT live here: this registry
|
|
469
|
+
// is site/locale scoped only, with no notion of "per collection" — they
|
|
470
|
+
// live in `@cogenta/comments`'s own `CommentSettingsStore` instead (see
|
|
471
|
+
// its module comment for why), which reads `discussion.enabled` and
|
|
472
|
+
// `discussion.moderationRequired` as the bottom of its own inheritance
|
|
473
|
+
// chain (`effectiveEnabled`/`effectiveModerationRequired`).
|
|
474
|
+
{
|
|
475
|
+
key: 'discussion.enabled',
|
|
476
|
+
group: 'discussion',
|
|
477
|
+
order: 0,
|
|
478
|
+
uiType: 'boolean',
|
|
479
|
+
scope: 'site',
|
|
480
|
+
schema: z.boolean(),
|
|
481
|
+
defaultValue: true,
|
|
482
|
+
writeRoles: ADMIN_ONLY,
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
key: 'discussion.moderationRequired',
|
|
486
|
+
group: 'discussion',
|
|
487
|
+
order: 1,
|
|
488
|
+
uiType: 'boolean',
|
|
489
|
+
scope: 'site',
|
|
490
|
+
schema: z.boolean(),
|
|
491
|
+
// On by default: a comment nobody moderated appearing on a live page is
|
|
492
|
+
// the surprise a new site owner least wants (fiche 15's own comparison
|
|
493
|
+
// to WordPress, which ships the same default).
|
|
494
|
+
defaultValue: true,
|
|
495
|
+
writeRoles: ADMIN_ONLY,
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
key: 'discussion.allowAnonymous',
|
|
499
|
+
group: 'discussion',
|
|
500
|
+
order: 2,
|
|
501
|
+
uiType: 'boolean',
|
|
502
|
+
scope: 'site',
|
|
503
|
+
schema: z.boolean(),
|
|
504
|
+
defaultValue: true,
|
|
505
|
+
writeRoles: ADMIN_ONLY,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
key: 'discussion.autoCloseDays',
|
|
509
|
+
group: 'discussion',
|
|
510
|
+
order: 3,
|
|
511
|
+
uiType: 'number',
|
|
512
|
+
scope: 'site',
|
|
513
|
+
// 0 means "never auto-close" — not every site wants an expiry, and 0 is
|
|
514
|
+
// a clearer way to say that than a second boolean that could disagree
|
|
515
|
+
// with this number.
|
|
516
|
+
schema: z.number().int().min(0).max(3650),
|
|
517
|
+
defaultValue: 0,
|
|
518
|
+
writeRoles: ADMIN_ONLY,
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
key: 'discussion.maxNestingDepth',
|
|
522
|
+
group: 'discussion',
|
|
523
|
+
order: 4,
|
|
524
|
+
uiType: 'number',
|
|
525
|
+
scope: 'site',
|
|
526
|
+
schema: z.number().int().min(1).max(10),
|
|
527
|
+
defaultValue: 5,
|
|
528
|
+
writeRoles: ADMIN_ONLY,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
key: 'discussion.notifyEmail',
|
|
532
|
+
group: 'discussion',
|
|
533
|
+
order: 5,
|
|
534
|
+
uiType: 'email',
|
|
535
|
+
scope: 'site',
|
|
536
|
+
schema: emailOrEmpty,
|
|
537
|
+
defaultValue: '',
|
|
538
|
+
writeRoles: ADMIN_ONLY,
|
|
539
|
+
},
|
|
540
|
+
// Media
|
|
541
|
+
{
|
|
542
|
+
key: 'media.maxUploadSizeMb',
|
|
543
|
+
group: 'media',
|
|
544
|
+
order: 0,
|
|
545
|
+
uiType: 'number',
|
|
546
|
+
scope: 'site',
|
|
547
|
+
schema: z.number().int().min(1).max(200),
|
|
548
|
+
defaultValue: 15,
|
|
549
|
+
writeRoles: ADMIN_ONLY,
|
|
550
|
+
},
|
|
551
|
+
// Privacy
|
|
552
|
+
{
|
|
553
|
+
key: 'privacy.policyPath',
|
|
554
|
+
group: 'privacy',
|
|
555
|
+
order: 0,
|
|
556
|
+
uiType: 'path',
|
|
557
|
+
scope: 'site',
|
|
558
|
+
schema: pathOrEmpty,
|
|
559
|
+
defaultValue: '',
|
|
560
|
+
writeRoles: ADMIN_ONLY,
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
key: 'privacy.cookieBannerEnabled',
|
|
564
|
+
group: 'privacy',
|
|
565
|
+
order: 1,
|
|
566
|
+
uiType: 'boolean',
|
|
567
|
+
scope: 'site',
|
|
568
|
+
schema: z.boolean(),
|
|
569
|
+
// Off by default and on purpose (fiche 23 § pièges): Cogenta sets no
|
|
570
|
+
// cookie of its own, so a banner nobody asked for would be pure noise —
|
|
571
|
+
// this exists for the site that adds a tracker of its own, not to
|
|
572
|
+
// impose a fixture on every site that does not.
|
|
573
|
+
defaultValue: false,
|
|
574
|
+
writeRoles: ADMIN_ONLY,
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
key: 'privacy.cookieBannerMessage',
|
|
578
|
+
group: 'privacy',
|
|
579
|
+
order: 2,
|
|
580
|
+
uiType: 'text',
|
|
581
|
+
scope: 'site',
|
|
582
|
+
schema: freeText,
|
|
583
|
+
defaultValue: '',
|
|
584
|
+
writeRoles: ADMIN_ONLY,
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
key: 'privacy.dataRetentionNote',
|
|
588
|
+
group: 'privacy',
|
|
589
|
+
order: 3,
|
|
590
|
+
uiType: 'text',
|
|
591
|
+
scope: 'site',
|
|
592
|
+
schema: freeText,
|
|
593
|
+
defaultValue: '',
|
|
594
|
+
writeRoles: ADMIN_ONLY,
|
|
595
|
+
},
|
|
596
|
+
// Commerce (fiche 34 task 4) — general store settings. `commerce.currency`
|
|
597
|
+
// and `commerce.priceDisplay` feed the checkout's own totals engine
|
|
598
|
+
// (@cogenta/commerce) as the *default* when an order does not say
|
|
599
|
+
// otherwise; they never duplicate a value the totals engine itself owns.
|
|
600
|
+
{
|
|
601
|
+
key: 'commerce.currency',
|
|
602
|
+
group: 'commerce',
|
|
603
|
+
order: 0,
|
|
604
|
+
uiType: 'string',
|
|
605
|
+
scope: 'site',
|
|
606
|
+
schema: z
|
|
607
|
+
.string()
|
|
608
|
+
.regex(/^[A-Z]{3}$/u, { error: 'A currency is a three-letter ISO 4217 code, e.g. "EUR".' }),
|
|
609
|
+
defaultValue: 'EUR',
|
|
610
|
+
writeRoles: ADMIN_ONLY,
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
key: 'commerce.priceDisplay',
|
|
614
|
+
group: 'commerce',
|
|
615
|
+
order: 1,
|
|
616
|
+
uiType: 'select',
|
|
617
|
+
scope: 'site',
|
|
618
|
+
schema: z.enum(['ttc', 'ht']),
|
|
619
|
+
// Tax-inclusive by default — the European convention this codebase's own
|
|
620
|
+
// tax engine already documents (`TaxRule.includedInPrice`).
|
|
621
|
+
defaultValue: 'ttc',
|
|
622
|
+
options: [
|
|
623
|
+
{ value: 'ttc', label: 'Tax-inclusive (TTC)' },
|
|
624
|
+
{ value: 'ht', label: 'Tax-exclusive (HT)' },
|
|
625
|
+
],
|
|
626
|
+
writeRoles: ADMIN_ONLY,
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
key: 'commerce.countriesServed',
|
|
630
|
+
group: 'commerce',
|
|
631
|
+
order: 2,
|
|
632
|
+
uiType: 'text',
|
|
633
|
+
scope: 'site',
|
|
634
|
+
// Comma-separated ISO 3166-1 alpha-2 codes. Empty means "every country" —
|
|
635
|
+
// the same "unset means no restriction" convention `path` settings use.
|
|
636
|
+
schema: z
|
|
637
|
+
.string()
|
|
638
|
+
.max(2000)
|
|
639
|
+
.refine((value) => value
|
|
640
|
+
.split(',')
|
|
641
|
+
.map((code) => code.trim())
|
|
642
|
+
.filter((code) => code !== '')
|
|
643
|
+
.every((code) => /^[A-Z]{2}$/u.test(code)), {
|
|
644
|
+
error: 'Each country must be a two-letter ISO 3166-1 code, comma-separated (e.g. "FR, BE, LU").',
|
|
645
|
+
}),
|
|
646
|
+
defaultValue: '',
|
|
647
|
+
writeRoles: ADMIN_ONLY,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
key: 'commerce.minOrderSubtotalMinor',
|
|
651
|
+
group: 'commerce',
|
|
652
|
+
order: 3,
|
|
653
|
+
uiType: 'number',
|
|
654
|
+
scope: 'site',
|
|
655
|
+
// Minor units, like every other amount in @cogenta/commerce (ADR-0006) —
|
|
656
|
+
// 0 means no minimum.
|
|
657
|
+
schema: z.number().int().nonnegative().max(1_000_000_000),
|
|
658
|
+
defaultValue: 0,
|
|
659
|
+
writeRoles: ADMIN_ONLY,
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
key: 'commerce.allowBackorderDefault',
|
|
663
|
+
group: 'commerce',
|
|
664
|
+
order: 4,
|
|
665
|
+
uiType: 'boolean',
|
|
666
|
+
scope: 'site',
|
|
667
|
+
schema: z.boolean(),
|
|
668
|
+
defaultValue: false,
|
|
669
|
+
writeRoles: ADMIN_ONLY,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
key: 'commerce.tosPagePath',
|
|
673
|
+
group: 'commerce',
|
|
674
|
+
order: 5,
|
|
675
|
+
uiType: 'path',
|
|
676
|
+
scope: 'site',
|
|
677
|
+
schema: pathOrEmpty,
|
|
678
|
+
defaultValue: '',
|
|
679
|
+
writeRoles: ADMIN_ONLY,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
key: 'commerce.returnPolicyPagePath',
|
|
683
|
+
group: 'commerce',
|
|
684
|
+
order: 6,
|
|
685
|
+
uiType: 'path',
|
|
686
|
+
scope: 'site',
|
|
687
|
+
schema: pathOrEmpty,
|
|
688
|
+
defaultValue: '',
|
|
689
|
+
writeRoles: ADMIN_ONLY,
|
|
690
|
+
},
|
|
691
|
+
// Commerce — invoice template (fiche 34 task 5). `billing` (legal name,
|
|
692
|
+
// address, tax id, footer) stays in `cogenta.config.mjs` — ADR-0024's
|
|
693
|
+
// seller identity, and infrastructure per ADR-0025's classification. These
|
|
694
|
+
// four are editorial on top of it: how numbering and wording read, which a
|
|
695
|
+
// shop owner changes without redeploying.
|
|
696
|
+
{
|
|
697
|
+
key: 'commerce.invoiceSeriesPrefix',
|
|
698
|
+
group: 'commerce',
|
|
699
|
+
order: 7,
|
|
700
|
+
uiType: 'string',
|
|
701
|
+
scope: 'site',
|
|
702
|
+
// Prepended to the year-based series (`formatInvoiceNumber`), e.g.
|
|
703
|
+
// "AC" + "2026" => series "AC2026". Empty keeps today's plain year series.
|
|
704
|
+
schema: z
|
|
705
|
+
.string()
|
|
706
|
+
.max(20)
|
|
707
|
+
.regex(/^[A-Za-z0-9-]*$/u, {
|
|
708
|
+
error: 'A series prefix may only contain letters, digits and hyphens.',
|
|
709
|
+
}),
|
|
710
|
+
defaultValue: '',
|
|
711
|
+
writeRoles: ADMIN_ONLY,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
key: 'commerce.invoicePaymentTerms',
|
|
715
|
+
group: 'commerce',
|
|
716
|
+
order: 8,
|
|
717
|
+
uiType: 'text',
|
|
718
|
+
scope: 'site',
|
|
719
|
+
schema: freeText,
|
|
720
|
+
defaultValue: '',
|
|
721
|
+
writeRoles: ADMIN_ONLY,
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
key: 'commerce.invoiceLanguage',
|
|
725
|
+
group: 'commerce',
|
|
726
|
+
order: 9,
|
|
727
|
+
uiType: 'string',
|
|
728
|
+
scope: 'site',
|
|
729
|
+
schema: z.string().max(10),
|
|
730
|
+
defaultValue: 'en',
|
|
731
|
+
writeRoles: ADMIN_ONLY,
|
|
732
|
+
},
|
|
733
|
+
// Branding (fiche L21 task 8) — Cogenta's own credit, and its white-label
|
|
734
|
+
// override. Read publicly (GET /api/settings is unauthenticated, exactly
|
|
735
|
+
// like every other setting here): the public footer needs both values on
|
|
736
|
+
// every anonymous request, the same way it already reads `general.title`.
|
|
737
|
+
{
|
|
738
|
+
key: 'branding.showCogentaBranding',
|
|
739
|
+
group: 'branding',
|
|
740
|
+
order: 0,
|
|
741
|
+
uiType: 'boolean',
|
|
742
|
+
scope: 'site',
|
|
743
|
+
schema: z.boolean(),
|
|
744
|
+
// On by default: an install says "built with Cogenta" until someone
|
|
745
|
+
// deliberately turns it off, never the other way round.
|
|
746
|
+
defaultValue: true,
|
|
747
|
+
writeRoles: ADMIN_ONLY,
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
key: 'branding.customLogoMediaId',
|
|
751
|
+
group: 'branding',
|
|
752
|
+
order: 1,
|
|
753
|
+
uiType: 'media',
|
|
754
|
+
scope: 'site',
|
|
755
|
+
// A media id, or '' for "unset" — only meaningful once
|
|
756
|
+
// `showCogentaBranding` is off; kept independent of it so turning
|
|
757
|
+
// Cogenta's credit back on never throws away a logo already uploaded.
|
|
758
|
+
schema: z.string().max(200),
|
|
759
|
+
defaultValue: '',
|
|
760
|
+
writeRoles: ADMIN_ONLY,
|
|
761
|
+
},
|
|
762
|
+
// SEO (fiche 21 task 3) — overrides `seo.tsx`'s previous "read-only by
|
|
763
|
+
// design" scope choice (never an ADR): title/description templates,
|
|
764
|
+
// per-collection sitemap hints and social-card defaults, all live-read by
|
|
765
|
+
// `@cogenta/cli`'s render path and by `@cogenta/api`'s `SeoRouter`
|
|
766
|
+
// (`titleDefaults`), never cached at server startup.
|
|
767
|
+
{
|
|
768
|
+
key: 'seo.titleTemplate',
|
|
769
|
+
group: 'seo',
|
|
770
|
+
order: 0,
|
|
771
|
+
uiType: 'string',
|
|
772
|
+
scope: 'site',
|
|
773
|
+
// `%title% — %site%`-style, applied to every page with no per-collection
|
|
774
|
+
// override below and no `seoTitle` field value of its own.
|
|
775
|
+
schema: titleTemplateOrEmpty,
|
|
776
|
+
defaultValue: '',
|
|
777
|
+
writeRoles: ADMIN_ONLY,
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
key: 'seo.collectionTitleTemplates',
|
|
781
|
+
group: 'seo',
|
|
782
|
+
order: 1,
|
|
783
|
+
// Bypasses the generic per-`uiType` renderer (`SiteSettingsField`) —
|
|
784
|
+
// `SeoRoute` renders this one key with a bespoke per-collection table
|
|
785
|
+
// instead, the same way `ReadingTab` special-cases `notFoundPath`. `text`
|
|
786
|
+
// is the closest shape in the closed `uiType` set; nothing reads it for
|
|
787
|
+
// this key.
|
|
788
|
+
uiType: 'text',
|
|
789
|
+
scope: 'site',
|
|
790
|
+
schema: collectionTitleTemplates,
|
|
791
|
+
defaultValue: {},
|
|
792
|
+
writeRoles: ADMIN_ONLY,
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
key: 'seo.defaultMetaDescription',
|
|
796
|
+
group: 'seo',
|
|
797
|
+
order: 2,
|
|
798
|
+
uiType: 'text',
|
|
799
|
+
scope: 'site',
|
|
800
|
+
// Fed into `@cogenta/seo`'s `SeoSite.description`, the fallback every
|
|
801
|
+
// page's own `excerpt`/`seoDescription` already takes priority over.
|
|
802
|
+
schema: metaDescriptionOrEmpty,
|
|
803
|
+
defaultValue: '',
|
|
804
|
+
writeRoles: ADMIN_ONLY,
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
key: 'seo.sitemapCollectionSettings',
|
|
808
|
+
group: 'seo',
|
|
809
|
+
order: 3,
|
|
810
|
+
// Bypassed the same way `collectionTitleTemplates` is — see that entry's
|
|
811
|
+
// own comment.
|
|
812
|
+
uiType: 'text',
|
|
813
|
+
scope: 'site',
|
|
814
|
+
schema: sitemapCollectionSettings,
|
|
815
|
+
defaultValue: {},
|
|
816
|
+
writeRoles: ADMIN_ONLY,
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
key: 'seo.twitterHandle',
|
|
820
|
+
group: 'seo',
|
|
821
|
+
order: 4,
|
|
822
|
+
uiType: 'string',
|
|
823
|
+
scope: 'site',
|
|
824
|
+
// Fed into `@cogenta/seo`'s `SeoSite.twitterSite`, rendered as `twitter:site`.
|
|
825
|
+
schema: twitterHandleOrEmpty,
|
|
826
|
+
defaultValue: '',
|
|
827
|
+
writeRoles: ADMIN_ONLY,
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
key: 'seo.defaultSocialImageUrl',
|
|
831
|
+
group: 'seo',
|
|
832
|
+
order: 5,
|
|
833
|
+
uiType: 'path',
|
|
834
|
+
scope: 'site',
|
|
835
|
+
// A plain URL/path setting, deliberately not a media reference: resolving
|
|
836
|
+
// an arbitrary media id at request time would need the render pipeline's
|
|
837
|
+
// media store reachable from a settings read, for one field whose value
|
|
838
|
+
// rarely changes. A site that wants its own asset pastes the same URL
|
|
839
|
+
// `/api/media/{id}/file` or `/_image?id=…` would answer.
|
|
840
|
+
schema: urlOrPathOrEmpty,
|
|
841
|
+
defaultValue: '',
|
|
842
|
+
writeRoles: ADMIN_ONLY,
|
|
843
|
+
},
|
|
844
|
+
// Fiche 50 tasks 2-5 — search-engine verification, a hand-written
|
|
845
|
+
// robots.txt addendum, and the two off-by-default indexing extras
|
|
846
|
+
// (`indexnow.ts`/`llms-txt.ts`) that were written and unit-tested back in
|
|
847
|
+
// L3/L9 but never wired to a route or a setting until now.
|
|
848
|
+
{
|
|
849
|
+
key: 'seo.googleSiteVerification',
|
|
850
|
+
group: 'seo',
|
|
851
|
+
order: 6,
|
|
852
|
+
uiType: 'string',
|
|
853
|
+
scope: 'site',
|
|
854
|
+
// Rendered verbatim into `<meta name="google-site-verification" content="…">`
|
|
855
|
+
// — no OAuth, no Search Console API call (R1/R7): this is the same
|
|
856
|
+
// "paste the token Google's own verification page shows you" flow every
|
|
857
|
+
// other CMS offers next to domain/DNS verification.
|
|
858
|
+
schema: siteVerificationTokenOrEmpty,
|
|
859
|
+
defaultValue: '',
|
|
860
|
+
writeRoles: ADMIN_ONLY,
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
key: 'seo.bingSiteVerification',
|
|
864
|
+
group: 'seo',
|
|
865
|
+
order: 7,
|
|
866
|
+
uiType: 'string',
|
|
867
|
+
scope: 'site',
|
|
868
|
+
// Rendered into `<meta name="msvalidate.01" content="…">` — Bing
|
|
869
|
+
// Webmaster Tools' own meta-tag verification method, same reasoning as
|
|
870
|
+
// `googleSiteVerification` above.
|
|
871
|
+
schema: siteVerificationTokenOrEmpty,
|
|
872
|
+
defaultValue: '',
|
|
873
|
+
writeRoles: ADMIN_ONLY,
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
key: 'seo.robotsCustomRules',
|
|
877
|
+
group: 'seo',
|
|
878
|
+
order: 8,
|
|
879
|
+
// Bypassed the same way `collectionTitleTemplates` is — `SeoRoute`'s
|
|
880
|
+
// Diagnostics tab renders this one key with a bespoke textarea that
|
|
881
|
+
// confirms before saving a rule that would block every crawler
|
|
882
|
+
// (`Disallow: /`), a piège the fiche names explicitly.
|
|
883
|
+
uiType: 'text',
|
|
884
|
+
scope: 'site',
|
|
885
|
+
// Free-form robots.txt lines, merged after the derived group and before
|
|
886
|
+
// the `Sitemap:` directive (`renderRobotsTxt`'s own `customRules`
|
|
887
|
+
// option). Not `assertSingleLine`-checked like a single directive value:
|
|
888
|
+
// this field is deliberately multi-line.
|
|
889
|
+
schema: freeText,
|
|
890
|
+
defaultValue: '',
|
|
891
|
+
writeRoles: ADMIN_ONLY,
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
key: 'seo.indexNowEnabled',
|
|
895
|
+
group: 'seo',
|
|
896
|
+
order: 9,
|
|
897
|
+
uiType: 'boolean',
|
|
898
|
+
scope: 'site',
|
|
899
|
+
schema: z.boolean(),
|
|
900
|
+
// Off by default: pinging a third-party endpoint on every publish is
|
|
901
|
+
// real, deliberate behaviour an admin opts into, the same reasoning
|
|
902
|
+
// `updates.autoUpdatePolicy` gives for defaulting off.
|
|
903
|
+
defaultValue: false,
|
|
904
|
+
writeRoles: ADMIN_ONLY,
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
key: 'seo.indexNowKey',
|
|
908
|
+
group: 'seo',
|
|
909
|
+
order: 10,
|
|
910
|
+
uiType: 'string',
|
|
911
|
+
scope: 'site',
|
|
912
|
+
schema: indexNowKeyOrEmpty,
|
|
913
|
+
defaultValue: '',
|
|
914
|
+
writeRoles: ADMIN_ONLY,
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
key: 'seo.llmsTxtEnabled',
|
|
918
|
+
group: 'seo',
|
|
919
|
+
order: 11,
|
|
920
|
+
uiType: 'boolean',
|
|
921
|
+
scope: 'site',
|
|
922
|
+
schema: z.boolean(),
|
|
923
|
+
// Off by default, same reasoning as `indexNowEnabled`: `llms.txt` lists
|
|
924
|
+
// every published title site-wide, which an admin should choose to
|
|
925
|
+
// expose rather than find already on by an install nobody configured.
|
|
926
|
+
defaultValue: false,
|
|
927
|
+
writeRoles: ADMIN_ONLY,
|
|
928
|
+
},
|
|
929
|
+
// Fiche 70 task 3 — the feature-activation grid's own four gates. Every
|
|
930
|
+
// one of these defaults to `true`: unlike `indexNowEnabled`/`llmsTxtEnabled`
|
|
931
|
+
// (which switch on a real effect an install nobody configured should not
|
|
932
|
+
// get for free), these four gate a feature that is *already* opt-in by
|
|
933
|
+
// construction — a score/link report nobody asked to see is simply not
|
|
934
|
+
// rendered, and a verification token/custom robots rule nobody typed has
|
|
935
|
+
// no content to gate in the first place. Defaulting to `true` is what
|
|
936
|
+
// keeps an upgrading site's behaviour byte-identical the day this fiche
|
|
937
|
+
// ships: every admin who had already typed a verification token or a
|
|
938
|
+
// custom robots.txt rule keeps seeing it applied, with no silent regression.
|
|
939
|
+
{
|
|
940
|
+
key: 'seo.contentScoreEnabled',
|
|
941
|
+
group: 'seo',
|
|
942
|
+
order: 12,
|
|
943
|
+
uiType: 'boolean',
|
|
944
|
+
scope: 'site',
|
|
945
|
+
// Read only by the admin (`content-analysis.ts`'s `analyseContent` runs
|
|
946
|
+
// entirely client-side, R2/R9) — this setting merely decides whether the
|
|
947
|
+
// SEO panel renders the score at all, never a server-side gate.
|
|
948
|
+
schema: z.boolean(),
|
|
949
|
+
defaultValue: true,
|
|
950
|
+
writeRoles: ADMIN_ONLY,
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
key: 'seo.linkAssistantEnabled',
|
|
954
|
+
group: 'seo',
|
|
955
|
+
order: 13,
|
|
956
|
+
uiType: 'boolean',
|
|
957
|
+
scope: 'site',
|
|
958
|
+
// Gates `GET /api/seo/link-suggestions` itself (`seo-router.ts`) —
|
|
959
|
+
// unlike `contentScoreEnabled`, this one *does* reach the server, since
|
|
960
|
+
// the underlying scan reads every routed collection's published
|
|
961
|
+
// entries and is not free to run.
|
|
962
|
+
schema: z.boolean(),
|
|
963
|
+
defaultValue: true,
|
|
964
|
+
writeRoles: ADMIN_ONLY,
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
key: 'seo.searchVerificationEnabled',
|
|
968
|
+
group: 'seo',
|
|
969
|
+
order: 14,
|
|
970
|
+
uiType: 'boolean',
|
|
971
|
+
scope: 'site',
|
|
972
|
+
// Gates whether `seo.googleSiteVerification`/`seo.bingSiteVerification`
|
|
973
|
+
// are actually rendered into `<meta>` tags (`readSeoRenderDefaults`,
|
|
974
|
+
// `@cogenta/cli`) — the saved tokens are never erased by turning this
|
|
975
|
+
// off, exactly the same "data persists, activation gates whether it
|
|
976
|
+
// applies" shape `robotsCustomRulesEnabled` below uses.
|
|
977
|
+
schema: z.boolean(),
|
|
978
|
+
defaultValue: true,
|
|
979
|
+
writeRoles: ADMIN_ONLY,
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
key: 'seo.robotsCustomRulesEnabled',
|
|
983
|
+
group: 'seo',
|
|
984
|
+
order: 15,
|
|
985
|
+
uiType: 'boolean',
|
|
986
|
+
scope: 'site',
|
|
987
|
+
// Gates whether `seo.robotsCustomRules` is actually merged into the
|
|
988
|
+
// served `robots.txt` (`readSeoRenderDefaults`) — the saved text stays
|
|
989
|
+
// in the textarea either way; only whether a crawler ever sees it changes.
|
|
990
|
+
schema: z.boolean(),
|
|
991
|
+
defaultValue: true,
|
|
992
|
+
writeRoles: ADMIN_ONLY,
|
|
993
|
+
},
|
|
994
|
+
// Observability (fiche L22 task 5) — whether the local trace/log
|
|
995
|
+
// collection behind the admin's "Exploitation" screen runs, and how
|
|
996
|
+
// verbose it is. The OTLP export destination itself is not here (see the
|
|
997
|
+
// `observability` group's own doc comment) — this registry only ever
|
|
998
|
+
// holds editorial state, never infra wiring that can carry a secret.
|
|
999
|
+
{
|
|
1000
|
+
key: 'observability.enabled',
|
|
1001
|
+
group: 'observability',
|
|
1002
|
+
order: 0,
|
|
1003
|
+
uiType: 'boolean',
|
|
1004
|
+
scope: 'site',
|
|
1005
|
+
schema: z.boolean(),
|
|
1006
|
+
// On by default — fiche L22 task 5's own "actif par défaut pour la
|
|
1007
|
+
// collecte et l'affichage local (R1)": a fresh site shows something on
|
|
1008
|
+
// its Exploitation screen without anyone having to opt in first.
|
|
1009
|
+
defaultValue: true,
|
|
1010
|
+
writeRoles: ADMIN_ONLY,
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
key: 'observability.logLevel',
|
|
1014
|
+
group: 'observability',
|
|
1015
|
+
order: 1,
|
|
1016
|
+
uiType: 'select',
|
|
1017
|
+
scope: 'site',
|
|
1018
|
+
schema: z.enum(['error', 'warn', 'info', 'debug']),
|
|
1019
|
+
defaultValue: 'info',
|
|
1020
|
+
options: [
|
|
1021
|
+
{ value: 'error', label: 'Error' },
|
|
1022
|
+
{ value: 'warn', label: 'Warn' },
|
|
1023
|
+
{ value: 'info', label: 'Info' },
|
|
1024
|
+
{ value: 'debug', label: 'Debug' },
|
|
1025
|
+
],
|
|
1026
|
+
writeRoles: ADMIN_ONLY,
|
|
1027
|
+
},
|
|
1028
|
+
// Assistant (L22 task 4) — which collections the writing assistant's
|
|
1029
|
+
// vector index actually contains. Read live by `cogenta serve` on every
|
|
1030
|
+
// content write and by the "Reindex vectors" tool (`@cogenta/cli`'s
|
|
1031
|
+
// `isAssistantCollectionEnabled`) — never cached, so a toggle here takes
|
|
1032
|
+
// effect on the next save with no restart.
|
|
1033
|
+
{
|
|
1034
|
+
key: 'assistant.indexedCollections',
|
|
1035
|
+
group: 'assistant',
|
|
1036
|
+
order: 0,
|
|
1037
|
+
// Bypasses the generic per-`uiType` renderer, the same way
|
|
1038
|
+
// `seo.sitemapCollectionSettings` does — the redesigned assistant
|
|
1039
|
+
// screen renders this one key as a per-collection toggle list instead.
|
|
1040
|
+
uiType: 'text',
|
|
1041
|
+
scope: 'site',
|
|
1042
|
+
schema: indexedCollectionsSettings,
|
|
1043
|
+
defaultValue: {},
|
|
1044
|
+
writeRoles: ADMIN_ONLY,
|
|
1045
|
+
},
|
|
1046
|
+
// Update system (L22 task 9) — auto-update policy. A closed choice, never
|
|
1047
|
+
// free text: the scheduled task that reads this only ever compares it
|
|
1048
|
+
// against `UpdateBump` (`@cogenta/cli`'s `policyAllows`), so a typo here
|
|
1049
|
+
// must be impossible, the same reasoning `commerce.priceDisplay` already
|
|
1050
|
+
// gave for using `select` over a free string.
|
|
1051
|
+
{
|
|
1052
|
+
key: 'updates.autoUpdatePolicy',
|
|
1053
|
+
group: 'updates',
|
|
1054
|
+
order: 0,
|
|
1055
|
+
uiType: 'select',
|
|
1056
|
+
scope: 'site',
|
|
1057
|
+
schema: z.enum(['off', 'patch', 'patch-minor', 'patch-minor-major']),
|
|
1058
|
+
// Off by default: an unattended `npm install` against a running site is
|
|
1059
|
+
// real, deliberate behaviour that has to be opted into — never a
|
|
1060
|
+
// surprise sprung on whoever's default install this is (same reasoning
|
|
1061
|
+
// `privacy.cookieBannerEnabled` gives for defaulting off).
|
|
1062
|
+
defaultValue: 'off',
|
|
1063
|
+
options: [
|
|
1064
|
+
{ value: 'off', label: 'Off — check only, never apply automatically' },
|
|
1065
|
+
{ value: 'patch', label: 'Patch releases only' },
|
|
1066
|
+
{ value: 'patch-minor', label: 'Patch and minor releases' },
|
|
1067
|
+
{ value: 'patch-minor-major', label: 'Patch, minor and major releases' },
|
|
1068
|
+
],
|
|
1069
|
+
writeRoles: ADMIN_ONLY,
|
|
1070
|
+
},
|
|
1071
|
+
// Navigation (fiche 22 tâche 8, part 3) — admin sidebar reordering and
|
|
1072
|
+
// hiding, site-wide. Four independent lists rather than one nested
|
|
1073
|
+
// structure: each is a plain `navTokenList`, the same "comma-separated
|
|
1074
|
+
// subset of a vocabulary this package cannot import" shape every other
|
|
1075
|
+
// string-shaped setting here already uses, so this registry stays generic
|
|
1076
|
+
// (no key here gets a bespoke value shape the store has to special-case).
|
|
1077
|
+
{
|
|
1078
|
+
key: 'navigation.sectionOrder',
|
|
1079
|
+
group: 'navigation',
|
|
1080
|
+
order: 0,
|
|
1081
|
+
uiType: 'string',
|
|
1082
|
+
scope: 'site',
|
|
1083
|
+
schema: navTokenList,
|
|
1084
|
+
defaultValue: '',
|
|
1085
|
+
writeRoles: ADMIN_ONLY,
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
key: 'navigation.hiddenSections',
|
|
1089
|
+
group: 'navigation',
|
|
1090
|
+
order: 1,
|
|
1091
|
+
uiType: 'string',
|
|
1092
|
+
scope: 'site',
|
|
1093
|
+
schema: navTokenList,
|
|
1094
|
+
defaultValue: '',
|
|
1095
|
+
writeRoles: ADMIN_ONLY,
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
key: 'navigation.itemOrder',
|
|
1099
|
+
group: 'navigation',
|
|
1100
|
+
order: 2,
|
|
1101
|
+
uiType: 'string',
|
|
1102
|
+
scope: 'site',
|
|
1103
|
+
schema: navTokenList,
|
|
1104
|
+
defaultValue: '',
|
|
1105
|
+
writeRoles: ADMIN_ONLY,
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
key: 'navigation.hiddenItems',
|
|
1109
|
+
group: 'navigation',
|
|
1110
|
+
order: 3,
|
|
1111
|
+
uiType: 'string',
|
|
1112
|
+
scope: 'site',
|
|
1113
|
+
schema: navTokenList,
|
|
1114
|
+
defaultValue: '',
|
|
1115
|
+
writeRoles: ADMIN_ONLY,
|
|
1116
|
+
},
|
|
1117
|
+
// Channels (fiche 59) — the operator-entered, non-secret bot handle each
|
|
1118
|
+
// channel's in-admin step-by-step guide names, so a person linking their
|
|
1119
|
+
// account reads "open a chat with @my_cogenta_bot" instead of a generic
|
|
1120
|
+
// placeholder. Free text on purpose (`botNameOrEmpty`, not a `path`/`email`
|
|
1121
|
+
// shape): a Telegram handle, a Slack app name and a Discord tag
|
|
1122
|
+
// (`MyBot#1234`) have three different formats, and this field never
|
|
1123
|
+
// validates against any of them — the real bot credential (the token) is
|
|
1124
|
+
// environment-only (R7) and has no row anywhere in this table.
|
|
1125
|
+
{
|
|
1126
|
+
key: 'channels.telegramBotName',
|
|
1127
|
+
group: 'channels',
|
|
1128
|
+
order: 0,
|
|
1129
|
+
uiType: 'string',
|
|
1130
|
+
scope: 'site',
|
|
1131
|
+
schema: botNameOrEmpty,
|
|
1132
|
+
defaultValue: '',
|
|
1133
|
+
writeRoles: ADMIN_ONLY,
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
key: 'channels.slackBotName',
|
|
1137
|
+
group: 'channels',
|
|
1138
|
+
order: 1,
|
|
1139
|
+
uiType: 'string',
|
|
1140
|
+
scope: 'site',
|
|
1141
|
+
schema: botNameOrEmpty,
|
|
1142
|
+
defaultValue: '',
|
|
1143
|
+
writeRoles: ADMIN_ONLY,
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
key: 'channels.discordBotName',
|
|
1147
|
+
group: 'channels',
|
|
1148
|
+
order: 2,
|
|
1149
|
+
uiType: 'string',
|
|
1150
|
+
scope: 'site',
|
|
1151
|
+
schema: botNameOrEmpty,
|
|
1152
|
+
defaultValue: '',
|
|
1153
|
+
writeRoles: ADMIN_ONLY,
|
|
1154
|
+
},
|
|
1155
|
+
];
|
|
1156
|
+
const BY_KEY = new Map(SITE_SETTINGS_REGISTRY.map((entry) => [entry.key, entry]));
|
|
1157
|
+
export function siteSettingByKey(key) {
|
|
1158
|
+
return BY_KEY.get(key);
|
|
1159
|
+
}
|
|
1160
|
+
//# sourceMappingURL=site-settings-registry.js.map
|