@hartl-services/medusa-base 2.6.0 → 2.7.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 (149) hide show
  1. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +3 -2
  2. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ImageSettingInput.js +4 -4
  3. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginActionsSection.js +4 -4
  4. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.d.ts +16 -0
  5. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginBreadcrumb.js +25 -0
  6. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginDetailPage.js +1 -1
  7. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +5 -4
  8. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeatureSections.js +8 -5
  9. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +4 -4
  10. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesCard.js +24 -27
  11. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +2 -2
  12. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +9 -9
  13. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.d.ts +5 -1
  14. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginHubCard.js +11 -15
  15. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.d.ts +10 -4
  16. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginPageShell.js +10 -5
  17. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +0 -1
  18. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +73 -26
  19. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -1
  20. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -6
  21. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.d.ts +11 -0
  22. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/ShopMasterDataBanner.js +15 -0
  23. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +36 -5
  24. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +144 -20
  25. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +36 -5
  26. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.d.ts +15 -0
  27. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSwitch.js +66 -0
  28. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +144 -20
  29. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +49 -0
  30. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +72 -0
  31. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.d.ts +1 -12
  32. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-draft.js +4 -20
  33. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +43 -0
  34. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +66 -0
  35. package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +22 -2
  36. package/.medusa/server/src/admin/admin-components/i18n/texts.js +29 -7
  37. package/.medusa/server/src/admin/admin-components/index.d.ts +12 -0
  38. package/.medusa/server/src/admin/admin-components/index.js +11 -0
  39. package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.d.ts +22 -0
  40. package/.medusa/server/src/admin/admin-components/shared/ui/AdminPageHeader.js +8 -0
  41. package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.d.ts +26 -0
  42. package/.medusa/server/src/admin/admin-components/shared/ui/AdminSection.js +12 -0
  43. package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.d.ts +23 -0
  44. package/.medusa/server/src/admin/admin-components/shared/ui/BreadcrumbTrail.js +20 -0
  45. package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.d.ts +21 -0
  46. package/.medusa/server/src/admin/admin-components/shared/ui/FileDropField.js +33 -0
  47. package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.d.ts +27 -0
  48. package/.medusa/server/src/admin/admin-components/shared/ui/FocusModalShell.js +13 -0
  49. package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.d.ts +13 -0
  50. package/.medusa/server/src/admin/admin-components/shared/ui/InfoTooltip.js +8 -0
  51. package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.d.ts +22 -0
  52. package/.medusa/server/src/admin/admin-components/shared/ui/MultiSelectDropdown.js +14 -0
  53. package/.medusa/server/src/admin/admin-components/shared/ui/PdfPreviewModal.js +1 -1
  54. package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.d.ts +10 -0
  55. package/.medusa/server/src/admin/admin-components/shared/ui/PluginIcon.js +10 -0
  56. package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.d.ts +27 -0
  57. package/.medusa/server/src/admin/admin-components/shared/ui/SwitchRow.js +15 -0
  58. package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.d.ts +15 -0
  59. package/.medusa/server/src/admin/admin-components/shared/ui/WidgetHeader.js +8 -0
  60. package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.d.ts +13 -0
  61. package/.medusa/server/src/admin/admin-components/shared/ui/file-drop.js +16 -0
  62. package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.d.ts +13 -0
  63. package/.medusa/server/src/admin/admin-components/shared/ui/multi-select.js +11 -0
  64. package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.d.ts +12 -0
  65. package/.medusa/server/src/admin/admin-components/shared/ui/plugin-icon.js +29 -0
  66. package/.medusa/server/src/admin/index.js +3123 -2019
  67. package/.medusa/server/src/admin/index.mjs +3126 -2022
  68. package/.medusa/server/src/api/admin-first-login-password-policy.js +56 -0
  69. package/.medusa/server/src/api/answer-actor-type-casing-not-found.js +26 -0
  70. package/.medusa/server/src/api/answer-code-provider-route-not-found.js +34 -0
  71. package/.medusa/server/src/api/code-account-password-routes.js +92 -0
  72. package/.medusa/server/src/api/middlewares.js +82 -17
  73. package/.medusa/server/src/api/password-policy.js +55 -10
  74. package/.medusa/server/src/api/require-login-code-customer-email.js +40 -0
  75. package/.medusa/server/src/api/store/customer-auth/login-code/route.js +17 -0
  76. package/.medusa/server/src/api/store/customer-auth/password-policy/route.js +19 -0
  77. package/.medusa/server/src/customer-auth/application/prepare-code-account-password-reset.js +111 -0
  78. package/.medusa/server/src/customer-auth/application/request-login-code.js +145 -0
  79. package/.medusa/server/src/customer-auth/customer-auth-methods-snapshot.js +55 -0
  80. package/.medusa/server/src/customer-auth/customer-password-policy.js +40 -0
  81. package/.medusa/server/src/customer-auth/domain/account-linking.js +53 -0
  82. package/.medusa/server/src/customer-auth/domain/auth-methods.js +32 -0
  83. package/.medusa/server/src/customer-auth/domain/login-code.js +39 -0
  84. package/.medusa/server/src/customer-auth/login-code-accounts.js +146 -0
  85. package/.medusa/server/src/customer-auth/login-code-state.js +139 -0
  86. package/.medusa/server/src/customer-auth/plugin-features.js +80 -0
  87. package/.medusa/server/src/customer-auth/remove-provider-identity.js +18 -0
  88. package/.medusa/server/src/customer-auth/start-customer-auth-methods.js +20 -0
  89. package/.medusa/server/src/documents/plugin-features.js +8 -2
  90. package/.medusa/server/src/email/application/deliver-ephemeral-auth-mail.js +186 -0
  91. package/.medusa/server/src/email/application/deliver-login-code.js +89 -0
  92. package/.medusa/server/src/email/application/handle-customer-verification-event.js +29 -168
  93. package/.medusa/server/src/email/application/handle-password-reset-event.js +27 -166
  94. package/.medusa/server/src/email/customer-verification-snapshot.js +20 -108
  95. package/.medusa/server/src/email/domain/business-keys.js +15 -2
  96. package/.medusa/server/src/email/plugin-features.js +8 -3
  97. package/.medusa/server/src/email/rendering/default-templates.js +2 -1
  98. package/.medusa/server/src/email/templates/blocks/dynamic-blocks.js +19 -1
  99. package/.medusa/server/src/email/templates/defaults/documents.js +7 -1
  100. package/.medusa/server/src/email/templates/defaults/subjects.js +3 -1
  101. package/.medusa/server/src/email/templates/defaults/texts.js +5 -2
  102. package/.medusa/server/src/email/templates/render.js +2 -1
  103. package/.medusa/server/src/email/templates/sample-data.js +2 -1
  104. package/.medusa/server/src/email/templates/validate.js +8 -3
  105. package/.medusa/server/src/email/types/email-kinds.js +4 -2
  106. package/.medusa/server/src/email/types/template-data.js +9 -1
  107. package/.medusa/server/src/modules/customer-auth/index.js +14 -0
  108. package/.medusa/server/src/modules/customer-auth/service.js +35 -0
  109. package/.medusa/server/src/modules/email/migrations/Migration20261010090000.js +20 -0
  110. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -1
  111. package/.medusa/server/src/modules/plugin-features/service.js +26 -1
  112. package/.medusa/server/src/providers/email-otp/index.js +8 -0
  113. package/.medusa/server/src/providers/email-otp/service.js +96 -0
  114. package/.medusa/server/src/public/descriptor.js +117 -5
  115. package/.medusa/server/src/public/effective-features.js +22 -1
  116. package/.medusa/server/src/public/index.js +4 -2
  117. package/.medusa/server/src/public/plugin-icons.js +28 -0
  118. package/.medusa/server/src/public/shop-master-data.js +33 -14
  119. package/.medusa/server/src/shared/lib/index.js +2 -1
  120. package/.medusa/server/src/shared/lib/refreshing-snapshot.js +122 -0
  121. package/.medusa/server/src/shop-config/build-food-shop-config.js +38 -1
  122. package/README.md +436 -87
  123. package/dist/customer-auth/customer-auth-methods-snapshot.d.ts +25 -0
  124. package/dist/customer-auth/customer-auth-methods-snapshot.js +54 -0
  125. package/dist/customer-auth/domain/auth-methods.d.ts +16 -0
  126. package/dist/customer-auth/domain/auth-methods.js +31 -0
  127. package/dist/customer-auth/plugin-features.d.ts +2 -0
  128. package/dist/customer-auth/plugin-features.js +79 -0
  129. package/dist/email/customer-verification-snapshot.d.ts +3 -15
  130. package/dist/email/customer-verification-snapshot.js +19 -107
  131. package/dist/email/types/email-kinds.d.ts +4 -3
  132. package/dist/email/types/email-kinds.js +3 -1
  133. package/dist/email/types/template-data.d.ts +8 -0
  134. package/dist/email/types/template-data.js +8 -0
  135. package/dist/public/descriptor.js +116 -4
  136. package/dist/public/effective-features.d.ts +10 -0
  137. package/dist/public/effective-features.js +21 -0
  138. package/dist/public/index.d.ts +2 -1
  139. package/dist/public/index.js +3 -1
  140. package/dist/public/plugin-icons.d.ts +7 -0
  141. package/dist/public/plugin-icons.js +27 -0
  142. package/dist/public/shop-master-data.js +32 -13
  143. package/dist/public/types.d.ts +63 -1
  144. package/dist/shared/lib/index.d.ts +1 -0
  145. package/dist/shared/lib/index.js +1 -0
  146. package/dist/shared/lib/refreshing-snapshot.d.ts +52 -0
  147. package/dist/shared/lib/refreshing-snapshot.js +121 -0
  148. package/dist/shop-config/build-food-shop-config.js +37 -0
  149. package/package.json +8 -3
