@endora-commerce/mod-settings 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -0
  3. package/dist/admin/api/settings-client.d.ts +47 -0
  4. package/dist/admin/api/settings-client.d.ts.map +1 -0
  5. package/dist/admin/api/settings-client.js +41 -0
  6. package/dist/admin/api/settings-client.js.map +1 -0
  7. package/dist/admin/components/ActivationPointerRow.d.ts +21 -0
  8. package/dist/admin/components/ActivationPointerRow.d.ts.map +1 -0
  9. package/dist/admin/components/ActivationPointerRow.js +28 -0
  10. package/dist/admin/components/ActivationPointerRow.js.map +1 -0
  11. package/dist/admin/components/AssetIdSettingInput.d.ts +12 -0
  12. package/dist/admin/components/AssetIdSettingInput.d.ts.map +1 -0
  13. package/dist/admin/components/AssetIdSettingInput.js +10 -0
  14. package/dist/admin/components/AssetIdSettingInput.js.map +1 -0
  15. package/dist/admin/components/ConfigurationReferenceInput.d.ts +39 -0
  16. package/dist/admin/components/ConfigurationReferenceInput.d.ts.map +1 -0
  17. package/dist/admin/components/ConfigurationReferenceInput.js +74 -0
  18. package/dist/admin/components/ConfigurationReferenceInput.js.map +1 -0
  19. package/dist/admin/components/ConflictBanner.d.ts +13 -0
  20. package/dist/admin/components/ConflictBanner.d.ts.map +1 -0
  21. package/dist/admin/components/ConflictBanner.js +12 -0
  22. package/dist/admin/components/ConflictBanner.js.map +1 -0
  23. package/dist/admin/components/ImageSettingInput.d.ts +34 -0
  24. package/dist/admin/components/ImageSettingInput.d.ts.map +1 -0
  25. package/dist/admin/components/ImageSettingInput.js +36 -0
  26. package/dist/admin/components/ImageSettingInput.js.map +1 -0
  27. package/dist/admin/components/QuoteRequestsSettingsTab.d.ts +3 -0
  28. package/dist/admin/components/QuoteRequestsSettingsTab.d.ts.map +1 -0
  29. package/dist/admin/components/QuoteRequestsSettingsTab.js +69 -0
  30. package/dist/admin/components/QuoteRequestsSettingsTab.js.map +1 -0
  31. package/dist/admin/components/SellerCompanyDataInput.d.ts +22 -0
  32. package/dist/admin/components/SellerCompanyDataInput.d.ts.map +1 -0
  33. package/dist/admin/components/SellerCompanyDataInput.js +53 -0
  34. package/dist/admin/components/SellerCompanyDataInput.js.map +1 -0
  35. package/dist/admin/components/SettingRowEditor.d.ts +55 -0
  36. package/dist/admin/components/SettingRowEditor.d.ts.map +1 -0
  37. package/dist/admin/components/SettingRowEditor.js +160 -0
  38. package/dist/admin/components/SettingRowEditor.js.map +1 -0
  39. package/dist/admin/index.d.ts +39 -0
  40. package/dist/admin/index.d.ts.map +1 -0
  41. package/dist/admin/index.js +87 -0
  42. package/dist/admin/index.js.map +1 -0
  43. package/dist/admin/pages/CachePage.d.ts +14 -0
  44. package/dist/admin/pages/CachePage.d.ts.map +1 -0
  45. package/dist/admin/pages/CachePage.js +124 -0
  46. package/dist/admin/pages/CachePage.js.map +1 -0
  47. package/dist/admin/pages/GroupsPage.d.ts +17 -0
  48. package/dist/admin/pages/GroupsPage.d.ts.map +1 -0
  49. package/dist/admin/pages/GroupsPage.js +84 -0
  50. package/dist/admin/pages/GroupsPage.js.map +1 -0
  51. package/dist/admin/pages/SettingsPage.d.ts +20 -0
  52. package/dist/admin/pages/SettingsPage.d.ts.map +1 -0
  53. package/dist/admin/pages/SettingsPage.js +453 -0
  54. package/dist/admin/pages/SettingsPage.js.map +1 -0
  55. package/dist/backend/cli/cache-clear.d.ts +23 -0
  56. package/dist/backend/cli/cache-clear.d.ts.map +1 -0
  57. package/dist/backend/cli/cache-clear.js +37 -0
  58. package/dist/backend/cli/cache-clear.js.map +1 -0
  59. package/dist/backend/index.d.ts +100 -0
  60. package/dist/backend/index.d.ts.map +1 -0
  61. package/dist/backend/index.js +125 -0
  62. package/dist/backend/index.js.map +1 -0
  63. package/dist/backend/routes.admin.d.ts +308 -0
  64. package/dist/backend/routes.admin.d.ts.map +1 -0
  65. package/dist/backend/routes.admin.js +188 -0
  66. package/dist/backend/routes.admin.js.map +1 -0
  67. package/dist/backend/routes.cache.d.ts +20 -0
  68. package/dist/backend/routes.cache.d.ts.map +1 -0
  69. package/dist/backend/routes.cache.js +22 -0
  70. package/dist/backend/routes.cache.js.map +1 -0
  71. package/dist/backend/routes.homepage.d.ts +15 -0
  72. package/dist/backend/routes.homepage.d.ts.map +1 -0
  73. package/dist/backend/routes.homepage.js +20 -0
  74. package/dist/backend/routes.homepage.js.map +1 -0
  75. package/dist/backend/routes.product-card-buttons.d.ts +15 -0
  76. package/dist/backend/routes.product-card-buttons.d.ts.map +1 -0
  77. package/dist/backend/routes.product-card-buttons.js +21 -0
  78. package/dist/backend/routes.product-card-buttons.js.map +1 -0
  79. package/dist/backend/routes.speculation-rules.d.ts +16 -0
  80. package/dist/backend/routes.speculation-rules.d.ts.map +1 -0
  81. package/dist/backend/routes.speculation-rules.js +21 -0
  82. package/dist/backend/routes.speculation-rules.js.map +1 -0
  83. package/dist/backend/routes.storefront.d.ts +15 -0
  84. package/dist/backend/routes.storefront.d.ts.map +1 -0
  85. package/dist/backend/routes.storefront.js +20 -0
  86. package/dist/backend/routes.storefront.js.map +1 -0
  87. package/dist/backend/services/cache-admin.service.d.ts +83 -0
  88. package/dist/backend/services/cache-admin.service.d.ts.map +1 -0
  89. package/dist/backend/services/cache-admin.service.js +148 -0
  90. package/dist/backend/services/cache-admin.service.js.map +1 -0
  91. package/dist/backend/services/homepage-resolver.d.ts +25 -0
  92. package/dist/backend/services/homepage-resolver.d.ts.map +1 -0
  93. package/dist/backend/services/homepage-resolver.js +31 -0
  94. package/dist/backend/services/homepage-resolver.js.map +1 -0
  95. package/dist/backend/services/product-card-buttons-resolver.d.ts +25 -0
  96. package/dist/backend/services/product-card-buttons-resolver.d.ts.map +1 -0
  97. package/dist/backend/services/product-card-buttons-resolver.js +37 -0
  98. package/dist/backend/services/product-card-buttons-resolver.js.map +1 -0
  99. package/dist/backend/services/registered-settings-manifests.d.ts +45 -0
  100. package/dist/backend/services/registered-settings-manifests.d.ts.map +1 -0
  101. package/dist/backend/services/registered-settings-manifests.js +55 -0
  102. package/dist/backend/services/registered-settings-manifests.js.map +1 -0
  103. package/dist/backend/services/setting-write-validators.d.ts +43 -0
  104. package/dist/backend/services/setting-write-validators.d.ts.map +1 -0
  105. package/dist/backend/services/setting-write-validators.js +43 -0
  106. package/dist/backend/services/setting-write-validators.js.map +1 -0
  107. package/dist/backend/services/settings-admin.service.d.ts +206 -0
  108. package/dist/backend/services/settings-admin.service.d.ts.map +1 -0
  109. package/dist/backend/services/settings-admin.service.js +642 -0
  110. package/dist/backend/services/settings-admin.service.js.map +1 -0
  111. package/dist/backend/services/shop-info-resolver.d.ts +15 -0
  112. package/dist/backend/services/shop-info-resolver.d.ts.map +1 -0
  113. package/dist/backend/services/shop-info-resolver.js +44 -0
  114. package/dist/backend/services/shop-info-resolver.js.map +1 -0
  115. package/dist/backend/services/speculation-rules-resolver.d.ts +28 -0
  116. package/dist/backend/services/speculation-rules-resolver.d.ts.map +1 -0
  117. package/dist/backend/services/speculation-rules-resolver.js +39 -0
  118. package/dist/backend/services/speculation-rules-resolver.js.map +1 -0
  119. package/dist/manifest.d.ts +194 -0
  120. package/dist/manifest.d.ts.map +1 -0
  121. package/dist/manifest.js +379 -0
  122. package/dist/manifest.js.map +1 -0
  123. package/docs/settings/index.md +171 -0
  124. package/i18n/en.json +167 -0
  125. package/i18n/pl.json +167 -0
  126. package/package.json +96 -0
  127. package/tailwind.css +14 -0
@@ -0,0 +1,379 @@
1
+ import { defineModuleManifest, defineModuleSettingsManifest, } from '@endora-commerce/contracts';
2
+ /**
3
+ * Built-in settings manifest for the settings module itself — feature 004.
4
+ *
5
+ * Seeds the platform-wide `general` group on every backend boot so other
6
+ * modules' manifests can default-attach to it without a chicken-and-egg
7
+ * problem. The group is `isSystemProtected: true`, which the admin service
8
+ * refuses to delete.
9
+ */
10
+ const settings = defineModuleSettingsManifest({
11
+ moduleCode: 'settings',
12
+ groups: [
13
+ {
14
+ code: 'general',
15
+ name: 'General',
16
+ isSystemProtected: true,
17
+ // Empty salesChannelCodes ⇒ applies to every sales channel.
18
+ },
19
+ {
20
+ // Shop / company contact information surfaced across the storefront
21
+ // (footer, 404 "need help?" block, contact form recipients, etc.).
22
+ code: 'shop',
23
+ name: 'Shop information',
24
+ // Empty salesChannelCodes ⇒ applies to every sales channel; values can
25
+ // still be overridden per channel via the standard scope mechanism.
26
+ },
27
+ {
28
+ // Storefront-template behaviour toggles (performance hints, etc.).
29
+ code: 'storefront',
30
+ name: 'Storefront',
31
+ },
32
+ ],
33
+ settings: [
34
+ {
35
+ // Image URL shown when a product has no image of its own, on product
36
+ // cards / listings (and the product page). Resolvable globally or
37
+ // per sales channel via the standard settings scope mechanism.
38
+ code: 'product_image_placeholder_url',
39
+ name: 'Product image placeholder',
40
+ description: 'Image displayed on product cards and listings when a product has no ' +
41
+ 'image of its own. Upload a file (drag-and-drop or file picker) or enter ' +
42
+ 'an image URL. Leave empty to show no placeholder. Can be overridden per ' +
43
+ 'sales channel.',
44
+ groupCode: 'general',
45
+ valueType: 'string',
46
+ defaultValue: '',
47
+ },
48
+ {
49
+ // Minutes of inactivity after which an admin is signed out of the Admin
50
+ // UI. Enforced client-side by an idle timer in the admin app.
51
+ code: 'admin.idle_logout_minutes',
52
+ name: 'Admin idle logout (minutes)',
53
+ description: 'Number of minutes of inactivity after which an administrator is ' +
54
+ 'automatically signed out of the Admin UI. Default 60.',
55
+ groupCode: 'general',
56
+ valueType: 'number',
57
+ defaultValue: 60,
58
+ },
59
+ {
60
+ // Slug (URL path) of the CMS page to serve as the storefront home page.
61
+ // Empty ⇒ the storefront falls back to its built-in landing page. Can be
62
+ // overridden per sales channel.
63
+ code: 'homepage_cms_page_slug',
64
+ name: 'Home page CMS page',
65
+ description: 'Slug (URL path) of the CMS page to use as the storefront home page, ' +
66
+ 'e.g. "welcome". Leave empty to use the built-in landing page. Can be ' +
67
+ 'overridden per sales channel.',
68
+ groupCode: 'general',
69
+ valueType: 'string',
70
+ defaultValue: '',
71
+ },
72
+ {
73
+ code: 'shop.name',
74
+ name: 'Shop name',
75
+ description: 'Public name of the shop, shown across the storefront.',
76
+ groupCode: 'shop',
77
+ valueType: 'string',
78
+ defaultValue: '',
79
+ },
80
+ {
81
+ code: 'shop.address',
82
+ name: 'Shop address',
83
+ description: 'Postal address of the shop, shown across the storefront.',
84
+ groupCode: 'shop',
85
+ valueType: 'string',
86
+ defaultValue: '',
87
+ },
88
+ {
89
+ code: 'shop.contact_email',
90
+ name: 'Main contact email',
91
+ description: 'Primary email address used for general contact.',
92
+ groupCode: 'shop',
93
+ valueType: 'string',
94
+ defaultValue: '',
95
+ },
96
+ {
97
+ code: 'shop.support_email',
98
+ name: 'Support / customer service email',
99
+ description: 'Email address of the support / customer service desk. Shown to ' +
100
+ 'customers when they need help (e.g. on the 404 page).',
101
+ groupCode: 'shop',
102
+ valueType: 'string',
103
+ defaultValue: '',
104
+ },
105
+ {
106
+ code: 'shop.phone',
107
+ name: 'Shop phone number',
108
+ description: 'Contact phone number for the shop. May be left empty.',
109
+ groupCode: 'shop',
110
+ valueType: 'string',
111
+ defaultValue: '',
112
+ },
113
+ {
114
+ code: 'shop.contact_form_recipient_emails',
115
+ name: 'Contact form recipient emails',
116
+ description: 'Recipient email address(es) for the storefront contact form. ' +
117
+ 'Multiple addresses can be provided, separated by commas.',
118
+ groupCode: 'shop',
119
+ valueType: 'string',
120
+ defaultValue: '',
121
+ },
122
+ {
123
+ // Speculation Rules (prerender/prefetch) hint emitted by the storefront
124
+ // template for near-instant navigations.
125
+ code: 'storefront.speculation_rules.enabled',
126
+ name: 'Enable Speculation Rules',
127
+ description: 'Emit a Speculation Rules script in the storefront so the browser can ' +
128
+ 'prerender/prefetch likely next pages for near-instant navigation. ' +
129
+ 'Note: this setting only has an effect if the active Storefront UI ' +
130
+ 'theme supports the Speculation Rules mechanism; themes that do not ' +
131
+ 'implement it will ignore the toggle.',
132
+ groupCode: 'storefront',
133
+ valueType: 'boolean',
134
+ defaultValue: true,
135
+ },
136
+ {
137
+ // Eagerness level for the Speculation Rules above. "moderate" prerenders
138
+ // on hover/pointer intent — the recommended balance between instant
139
+ // navigation and resource use; "eager" speculates aggressively on every
140
+ // eligible link, "conservative" only on pointer-down.
141
+ code: 'storefront.speculation_rules.eagerness',
142
+ name: 'Speculation Rules eagerness',
143
+ description: 'Eagerness for the storefront Speculation Rules: "conservative" ' +
144
+ '(on pointer-down), "moderate" (on hover — recommended), or "eager" ' +
145
+ '(as soon as links are discovered). Only applies when Speculation ' +
146
+ 'Rules are enabled and supported by the active Storefront UI theme.',
147
+ groupCode: 'storefront',
148
+ valueType: 'string',
149
+ defaultValue: 'moderate',
150
+ enumOptions: ['conservative', 'moderate', 'eager'],
151
+ },
152
+ {
153
+ // Whether the storefront product card shows an "Add to cart" button.
154
+ code: 'storefront.product_card.show_add_to_cart',
155
+ name: 'Show "Add to cart" on product cards',
156
+ description: 'Toggles the "Add to cart" button on storefront product card listings. ' +
157
+ 'Only has an effect if the active Storefront UI theme renders the button.',
158
+ groupCode: 'general',
159
+ valueType: 'boolean',
160
+ defaultValue: true,
161
+ },
162
+ {
163
+ // Whether the storefront product card shows an "Add to shopping list"
164
+ // button (adds the product to the customer's default shopping list).
165
+ code: 'storefront.product_card.show_add_to_shopping_list',
166
+ name: 'Show "Add to shopping list" on product cards',
167
+ description: 'Toggles the "Add to shopping list" button on storefront product card ' +
168
+ 'listings; clicking it adds the product to the customer\'s default ' +
169
+ 'shopping list. Only has an effect if the active Storefront UI theme ' +
170
+ 'renders the button.',
171
+ groupCode: 'general',
172
+ valueType: 'boolean',
173
+ defaultValue: true,
174
+ },
175
+ ],
176
+ });
177
+ /** Module-lifecycle manifest (feature 018) + i18n bundle declaration (feature 019). */
178
+ export const manifest = defineModuleManifest({
179
+ id: 'settings',
180
+ name: 'Settings',
181
+ description: 'Per-module setting registry, admin UI, and value resolver.',
182
+ version: '1.0.0',
183
+ // Rule 1 (platform root) — specs/065-manifest-aware-migrations/research.md §R9.
184
+ // `setting_values.sales_channel_id`, `setting_sales_channels` and
185
+ // `setting_group_sales_channels` foreign-key `sales_channels`, and the edge is
186
+ // deliberately not declared: settings is a platform root that every other
187
+ // module (including sales_channels itself) installs on top of, and declaring
188
+ // it would cycle settings → sales_channels → settings.
189
+ //
190
+ // Since feature 072 (T018/T019) all four of those tables are kernel-owned, so
191
+ // the edge no longer crosses a module boundary at all and the acknowledged
192
+ // entry in test/unit/db/acknowledged-fk-edges.ts has been removed. The
193
+ // reasoning is kept because it is why this module declares no edge to
194
+ // `sales_channels`.
195
+ //
196
+ // Feature 072 (T118) — `auth` *is* declared: the admin routes are gated by
197
+ // `requireAdmin` and name the acting admin through `adminAuditActorResolver`,
198
+ // both of which `auth` owns. It closes no cycle (`auth` → `admin_roles` → ∅)
199
+ // and shifts no migration order, because T018 moved this module's tables into
200
+ // the kernel and it ships no migrations of its own.
201
+ dependencies: ['auth'],
202
+ settings,
203
+ // Feature 074 (Constitution XVII), test C2 — functional base, and named by
204
+ // ruling 1. The control that used to be here was one of the nineteen that
205
+ // never accepted a deactivation; making the lock explicit replaces an
206
+ // accidental refusal with a declared one and takes the dead button off the
207
+ // screen.
208
+ //
209
+ // The recoverability argument that used to carry the switch — D-36 moved the
210
+ // activation controls onto the kernel-served `/platform/modules`, T118 made
211
+ // the settings reader kernel-composed — is still true and is why switching
212
+ // this off is survivable. It is not why it should be offered. This module is
213
+ // the configuration surface for every other one, and a module that is off
214
+ // has no editable configuration, so `settings` off means nothing on the
215
+ // platform is configurable: a different product, not a smaller one.
216
+ //
217
+ // `settings.enabled` goes with the control. Left declared it would classify
218
+ // as an ordinary editable boolean that changes nothing, which is the
219
+ // present-but-ignored shape Principle XVII prohibits; the reconciler never
220
+ // deletes a row it stops seeing, so the existing rows are removed by a core
221
+ // data migration instead (feature 074, FR-010a).
222
+ activation: {
223
+ nonDeactivatable: true,
224
+ reason: 'The configuration surface for every other module. A module that is off has no editable ' +
225
+ 'configuration, so switching this off would leave nothing on the platform configurable.',
226
+ },
227
+ /**
228
+ * The ten error codes this module owns — feature 090 Phase 3
229
+ * (`specs/090-module-owned-error-codes/contracts/error-code-declaration.md`
230
+ * §1.1). This is where each one's sentence is looked up from: `errors.<CODE>`
231
+ * in this module's own `i18n/{en,pl}.json`.
232
+ *
233
+ * The list is answer-preserving, not a judgement (§6.2 and §6.5), and it was
234
+ * not written by hand: it is the verbatim output of the runbook's step-1
235
+ * derivation over the frozen capture at
236
+ * `backend/test/fixtures/error-code-routing/chain-answers.ts`, which records
237
+ * what the prefix chain in `@endora-commerce/mod-i18n` answered at
238
+ * `49f3c6817`. Re-routing a code to a better owner is
239
+ * `specs/082-error-code-ownership/rulings.md` §9's remaining work and is
240
+ * deliberately not done here.
241
+ *
242
+ * Nine of the ten carry a written sentence in both languages in this
243
+ * package's bundles, and those nine are exactly the `errors.*` keys those
244
+ * bundles hold — so there is no dead sentence in either direction. The tenth,
245
+ * `SETTING_SECRET_KEY_MISSING`, is already a `check-error-translations.ts`
246
+ * `UNTRANSLATED_ERROR_CODES` entry and stays one; declaring it changes
247
+ * nothing about that ledger, which is keyed off the chain.
248
+ *
249
+ * **No shadow reaches this module, and it is the one module that could have
250
+ * cast one.** Trap T1 is about the chain being an ordered `if`, and
251
+ * `SETTING_` is its *first* branch — ahead even of the generic set — so
252
+ * nothing above it can claim a `SETTING_`-prefixed code and every such member
253
+ * of `ERROR_CODES` lands here. Reading the rule's source and reading the
254
+ * chain's answer coincide, which for a migrating author is a conclusion of
255
+ * reading the whole chain and never a premise: the list below still comes off
256
+ * the answer. Nor does this branch take anything from a later one — no
257
+ * `SETTING_`-prefixed code belongs anywhere else under any reading.
258
+ *
259
+ * **Two codes here read like another module's, and both stay** (trap T2).
260
+ * `SETTING_OUT_OF_SCOPE_FOR_CHANNEL` names a sales channel and !1125 recorded
261
+ * it as a code `sales_channels` would look for and not find;
262
+ * `SETTING_SECRET_KEY_MISSING` is raised out of a secret codec that
263
+ * `@endora-commerce/platform`, `credentials` and `ksef` each ship a copy of.
264
+ * Both are refusals about a setting's value, which is the noun the chain
265
+ * follows.
266
+ *
267
+ * **The inverse holds and is the larger half.** This module raises four codes
268
+ * it does not own: `MODULE_ACTIVATION_PROTECTED` (twice) and
269
+ * `MODULE_SETTING_READ_ONLY` on the chain's `MODULE_` branch,
270
+ * `VERSION_CONFLICT` and `INTERNAL` in its generic set — all four route to
271
+ * `core` and are not declared here. And `invoices`'
272
+ * `INVOICE_NUMBER_PATTERN_COLLIDES` is refused *inside this module's write
273
+ * path*: the D-95.2 validator seam has `settings` answer "what would every
274
+ * channel's value be after this write" and the declaring module answer "is
275
+ * that legal", so the throw is `invoices`' own and !1121 declared it there.
276
+ * Routing follows the domain noun, never the thrower.
277
+ *
278
+ * **Three of the ten are raised outside this package**, which is the same
279
+ * rule seen from the other side and is why the raise-site grep is over
280
+ * `packages backend/src` rather than over this module: the platform's
281
+ * activation Command and `audit_logs`' recent-activity Command each raise
282
+ * `SETTING_NOT_REGISTERED` when a module declares a setting the reconciler
283
+ * never created a row for, and `search`'s LLM toggle raises
284
+ * `SETTING_OUT_OF_SCOPE_FOR_CHANNEL`.
285
+ *
286
+ * **Two of the ten are raised by nothing, and they are a kind the register
287
+ * does not yet hold** — `SETTING_CODE_CONFLICT` and
288
+ * `SETTING_BREAKING_CHANGE_REJECTED`.
289
+ * Both rules exist, both are enforced today and both are asserted by
290
+ * `backend/test/unit/settings/manifest-reconciler.test.ts`; they refuse where
291
+ * no envelope reaches. `@endora-commerce/platform`'s settings manifest
292
+ * reconciler throws `SettingCodeConflict` and `BreakingChangeRejected`, plain
293
+ * `Error` subclasses, at boot and on `module:install`, and a setting
294
+ * *definition* has no HTTP door at all — `routes.admin.ts` exposes values and
295
+ * groups and never definitions — so the HTTP code was never wired to the
296
+ * refusal it names. That is none of the four kinds
297
+ * `specs/deferred-defects.md` records: the schema does not pre-empt it, no
298
+ * sibling guard answers differently, no later rule superseded it, and it was
299
+ * not left unbuilt. The rule was built, and the `errors.*` sentence is a
300
+ * second representation of it that no client can receive. Not repaired here —
301
+ * both are declared, because ownership follows the capture and not the raise
302
+ * sites (trap T10), and dropping either reds the progress test as
303
+ * `undeclared`.
304
+ *
305
+ * Both spellings were searched, which trap T12 asks for: `ERROR_CODES.<CODE>`
306
+ * and the bare quoted literal, over `packages`, `backend`, `admin` and
307
+ * `storefront`. The only occurrences of either are the enumeration entry in
308
+ * `@endora-commerce/contracts` and this declaration. What makes the miss
309
+ * legible is that the pattern which would have connected them exists in the
310
+ * same directory and was not applied: `SettingNotRegistered`,
311
+ * `SettingOutOfScopeForChannel` and `SettingValueShapeMismatch` in
312
+ * `kernel/settings/settings.service.ts` each carry
313
+ * `readonly code = 'SETTING_…' as const`, while `BreakingChangeRejected`
314
+ * carries no `code` and `SettingCodeConflict`'s `code` is the *setting's*
315
+ * code, not an error code — a name collision that reads like the link and is
316
+ * not one.
317
+ *
318
+ * No `tokens`, derived rather than assumed. `refusalToken`
319
+ * (`packages/platform/src/http/error-envelope.ts`) reads `details.code` and
320
+ * nothing else; all seventeen raises of these ten codes across `packages` and
321
+ * `backend/src` were read, fifteen pass no fourth argument at all, and the
322
+ * two that do — `search`'s LLM toggle and this module's own `valueType`
323
+ * refusal — pass the Zod-style `Array<{path, issue}>`, which `refusalToken`
324
+ * returns `null` for by construction. The runbook's §5 raise-site scan
325
+ * attributes the tree's ten token-carrying codes over 41 sites to `core`,
326
+ * `invoices` and `carts` and names none of these, and the bundles hold no
327
+ * `errors.<CODE>.<token>` key in the other direction.
328
+ */
329
+ errorCodes: [
330
+ { code: 'SETTING_BREAKING_CHANGE_REJECTED' },
331
+ { code: 'SETTING_CODE_CONFLICT' },
332
+ { code: 'SETTING_EMPTY_SUBSET' },
333
+ { code: 'SETTING_GROUP_CODE_CONFLICT' },
334
+ { code: 'SETTING_GROUP_NOT_FOUND' },
335
+ { code: 'SETTING_GROUP_PROTECTED' },
336
+ { code: 'SETTING_NOT_REGISTERED' },
337
+ { code: 'SETTING_OUT_OF_SCOPE_FOR_CHANNEL' },
338
+ { code: 'SETTING_SECRET_KEY_MISSING' },
339
+ { code: 'SETTING_VALUE_SHAPE_MISMATCH' },
340
+ ],
341
+ i18n: { bundlesDir: 'i18n' },
342
+ docs: { dir: 'docs' },
343
+ permissions: [
344
+ { code: 'settings:read', label: 'View settings' },
345
+ { code: 'settings:write', label: 'Edit settings' },
346
+ ],
347
+ actions: [
348
+ {
349
+ id: 'open-settings',
350
+ labelKey: 'actions.openSettings.label',
351
+ descriptionKey: 'actions.openSettings.description',
352
+ icon: 'Settings',
353
+ targetRoute: '/settings',
354
+ // The only one of the 53 shipped actions that declared no code at all
355
+ // (issue #232), while `/api/v1/admin/settings` is
356
+ // `requireAdmin('settings:read')` — so the palette offered the screen to
357
+ // every role and every role without the code collected a 403 on arrival.
358
+ requiredPermission: 'settings:read',
359
+ keywords: ['settings', 'preferences', 'config', 'ustawienia'],
360
+ weight: 250,
361
+ },
362
+ ],
363
+ });
364
+ /** Legacy export retained for backward compatibility. */
365
+ export const settingsManifest = settings;
366
+ /**
367
+ * The operator command this module declares — feature 080, T042b / D-160.9.
368
+ *
369
+ * It was `scripts/cache-clear.ts`, which opened its own Redis connection and
370
+ * built its own `CacheAdminService`. It flushes through the composition's now.
371
+ */
372
+ export const cliCommands = [
373
+ {
374
+ name: 'cache-clear',
375
+ summary: 'Flush selected (or all) Redis cache namespaces.',
376
+ run: async (context) => (await import('./backend/cli/cache-clear.js')).cacheClear(context),
377
+ },
378
+ ];
379
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ ---
2
+ title: Settings
3
+ sidebar_position: 1
4
+ description: Manifest-driven, per-sales-channel platform configuration with cached read API
5
+ ---
6
+
7
+ # Settings
8
+
9
+ The Settings module owns platform-wide configuration. Other modules contribute
10
+ their own setting groups and individual settings through a typed manifest;
11
+ platform administrators tune values per sales channel from the Admin UI; any
12
+ backend module reads values through one well-known service.
13
+
14
+ ## Concepts
15
+
16
+ - **Setting** — a single tunable knob. Carries a name, a globally-unique
17
+ machine code, a value type (`string` / `number` / `boolean` / `json` /
18
+ `string_list`), a manifest-supplied default value, and a sales-channel
19
+ scope (empty scope means "applies to every channel").
20
+ - **Setting Value** — an admin-chosen value for a `(setting, sales_channel)`
21
+ pair. Replaces the manifest default for that channel. Resolution order is
22
+ always: per-channel value → manifest default.
23
+ - **Setting Group** — a logical section under which related settings appear in
24
+ the Admin UI. The built-in `general` group is system-protected. Deleting any
25
+ other group reassigns its settings to `general` and preserves their values.
26
+
27
+ ## For module authors — declaring your settings
28
+
29
+ Each module that wants to register settings or groups exports a sibling
30
+ `manifest.ts` file using the helper from `@endora-commerce/contracts`:
31
+
32
+ ```ts
33
+ // packages/modules/<your_module>/src/manifest.ts
34
+ import { defineModuleSettingsManifest } from '@endora-commerce/contracts';
35
+
36
+ export const settingsManifest = defineModuleSettingsManifest({
37
+ moduleCode: 'your_module',
38
+ groups: [{ code: 'your_section', name: 'Your section' }],
39
+ settings: [
40
+ {
41
+ code: 'your_module.base_url',
42
+ name: 'Base URL',
43
+ groupCode: 'your_section', // optional — defaults to 'general'
44
+ valueType: 'string',
45
+ defaultValue: 'https://default.example',
46
+ // salesChannelCodes: ['main', 'wholesale'] // optional — empty = all
47
+ },
48
+ ],
49
+ });
50
+ ```
51
+
52
+ Append your manifest to the array in `backend/src/composition.ts` — the
53
+ boot-time reconciler walks every entry and inserts any missing rows
54
+ idempotently. Re-running is always safe; admin-chosen values are never
55
+ overwritten.
56
+
57
+ ### Reconciler guarantees
58
+
59
+ | Behavior | Outcome |
60
+ |----------|---------|
61
+ | First-time apply | New groups + settings inserted; `owner_module` set to your module code. |
62
+ | Re-apply (no changes) | No-op. |
63
+ | Re-apply (added entry) | Only new entries are inserted. |
64
+ | Re-apply (renamed `name`/`description`) | Updated in place. |
65
+ | Re-apply (changed `valueType` or `defaultValue`) | Rejected without `--force` to keep existing per-channel values valid. |
66
+ | Re-apply (entry removed from manifest) | Boot sync ignores the removal — orphan rows are logged but not deleted. Only `modules:uninstall` is destructive. |
67
+ | Setting code conflicts with another module | Reconciliation aborts with a clear error. |
68
+
69
+ ## For platform admins — editing values
70
+
71
+ Open `Admin → Operations → Settings`. Settings are grouped by their owning
72
+ section. Selecting one opens an editor on the right; you can:
73
+
74
+ - Apply a value to **every sales channel in scope** in one click.
75
+ - Apply a value to a **chosen subset** of channels (the editor enforces the
76
+ "at least one channel" rule).
77
+ - **Reset** the per-channel values back to the manifest default.
78
+
79
+ Concurrent edits are detected via an `expectedVersion` (ISO timestamp). If
80
+ someone else has changed the setting since you opened it, the save returns
81
+ `409 VERSION_CONFLICT` and a banner asks you to refresh and retry — no silent
82
+ overwrites.
83
+
84
+ Setting groups are managed under `Admin → Operations → Setting groups`.
85
+ The `general` group is system-protected; the platform refuses to delete it.
86
+ Deleting any other group reassigns its settings to `general` and preserves
87
+ their per-channel values.
88
+
89
+ Every value change and group mutation lands an `audit_log_entries` row with
90
+ the actor, action, target, and new value.
91
+
92
+ ## For module consumers — reading values
93
+
94
+ Other modules inject `SettingsService` from the composition root and read
95
+ through a single API. The result is always either the admin-chosen value
96
+ for the requested channel or the manifest default.
97
+
98
+ ```ts
99
+ import { z } from 'zod';
100
+
101
+ // In your module's plugin:
102
+ const baseUrl = await settingsService.get(
103
+ 'your_module.base_url',
104
+ request.salesChannelId,
105
+ z.string().url(),
106
+ );
107
+ ```
108
+
109
+ ### Errors
110
+
111
+ | Thrown | When |
112
+ |--------|------|
113
+ | `SettingNotRegistered` | The code has no row in `settings`. The caller has a typo or the module that should declare it has not been installed. |
114
+ | `SettingOutOfScopeForChannel` | The setting was registered with an explicit channel scope and the requested channel is not in that scope. Indicates a programmer error: the consuming module should not be reading this setting in this context. |
115
+ | `SettingValueShapeMismatch` | The stored value passed Zod validation at write time but failed the caller's schema (e.g. an admin set the value via a manifest with a wider type). Surfaced as a fail-fast misconfiguration. |
116
+
117
+ ### Performance
118
+
119
+ The universal getter is cheap enough to call freely from request paths.
120
+ Resolution order:
121
+
122
+ 1. Per-process LRU (1024 entries, entries older than 30 s ignored).
123
+ 2. Redis (`settings:v1:<code>:<channelId>`, TTL 1h).
124
+ 3. Postgres (one keyed lookup against `(setting_id, sales_channel_id)`).
125
+
126
+ Cache invalidation hangs off the EventBus events that the admin service
127
+ emits on every value or group mutation. The EventBus is in-process, so the
128
+ writing process picks the change up immediately, and every other process
129
+ picks it up on its next read past the 30 s window — invalidation dropped the
130
+ shared Redis entry, and the local window is measured from when the value was
131
+ loaded rather than from the last read, so even a setting read on every request
132
+ ages out.
133
+
134
+ That window is the bound on how long a missed invalidation can be visible. It
135
+ is also why the operator-facing "clear cache" page is a diagnostic tool rather
136
+ than a repair: it drops both layers in the process that serves it and the
137
+ shared entries for everyone, and every other process converges within the same
138
+ 30 s.
139
+
140
+ ## CLI
141
+
142
+ Two scripts ship with the backend:
143
+
144
+ ```bash
145
+ # Idempotently reconcile a module's manifest into the database.
146
+ pnpm --filter backend run modules:install <module-code> [--force] [--dry-run]
147
+
148
+ # Remove a module's settings + groups. The flag is required — there is no
149
+ # implicit default. --remove-settings deletes everything owned by the module
150
+ # (cascade-deletes per-channel values); --preserve-settings keeps everything
151
+ # in place so a future re-install picks the rows up unchanged.
152
+ pnpm --filter backend run modules:uninstall <module-code> \
153
+ (--remove-settings | --preserve-settings)
154
+ ```
155
+
156
+ Both commands write `audit_log_entries` rows. `modules:install` exit codes:
157
+ `0` success, `64` misuse, `65` invalid manifest, `66` conflict, `70` internal
158
+ error.
159
+
160
+ ## Database
161
+
162
+ Five tables introduced by migration `024_settings_init.ts`:
163
+
164
+ - `setting_groups` (with `is_system_protected` for the built-in `general`)
165
+ - `settings` (FK → `setting_groups`, value-type enum, `jsonb` default)
166
+ - `setting_values` (per-`(setting, sales_channel)` admin override; UNIQUE)
167
+ - `setting_group_sales_channels` (M:N scope)
168
+ - `setting_sales_channels` (M:N scope; empty = all channels)
169
+
170
+ `setting_values.sales_channel_id` cascade-deletes when the channel is
171
+ removed; remaining bindings on the same setting are preserved.