@@ -4,6 +4,7 @@ exports.validatePluginFeatureDescriptor = validatePluginFeatureDescriptor;
4
4
  exports.definePluginFeatures = definePluginFeatures;
5
5
  const utils_1 = require("@medusajs/framework/utils");
6
6
  const lib_1 = require("../shared/lib");
7
+ const plugin_icons_1 = require("./plugin-icons");
7
8
  const KEY_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
8
9
  const SETTING_TYPES = new Set([
9
10
  "string",
@@ -20,6 +21,10 @@ const ACTION_MODES = new Set(["sync", "background"]);
20
21
  const ADMIN_PAGE_PREFIX = "/settings/hartl-services/";
21
22
  const BASE_PAGE_PREFIX = "/settings/hartl-services/plugin/";
22
23
  const ADMIN_PAGE_PATTERN = /^\/settings\/hartl-services(\/[a-z0-9][a-z0-9_-]*)+$/;
24
+ const ICON_NAMES = new Set(plugin_icons_1.PLUGIN_ICON_NAMES);
25
+ const SETTING_WIDTHS = new Set(["full", "half"]);
26
+ /** Types an input placeholder makes no sense for. */
27
+ const NO_PLACEHOLDER_TYPES = new Set(["boolean", "select", "image", "color"]);
23
28
  const invalid = (pluginKey, message) => new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Ungültige Funktionsschalter für Plugin "${pluginKey}": ${message}`);
24
29
  const defaultMatchesType = (setting) => {
25
30
  const value = setting.default;
@@ -32,7 +37,9 @@ const defaultMatchesType = (setting) => {
32
37
  case "color":
33
38
  return typeof value === "string" && HEX_COLOR_PATTERN.test(value);
34
39
  case "number":
35
- return typeof value === "number" && Number.isFinite(value);
40
+ return (typeof value === "number" &&
41
+ Number.isFinite(value) &&
42
+ (setting.integer !== true || Number.isInteger(value)));
36
43
  case "boolean":
37
44
  return typeof value === "boolean";
38
45
  case "select":
@@ -46,7 +53,10 @@ const withinBounds = (setting) => typeof setting.default !== "number" ||
46
53
  ((setting.min === undefined || setting.default >= setting.min) &&
47
54
  (setting.max === undefined || setting.default <= setting.max));
48
55
  const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
49
- /** `options` only (and required) for select, `min`/`max` only for number. */
56
+ /**
57
+ * `options` only (and required) for select, `min`/`max`/`integer` only for
58
+ * number.
59
+ */
50
60
  const validateTypeExtras = (pluginKey, setting) => {
51
61
  if (setting.type === "select") {
52
62
  const options = setting.options;
@@ -68,11 +78,16 @@ const validateTypeExtras = (pluginKey, setting) => {
68
78
  throw invalid(pluginKey, `Einstellung "${setting.key}": options gibt es nur bei "select".`);
69
79
  }
70
80
  if (setting.type !== "number") {
71
- if (setting.min !== undefined || setting.max !== undefined) {
72
- throw invalid(pluginKey, `Einstellung "${setting.key}": min/max gibt es nur bei "number".`);
81
+ if (setting.min !== undefined ||
82
+ setting.max !== undefined ||
83
+ setting.integer !== undefined) {
84
+ throw invalid(pluginKey, `Einstellung "${setting.key}": min/max/integer gibt es nur bei "number".`);
73
85
  }
74
86
  return;
75
87
  }
88
+ if (setting.integer !== undefined && typeof setting.integer !== "boolean") {
89
+ throw invalid(pluginKey, `Einstellung "${setting.key}": integer muss boolean sein.`);
90
+ }
76
91
  for (const [name, bound] of [
77
92
  ["min", setting.min],
78
93
  ["max", setting.max],
@@ -111,6 +126,28 @@ const validateInvalidates = (pluginKey, setting, settings) => {
111
126
  seen.add(key);
112
127
  }
113
128
  };
129
+ /** `hint`, `placeholder`, `section` and `width` of one setting. */
130
+ const validateDisplayFields = (pluginKey, setting) => {
131
+ for (const name of ["hint", "placeholder", "section"]) {
132
+ const value = setting[name];
133
+ if (value !== undefined && !(0, lib_1.isNonEmptyString)(value)) {
134
+ throw invalid(pluginKey, `Einstellung "${setting.key}": ${name} muss ein nicht-leerer Text sein.`);
135
+ }
136
+ }
137
+ if (setting.placeholder !== undefined && NO_PLACEHOLDER_TYPES.has(setting.type)) {
138
+ throw invalid(pluginKey, `Einstellung "${setting.key}": placeholder gibt es nicht bei "${setting.type}".`);
139
+ }
140
+ const width = setting.width;
141
+ if (width === undefined) {
142
+ return;
143
+ }
144
+ if (typeof width !== "string" || !SETTING_WIDTHS.has(width)) {
145
+ throw invalid(pluginKey, `Einstellung "${setting.key}": width muss "full" oder "half" sein.`);
146
+ }
147
+ if (setting.type === "boolean") {
148
+ throw invalid(pluginKey, `Einstellung "${setting.key}": width gibt es nicht bei "boolean".`);
149
+ }
150
+ };
114
151
  const validateSettings = (pluginKey, settings) => {
115
152
  const seen = new Set();
116
153
  for (const setting of settings) {
@@ -134,6 +171,7 @@ const validateSettings = (pluginKey, settings) => {
134
171
  throw invalid(pluginKey, `Einstellung "${setting.key}": required muss boolean sein.`);
135
172
  }
136
173
  validateTypeExtras(pluginKey, setting);
174
+ validateDisplayFields(pluginKey, setting);
137
175
  validateInvalidates(pluginKey, setting, settings);
138
176
  if (setting.default === undefined) {
139
177
  continue;
@@ -184,6 +222,78 @@ const validateAdminPage = (pluginKey, path) => {
184
222
  throw invalid(pluginKey, `admin_page "${String(path)}" muss unter "${ADMIN_PAGE_PREFIX}<plugin>" liegen und darf nicht mit "${BASE_PAGE_PREFIX}" beginnen.`);
185
223
  }
186
224
  };
225
+ /**
226
+ * Metadata per settings group: unique names, each used by a setting's
227
+ * `group`, `requires_feature` a declared feature key.
228
+ */
229
+ const validateSettingGroups = (pluginKey, groups, settings, featureKeys) => {
230
+ if (groups === undefined) {
231
+ return;
232
+ }
233
+ if (!Array.isArray(groups)) {
234
+ throw invalid(pluginKey, "setting_groups muss eine Liste sein.");
235
+ }
236
+ const usedGroups = new Set(settings.map((setting) => setting.group));
237
+ const seen = new Set();
238
+ for (const group of groups) {
239
+ if (!(0, lib_1.isNonEmptyString)(group?.name)) {
240
+ throw invalid(pluginKey, "Eine Einstellungsgruppe hat keinen name.");
241
+ }
242
+ if (seen.has(group.name)) {
243
+ throw invalid(pluginKey, `Einstellungsgruppe "${group.name}" ist doppelt.`);
244
+ }
245
+ seen.add(group.name);
246
+ if (!usedGroups.has(group.name)) {
247
+ throw invalid(pluginKey, `Einstellungsgruppe "${group.name}" wird von keiner Einstellung verwendet.`);
248
+ }
249
+ for (const name of ["description", "note"]) {
250
+ if (group[name] !== undefined && !(0, lib_1.isNonEmptyString)(group[name])) {
251
+ throw invalid(pluginKey, `Einstellungsgruppe "${group.name}": ${name} muss ein nicht-leerer Text sein.`);
252
+ }
253
+ }
254
+ if (group.requires_feature !== undefined &&
255
+ !featureKeys.has(group.requires_feature)) {
256
+ throw invalid(pluginKey, `Einstellungsgruppe "${group.name}": requires_feature "${String(group.requires_feature)}" ist kein Feature dieses Plugins.`);
257
+ }
258
+ }
259
+ };
260
+ /** Optional texts and icon of the plugin's Admin page. */
261
+ const validatePresentation = (pluginKey, descriptor) => {
262
+ const icon = descriptor.icon;
263
+ if (icon !== undefined && (typeof icon !== "string" || !ICON_NAMES.has(icon))) {
264
+ throw invalid(pluginKey, `icon "${String(icon)}" ist kein bekannter Icon-Name.`);
265
+ }
266
+ for (const name of [
267
+ "summary",
268
+ "disable_hint",
269
+ "disable_confirm",
270
+ "features_heading",
271
+ "features_note",
272
+ "all_off_warning",
273
+ ]) {
274
+ const value = descriptor[name];
275
+ if (value !== undefined && !(0, lib_1.isNonEmptyString)(value)) {
276
+ throw invalid(pluginKey, `${name} muss ein nicht-leerer Text sein.`);
277
+ }
278
+ }
279
+ if (descriptor.switchable === false &&
280
+ (descriptor.disable_hint !== undefined ||
281
+ descriptor.disable_confirm !== undefined)) {
282
+ throw invalid(pluginKey, "disable_hint und disable_confirm gibt es nur bei abschaltbaren Plugins.");
283
+ }
284
+ if (descriptor.all_off_warning !== undefined && descriptor.features.length === 0) {
285
+ throw invalid(pluginKey, "all_off_warning braucht mindestens ein Feature.");
286
+ }
287
+ const requireEnabled = descriptor.require_enabled_feature;
288
+ if (requireEnabled !== undefined) {
289
+ if (requireEnabled !== true) {
290
+ throw invalid(pluginKey, "require_enabled_feature muss true sein.");
291
+ }
292
+ if (!descriptor.features.some((feature) => feature.parent_key === undefined && feature.default_enabled)) {
293
+ throw invalid(pluginKey, "require_enabled_feature braucht mindestens ein Feature ohne parent_key mit default_enabled: true.");
294
+ }
295
+ }
296
+ };
187
297
  /**
188
298
  * Checks key format, unique feature keys, existing parents and the single
189
299
  * allowed nesting level, plus the optional settings, actions and Admin page.
@@ -236,7 +346,9 @@ function validatePluginFeatureDescriptor(descriptor) {
236
346
  throw invalid(pluginKey, `Feature "${feature.key}" ist zu tief verschachtelt; erlaubt ist eine Ebene.`);
237
347
  }
238
348
  }
349
+ validatePresentation(pluginKey, descriptor);
239
350
  validateSettings(pluginKey, descriptor.settings ?? []);
351
+ validateSettingGroups(pluginKey, descriptor.setting_groups, descriptor.settings ?? [], new Set(byKey.keys()));
240
352
  validateActions(pluginKey, descriptor.actions ?? []);
241
353
  validateAdminPage(pluginKey, descriptor.admin_page);
242
354
  }
@@ -27,3 +27,13 @@ export declare function resolveEffectivePluginFeatures(descriptor: PluginFeature
27
27
  * default).
28
28
  */
29
29
  export declare function validateFeatureUpdate(descriptor: PluginFeatureDescriptor, features: Record<string, boolean>): Record<string, boolean>;
30
+ /**
31
+ * For a descriptor with `require_enabled_feature`: rejects (`INVALID_DATA`)
32
+ * an update that switches off the last enabled top-level feature. `features`
33
+ * are the values to persist (`validateFeatureUpdate`; a key without a value
34
+ * counts by its default), `current` the configured values before the update
35
+ * (`resolvePluginFeatureStates`). An update that does not reduce the enabled
36
+ * set is always allowed, so an already stored all-off state can be left and
37
+ * the plugin switch can be flipped with unchanged features.
38
+ */
39
+ export declare function assertRequiredFeatureEnabled(descriptor: PluginFeatureDescriptor, features: Record<string, boolean>, current: Record<string, boolean>): void;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolvePluginFeatureStates = resolvePluginFeatureStates;
4
4
  exports.resolveEffectivePluginFeatures = resolveEffectivePluginFeatures;
5
5
  exports.validateFeatureUpdate = validateFeatureUpdate;
6
+ exports.assertRequiredFeatureEnabled = assertRequiredFeatureEnabled;
6
7
  const utils_1 = require("@medusajs/framework/utils");
7
8
  /**
8
9
  * Configured and effective values of every declared feature. A descriptor
@@ -75,3 +76,23 @@ function validateFeatureUpdate(descriptor, features) {
75
76
  }
76
77
  return result;
77
78
  }
79
+ /**
80
+ * For a descriptor with `require_enabled_feature`: rejects (`INVALID_DATA`)
81
+ * an update that switches off the last enabled top-level feature. `features`
82
+ * are the values to persist (`validateFeatureUpdate`; a key without a value
83
+ * counts by its default), `current` the configured values before the update
84
+ * (`resolvePluginFeatureStates`). An update that does not reduce the enabled
85
+ * set is always allowed, so an already stored all-off state can be left and
86
+ * the plugin switch can be flipped with unchanged features.
87
+ */
88
+ function assertRequiredFeatureEnabled(descriptor, features, current) {
89
+ if (descriptor.require_enabled_feature !== true) {
90
+ return;
91
+ }
92
+ const topLevel = descriptor.features.filter((feature) => feature.parent_key === undefined);
93
+ const anyOnAfter = topLevel.some((feature) => (features[feature.key] ?? feature.default_enabled) === true);
94
+ const anyOnBefore = topLevel.some((feature) => current[feature.key] === true);
95
+ if (!anyOnAfter && anyOnBefore) {
96
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Mindestens eine Funktion muss eingeschaltet bleiben.");
97
+ }
98
+ }
@@ -3,8 +3,9 @@ export { definePluginFeatures } from "./descriptor";
3
3
  export { resolveEffectivePluginFeatures } from "./effective-features";
4
4
  export { isOwnPluginEvent, isPluginFeatureEnabled, isPluginFeatureEnabledByKey, pluginFeatureGate, requirePluginFeature, } from "./enforcement";
5
5
  export { isBasePluginRegistered, resolveBaseModule } from "./module-access";
6
+ export { PLUGIN_ICON_NAMES } from "./plugin-icons";
6
7
  export { getPluginSettings } from "./settings";
7
8
  export { getShopAppearance, getShopMasterData, shopMasterDataDescriptor, type ResolvedShopMasterData, type ShopAppearance, type ShopAppearanceColors, type ShopMasterData, type ShopMasterDataKey, } from "./shop-master-data";
8
9
  export { reportPluginActionProgress, type PluginActionProgress, } from "./action-progress";
9
- export type { BasePluginOptions, EffectivePluginFeatures, PluginActionDefinition, PluginActionMode, PluginActionProgressInput, PluginActionWorkflowInput, PluginFeatureDefinition, PluginFeatureDescriptor, PluginFeatureProvider, PluginSettingDefinition, PluginSettingOption, PluginSettingsFallback, PluginSettingType, PluginSettingValue, ResolvedPluginSettings, } from "./types";
10
+ export type { BasePluginOptions, EffectivePluginFeatures, PluginActionDefinition, PluginActionMode, PluginActionProgressInput, PluginActionWorkflowInput, PluginFeatureDefinition, PluginFeatureDescriptor, PluginFeatureProvider, PluginIconName, PluginSettingDefinition, PluginSettingGroupDefinition, PluginSettingOption, PluginSettingsFallback, PluginSettingType, PluginSettingValue, ResolvedPluginSettings, } from "./types";
10
11
  export type { CompleteCartHookDeclarations, CompleteCartHookName, CompleteCartOrderCreatedInput, CompleteCartParticipant, CompleteCartParticipantStep, CompleteCartValidateInput, } from "./complete-cart";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.reportPluginActionProgress = exports.shopMasterDataDescriptor = exports.getShopMasterData = exports.getShopAppearance = exports.getPluginSettings = exports.resolveBaseModule = exports.isBasePluginRegistered = exports.requirePluginFeature = exports.pluginFeatureGate = exports.isPluginFeatureEnabledByKey = exports.isPluginFeatureEnabled = exports.isOwnPluginEvent = exports.resolveEffectivePluginFeatures = exports.definePluginFeatures = exports.PLUGIN_FEATURES_MODULE = void 0;
3
+ exports.reportPluginActionProgress = exports.shopMasterDataDescriptor = exports.getShopMasterData = exports.getShopAppearance = exports.getPluginSettings = exports.PLUGIN_ICON_NAMES = exports.resolveBaseModule = exports.isBasePluginRegistered = exports.requirePluginFeature = exports.pluginFeatureGate = exports.isPluginFeatureEnabledByKey = exports.isPluginFeatureEnabled = exports.isOwnPluginEvent = exports.resolveEffectivePluginFeatures = exports.definePluginFeatures = exports.PLUGIN_FEATURES_MODULE = void 0;
4
4
  var constants_1 = require("./constants");
5
5
  Object.defineProperty(exports, "PLUGIN_FEATURES_MODULE", { enumerable: true, get: function () { return constants_1.PLUGIN_FEATURES_MODULE; } });
6
6
  var descriptor_1 = require("./descriptor");
@@ -16,6 +16,8 @@ Object.defineProperty(exports, "requirePluginFeature", { enumerable: true, get:
16
16
  var module_access_1 = require("./module-access");
17
17
  Object.defineProperty(exports, "isBasePluginRegistered", { enumerable: true, get: function () { return module_access_1.isBasePluginRegistered; } });
18
18
  Object.defineProperty(exports, "resolveBaseModule", { enumerable: true, get: function () { return module_access_1.resolveBaseModule; } });
19
+ var plugin_icons_1 = require("./plugin-icons");
20
+ Object.defineProperty(exports, "PLUGIN_ICON_NAMES", { enumerable: true, get: function () { return plugin_icons_1.PLUGIN_ICON_NAMES; } });
19
21
  var settings_1 = require("./settings");
20
22
  Object.defineProperty(exports, "getPluginSettings", { enumerable: true, get: function () { return settings_1.getPluginSettings; } });
21
23
  var shop_master_data_1 = require("./shop-master-data");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Names of `@medusajs/icons` components a plugin descriptor may use as `icon`.
3
+ * A plain list so the server needs no icon package; the Admin maps the name to
4
+ * the component and falls back to a default icon for an unknown one. Every
5
+ * name is an export of `@medusajs/icons`.
6
+ */
7
+ export declare const PLUGIN_ICON_NAMES: readonly ["ArchiveBox", "ArrowPath", "BuildingStorefront", "Buildings", "ClockSolid", "CogSixTooth", "CreditCard", "DocumentText", "Envelope", "ReceiptPercent", "ShieldCheck", "ShoppingCart", "SquaresPlus", "Tag", "TruckFast", "Users"];
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLUGIN_ICON_NAMES = void 0;
4
+ /**
5
+ * Names of `@medusajs/icons` components a plugin descriptor may use as `icon`.
6
+ * A plain list so the server needs no icon package; the Admin maps the name to
7
+ * the component and falls back to a default icon for an unknown one. Every
8
+ * name is an export of `@medusajs/icons`.
9
+ */
10
+ exports.PLUGIN_ICON_NAMES = [
11
+ "ArchiveBox",
12
+ "ArrowPath",
13
+ "BuildingStorefront",
14
+ "Buildings",
15
+ "ClockSolid",
16
+ "CogSixTooth",
17
+ "CreditCard",
18
+ "DocumentText",
19
+ "Envelope",
20
+ "ReceiptPercent",
21
+ "ShieldCheck",
22
+ "ShoppingCart",
23
+ "SquaresPlus",
24
+ "Tag",
25
+ "TruckFast",
26
+ "Users",
27
+ ];
@@ -7,6 +7,10 @@ const descriptor_1 = require("./descriptor");
7
7
  const settings_1 = require("./settings");
8
8
  const COMPANY = "Firmendaten";
9
9
  const APPEARANCE = "Erscheinungsbild";
10
+ const ADDRESS = "Adresse";
11
+ const TAX_CONTACT = "Steuer und Kontakt";
12
+ const BANK_REGISTER = "Bank und Firmenbuch";
13
+ const COLORS = "Farben";
10
14
  const DEFAULT_COLOR_HINT = "Leer lassen für die Standardfarbe von E-Mails und Dokumenten.";
11
15
  /**
12
16
  * The base's own descriptor: the shop's master data (Firmenangaben) and its
@@ -18,18 +22,22 @@ exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
18
22
  plugin_key: "shop",
19
23
  label: "Shop-Stammdaten",
20
24
  description: "Firmenangaben des Shops für E-Mails, Rechnungen, Dokumente und Steuerprüfungen sowie Logo und Farben von E-Mails und Dokumenten.",
25
+ summary: "Firmenangaben, Logo und Farben für E-Mails, Rechnungen, Dokumente und Steuerprüfungen.",
26
+ icon: "Buildings",
21
27
  switchable: false,
22
28
  features: [],
23
29
  settings: [
24
- { key: "legal_name", group: COMPANY, label: "Firmenname", type: "string", required: true },
25
- { key: "address_line1", group: COMPANY, label: "Adresse", type: "string", required: true },
26
- { key: "address_line2", group: COMPANY, label: "Adresszusatz", type: "string" },
27
- { key: "postal_code", group: COMPANY, label: "Postleitzahl", type: "string", required: true },
28
- { key: "city", group: COMPANY, label: "Ort", type: "string", required: true },
30
+ { key: "legal_name", group: COMPANY, section: ADDRESS, label: "Firmenname", type: "string", required: true },
31
+ { key: "address_line1", group: COMPANY, section: ADDRESS, width: "half", label: "Adresse", type: "string", required: true },
32
+ { key: "address_line2", group: COMPANY, section: ADDRESS, width: "half", label: "Adresszusatz", type: "string" },
33
+ { key: "postal_code", group: COMPANY, section: ADDRESS, width: "half", label: "Postleitzahl", type: "string", required: true },
34
+ { key: "city", group: COMPANY, section: ADDRESS, width: "half", label: "Ort", type: "string", required: true },
29
35
  {
30
36
  key: "country_code",
31
37
  group: COMPANY,
32
38
  label: "Land",
39
+ section: ADDRESS,
40
+ width: "half",
33
41
  type: "select",
34
42
  required: true,
35
43
  // EU-27 plus CH, GB, NO, IS, LI (ISO 3166-1 alpha-2, lowercase),
@@ -69,24 +77,27 @@ exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
69
77
  { value: "cy", label: "Zypern" },
70
78
  ],
71
79
  },
72
- { key: "vat_number", group: COMPANY, label: "UID-Nummer", type: "string", required: true },
73
- { key: "email", group: COMPANY, label: "E-Mail", type: "string" },
74
- { key: "phone", group: COMPANY, label: "Telefon", type: "string" },
75
- { key: "website", group: COMPANY, label: "Website", type: "url" },
76
- { key: "iban", group: COMPANY, label: "IBAN", type: "string" },
77
- { key: "bic", group: COMPANY, label: "BIC", type: "string" },
78
- { key: "company_register_number", group: COMPANY, label: "Firmenbuchnummer", type: "string" },
79
- { key: "company_register_court", group: COMPANY, label: "Firmenbuchgericht", type: "string" },
80
+ { key: "vat_number", group: COMPANY, section: TAX_CONTACT, width: "half", label: "UID-Nummer", type: "string", required: true },
81
+ { key: "email", group: COMPANY, section: TAX_CONTACT, width: "half", label: "E-Mail", type: "string" },
82
+ { key: "phone", group: COMPANY, section: TAX_CONTACT, width: "half", label: "Telefon", type: "string" },
83
+ { key: "website", group: COMPANY, section: TAX_CONTACT, width: "half", label: "Website", type: "url" },
84
+ { key: "iban", group: COMPANY, section: BANK_REGISTER, width: "half", label: "IBAN", type: "string" },
85
+ { key: "bic", group: COMPANY, section: BANK_REGISTER, width: "half", label: "BIC", type: "string" },
86
+ { key: "company_register_number", group: COMPANY, section: BANK_REGISTER, width: "half", label: "Firmenbuchnummer", type: "string" },
87
+ { key: "company_register_court", group: COMPANY, section: BANK_REGISTER, width: "half", label: "Firmenbuchgericht", type: "string" },
80
88
  {
81
89
  key: "logo",
82
90
  group: APPEARANCE,
83
91
  label: "Logo",
84
92
  description: "PNG oder JPG, erscheint oben in den E-Mails und auf Rechnungen und Lieferscheinen. Ohne Logo steht dort der Firmenname bzw. Shopname.",
93
+ hint: "PNG oder JPG, ohne Logo erscheint der Firmenname.",
85
94
  type: "image",
86
95
  },
87
96
  {
88
97
  key: "color_accent",
89
98
  group: APPEARANCE,
99
+ section: COLORS,
100
+ width: "half",
90
101
  label: "Akzentfarbe",
91
102
  description: `Buttons, Überschriften und Links. ${DEFAULT_COLOR_HINT}`,
92
103
  type: "color",
@@ -94,6 +105,8 @@ exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
94
105
  {
95
106
  key: "color_text",
96
107
  group: APPEARANCE,
108
+ section: COLORS,
109
+ width: "half",
97
110
  label: "Textfarbe",
98
111
  description: `Fließtext und Beträge. ${DEFAULT_COLOR_HINT}`,
99
112
  type: "color",
@@ -101,6 +114,8 @@ exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
101
114
  {
102
115
  key: "color_muted",
103
116
  group: APPEARANCE,
117
+ section: COLORS,
118
+ width: "half",
104
119
  label: "Nebentextfarbe",
105
120
  description: `Beschriftungen, Hinweise und Fußzeilen. ${DEFAULT_COLOR_HINT}`,
106
121
  type: "color",
@@ -108,6 +123,8 @@ exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
108
123
  {
109
124
  key: "color_line",
110
125
  group: APPEARANCE,
126
+ section: COLORS,
127
+ width: "half",
111
128
  label: "Linienfarbe",
112
129
  description: `Rahmen und Trennlinien. ${DEFAULT_COLOR_HINT}`,
113
130
  type: "color",
@@ -115,6 +132,8 @@ exports.shopMasterDataDescriptor = (0, descriptor_1.definePluginFeatures)({
115
132
  {
116
133
  key: "color_surface",
117
134
  group: APPEARANCE,
135
+ section: COLORS,
136
+ width: "half",
118
137
  label: "Flächenfarbe",
119
138
  description: `Hinterlegte Flächen wie Infoboxen und Tabellenköpfe. ${DEFAULT_COLOR_HINT}`,
120
139
  type: "color",
@@ -1,6 +1,7 @@
1
1
  import type { DocumentPluginOptions } from "../modules/documents/domain/options";
2
2
  import type { EmailPluginOptions } from "../email/types";
3
3
  import type { FilesPluginOptions } from "../files/types";
4
+ import type { PLUGIN_ICON_NAMES } from "./plugin-icons";
4
5
  /**
5
6
  * Options of the base plugin (`plugins: [{ resolve, options }]`). Medusa hands
6
7
  * the same options object to every module of the plugin; each module service
@@ -23,6 +24,8 @@ export type BasePluginOptions = {
23
24
  /** `/files` gateway signing; set from `createS3CacheStorageConfig`. */
24
25
  readonly files?: FilesPluginOptions;
25
26
  };
27
+ /** `@medusajs/icons` component name a plugin may use as its icon. */
28
+ export type PluginIconName = (typeof PLUGIN_ICON_NAMES)[number];
26
29
  export type PluginFeatureDefinition<K extends string = string> = {
27
30
  readonly key: K;
28
31
  readonly label: string;
@@ -53,7 +56,7 @@ export type PluginSettingDefinition<S extends string = string> = {
53
56
  readonly type: PluginSettingType;
54
57
  /** `false` when omitted. */
55
58
  readonly required?: boolean;
56
- /** Never for `"secret"`; must match `type` (and `options`/`min`/`max`). */
59
+ /** Never for `"secret"`; must match `type` (and `options`/`min`/`max`/`integer`). */
57
60
  readonly default?: PluginSettingValue;
58
61
  /** Required for `"select"` (non-empty, unique values); only there. */
59
62
  readonly options?: readonly PluginSettingOption[];
@@ -61,12 +64,44 @@ export type PluginSettingDefinition<S extends string = string> = {
61
64
  readonly min?: number;
62
65
  /** Inclusive upper bound; only for `"number"`. */
63
66
  readonly max?: number;
67
+ /** Only whole numbers are accepted; only for `"number"`. */
68
+ readonly integer?: boolean;
64
69
  /**
65
70
  * Secret settings of the same plugin whose stored values are deleted when
66
71
  * this setting's stored value changes (also when it is cleared), e.g. an
67
72
  * API key that must not be sent to a new URL.
68
73
  */
69
74
  readonly invalidates?: readonly S[];
75
+ /**
76
+ * Short text under the field; the Admin shows `hint ?? description`.
77
+ */
78
+ readonly hint?: string;
79
+ /** Input placeholder; not for boolean, select, image and color. */
80
+ readonly placeholder?: string;
81
+ /**
82
+ * `"half"`: consecutive half-width settings share one two-column row.
83
+ * `"full"` when omitted. Not for boolean (always a full-width switch row).
84
+ */
85
+ readonly width?: "full" | "half";
86
+ /**
87
+ * Sub-heading inside the group; consecutive settings with the same section
88
+ * share one sub-heading.
89
+ */
90
+ readonly section?: string;
91
+ };
92
+ /** Metadata of one settings group, keyed by the `group` name of its settings. */
93
+ export type PluginSettingGroupDefinition<K extends string = string> = {
94
+ /** Equals the `group` of at least one setting; unique per plugin. */
95
+ readonly name: string;
96
+ /** Explanatory text under the group heading. */
97
+ readonly description?: string;
98
+ /** Short note shown on the right of the group heading. */
99
+ readonly note?: string;
100
+ /**
101
+ * Feature that must be on for the group to apply; the Admin dims the group
102
+ * (still editable) while the feature is not effectively enabled.
103
+ */
104
+ readonly requires_feature?: K;
70
105
  };
71
106
  export type PluginActionMode = "sync" | "background";
72
107
  export type PluginActionDefinition<A extends string = string> = {
@@ -100,6 +135,33 @@ export type PluginFeatureDescriptor<K extends string = string, S extends string
100
135
  * omitted).
101
136
  */
102
137
  readonly admin_page?: string;
138
+ /** `@medusajs/icons` component name for the hub card and the page header. */
139
+ readonly icon?: PluginIconName;
140
+ /** One short sentence: hub card text and page subtitle (Admin falls back to `description`). */
141
+ readonly summary?: string;
142
+ /** Line under the plugin switch, e.g. what stops working when it is off. Only when switchable. */
143
+ readonly disable_hint?: string;
144
+ /** Body of the confirmation shown before the plugin is switched off. Only when switchable. */
145
+ readonly disable_confirm?: string;
146
+ /** Heading of the features card; "Funktionen" when omitted. */
147
+ readonly features_heading?: string;
148
+ /** Note on the right of the features card header. */
149
+ readonly features_note?: string;
150
+ /** Warning shown while every top-level feature is off. Needs at least one feature. */
151
+ readonly all_off_warning?: string;
152
+ /**
153
+ * `true`: at least one top-level feature (without `parent_key`) must stay
154
+ * enabled; an update that would switch all of them off is rejected with
155
+ * `INVALID_DATA` and the Admin locks the last enabled switch. Needs at least
156
+ * one top-level feature with `default_enabled: true`. Only an update that
157
+ * switches off the last enabled one is rejected: a stored state that already
158
+ * violates the rule (everything off) can still be read and left, and an
159
+ * update that does not reduce the enabled set (e.g. the plugin switch) is
160
+ * always allowed.
161
+ */
162
+ readonly require_enabled_feature?: true;
163
+ /** Metadata for the groups used in `settings[].group`. */
164
+ readonly setting_groups?: readonly PluginSettingGroupDefinition<K>[];
103
165
  };
104
166
  /** Values a plugin received from medusa-config; the base shows them as source "config". */
105
167
  export type PluginSettingsFallback<S extends string = string> = Partial<Record<S, PluginSettingValue | null | undefined>>;
@@ -5,6 +5,7 @@ export * from "./errors";
5
5
  export * from "./hash";
6
6
  export * from "./logging";
7
7
  export * from "./money";
8
+ export * from "./refreshing-snapshot";
8
9
  export * from "./text";
9
10
  export * from "./transaction";
10
11
  export * from "./wire-response";
@@ -23,6 +23,7 @@ __exportStar(require("./errors"), exports);
23
23
  __exportStar(require("./hash"), exports);
24
24
  __exportStar(require("./logging"), exports);
25
25
  __exportStar(require("./money"), exports);
26
+ __exportStar(require("./refreshing-snapshot"), exports);
26
27
  __exportStar(require("./text"), exports);
27
28
  __exportStar(require("./transaction"), exports);
28
29
  __exportStar(require("./wire-response"), exports);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * In-memory copy of one value for the places that cannot await it: Medusa
3
+ * reads some `projectConfig.http` settings synchronously on every login, and
4
+ * the shop preset puts a getter backed by such a copy there, so a change in
5
+ * the Admin takes effect without a restart.
6
+ *
7
+ * - Boot: `start` loads the value once.
8
+ * - Afterwards an unref'd timer refreshes every 30 s, so a change reaches an
9
+ * idle instance too. A read that finds the copy older than 30 s (a blocked
10
+ * timer, say) also starts one refresh in the background
11
+ * (stale-while-revalidate) and still answers from the copy. A change thus
12
+ * reaches a running instance within about 30 s plus the duration of the
13
+ * read.
14
+ * - A failed read keeps the last value (initially `initial`) and is logged;
15
+ * the next attempt waits another 30 s. Neither a throwing reader nor a
16
+ * throwing logger escapes.
17
+ *
18
+ * The state lives on `globalThis` under a `Symbol.for(key)`: the preset
19
+ * (`dist`) and the Medusa-loaded plugin code (`.medusa/server`) are compiled
20
+ * separately and would otherwise each hold their own copy.
21
+ */
22
+ export declare const SNAPSHOT_REFRESH_MS = 30000;
23
+ export type RefreshingSnapshot<T> = Readonly<{
24
+ /**
25
+ * Registers the reader, loads the value once and keeps it fresh every 30 s
26
+ * (application start). `warn` is the container logger. Starting again
27
+ * replaces the reader and the timer. Never rejects.
28
+ */
29
+ start: (input: Readonly<{
30
+ read: () => Promise<T>;
31
+ warn: (message: string) => void;
32
+ }>) => Promise<void>;
33
+ /** Reads the value once; never rejects. Concurrent calls share one read. */
34
+ refresh: () => Promise<void>;
35
+ /** Stops the periodic refresh (application shutdown). The value stays. */
36
+ stop: () => void;
37
+ /** Synchronous; starts a background refresh when the copy is stale. Never throws. */
38
+ current: () => T;
39
+ /**
40
+ * Test hook: sets the value and counts as just refreshed, so it holds for
41
+ * 30 s without a reader; `null` forgets reader and value (back to `initial`).
42
+ */
43
+ setForTests: (value: T | null) => void;
44
+ }>;
45
+ export declare const createRefreshingSnapshot: <T>(options: Readonly<{
46
+ /** Global registry key; one key = one shared state per process. */
47
+ key: string;
48
+ /** Start and fallback value: used until the first read and after a failed one. */
49
+ initial: T;
50
+ /** What the value is, for the failure log. */
51
+ describe: (value: T) => string;
52
+ }>) => RefreshingSnapshot<T>;