@hartl-services/medusa-base 0.2.0 → 0.4.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 (97) hide show
  1. package/.medusa/server/src/admin/index.js +741 -36
  2. package/.medusa/server/src/admin/index.mjs +743 -38
  3. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/route.js +31 -0
  4. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/actions/[action_key]/runs/route.js +16 -0
  5. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/route.js +14 -0
  6. package/.medusa/server/src/api/admin/plugin-features/[plugin_key]/settings/route.js +22 -0
  7. package/.medusa/server/src/api/admin/plugin-features/route.js +6 -4
  8. package/.medusa/server/src/api/middlewares.js +22 -8
  9. package/.medusa/server/src/api/plugin-actions.js +221 -0
  10. package/.medusa/server/src/api/plugin-features-http.js +67 -8
  11. package/.medusa/server/src/modules/plugin-features/domain/plugin-action-runs.js +92 -0
  12. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +213 -0
  13. package/.medusa/server/src/modules/plugin-features/domain/secret-box.js +56 -0
  14. package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928205031.js +16 -0
  15. package/.medusa/server/src/modules/plugin-features/migrations/Migration20260928215428.js +17 -0
  16. package/.medusa/server/src/modules/plugin-features/models/plugin-action-run.js +43 -0
  17. package/.medusa/server/src/modules/plugin-features/models/plugin-setting-state.js +32 -0
  18. package/.medusa/server/src/modules/plugin-features/service.js +419 -17
  19. package/.medusa/server/src/public/action-progress.js +67 -0
  20. package/.medusa/server/src/public/descriptor.js +199 -2
  21. package/.medusa/server/src/public/effective-features.js +9 -5
  22. package/.medusa/server/src/public/enforcement.js +18 -8
  23. package/.medusa/server/src/public/index.js +10 -2
  24. package/.medusa/server/src/public/module-access.js +14 -0
  25. package/.medusa/server/src/public/sentry-logger.js +82 -0
  26. package/.medusa/server/src/public/settings.js +17 -0
  27. package/.medusa/server/src/public/shop-master-data.js +95 -0
  28. package/.medusa/server/src/utils/discover-complete-cart-participants.js +9 -2
  29. package/.medusa/server/src/utils/discover-plugin-features.js +47 -9
  30. package/.medusa/server/src/workflows/hooks/complete-cart-participants.js +28 -16
  31. package/README.md +416 -12
  32. package/dist/admin-components/features/plugin-features/components/PluginActionsSection.d.ts +6 -0
  33. package/dist/admin-components/features/plugin-features/components/PluginActionsSection.js +66 -0
  34. package/dist/admin-components/features/plugin-features/components/PluginDetailPage.d.ts +2 -0
  35. package/dist/admin-components/features/plugin-features/components/PluginDetailPage.js +9 -0
  36. package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.d.ts +22 -0
  37. package/dist/admin-components/features/plugin-features/components/PluginFeatureGate.js +18 -0
  38. package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.d.ts +11 -0
  39. package/dist/admin-components/features/plugin-features/components/PluginFeatureSections.js +19 -0
  40. package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.d.ts +12 -0
  41. package/dist/admin-components/features/plugin-features/components/PluginFeaturesCard.js +34 -0
  42. package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.d.ts +5 -0
  43. package/dist/admin-components/features/plugin-features/components/PluginFeaturesSettingsPage.js +16 -0
  44. package/dist/admin-components/features/plugin-features/components/PluginHubCard.d.ts +7 -0
  45. package/dist/admin-components/features/plugin-features/components/PluginHubCard.js +21 -0
  46. package/dist/admin-components/features/plugin-features/components/PluginPageShell.d.ts +13 -0
  47. package/dist/admin-components/features/plugin-features/components/PluginPageShell.js +20 -0
  48. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.d.ts +7 -0
  49. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +39 -0
  50. package/dist/admin-components/features/plugin-features/components/PluginStatusChips.d.ts +5 -0
  51. package/dist/admin-components/features/plugin-features/components/PluginStatusChips.js +8 -0
  52. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +114 -0
  53. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.js +40 -0
  54. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +392 -0
  55. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.js +128 -0
  56. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +93 -0
  57. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.js +44 -0
  58. package/dist/admin-components/features/plugin-features/lib/action-runs.d.ts +19 -0
  59. package/dist/admin-components/features/plugin-features/lib/action-runs.js +74 -0
  60. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +363 -0
  61. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.js +9 -0
  62. package/dist/admin-components/features/plugin-features/lib/plugin-paths.d.ts +4 -0
  63. package/dist/admin-components/features/plugin-features/lib/plugin-paths.js +4 -0
  64. package/dist/admin-components/features/plugin-features/lib/settings-draft.d.ts +28 -0
  65. package/dist/admin-components/features/plugin-features/lib/settings-draft.js +86 -0
  66. package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.d.ts +7 -0
  67. package/dist/admin-components/features/plugin-features/queries/pluginFeatureKeys.js +7 -0
  68. package/dist/admin-components/i18n/texts.d.ts +90 -0
  69. package/dist/admin-components/i18n/texts.js +90 -0
  70. package/dist/admin-components/index.d.ts +14 -0
  71. package/dist/admin-components/index.js +14 -0
  72. package/dist/admin-components/lib/errors.d.ts +6 -0
  73. package/dist/admin-components/lib/errors.js +27 -0
  74. package/dist/admin-components/lib/sdk.d.ts +2 -0
  75. package/dist/admin-components/lib/sdk.js +13 -0
  76. package/dist/public/action-progress.d.ts +25 -0
  77. package/dist/public/action-progress.js +66 -0
  78. package/dist/public/descriptor.d.ts +3 -2
  79. package/dist/public/descriptor.js +198 -1
  80. package/dist/public/effective-features.d.ts +2 -1
  81. package/dist/public/effective-features.js +8 -4
  82. package/dist/public/enforcement.d.ts +7 -0
  83. package/dist/public/enforcement.js +17 -7
  84. package/dist/public/index.d.ts +5 -2
  85. package/dist/public/index.js +9 -1
  86. package/dist/public/module-access.d.ts +3 -0
  87. package/dist/public/module-access.js +13 -0
  88. package/dist/public/sentry-logger.d.ts +11 -0
  89. package/dist/public/sentry-logger.js +81 -0
  90. package/dist/public/settings.d.ts +11 -0
  91. package/dist/public/settings.js +16 -0
  92. package/dist/public/shop-master-data.d.ts +25 -0
  93. package/dist/public/shop-master-data.js +94 -0
  94. package/dist/public/types.d.ts +97 -1
  95. package/dist/utils/discover-plugin-features.d.ts +28 -0
  96. package/dist/utils/discover-plugin-features.js +96 -0
  97. package/package.json +20 -4
@@ -0,0 +1,90 @@
1
+ export const texts = {
2
+ menu: {
3
+ label: "Hartl Services Plugins",
4
+ },
5
+ page: {
6
+ title: "Hartl Services Plugins",
7
+ description: "Plugins ein- oder ausschalten und öffnen, um Funktionen, Einstellungen und Aktionen zu verwalten. Änderungen gelten sofort; abgeschaltete Funktionen sind im Shop nicht erreichbar.",
8
+ },
9
+ states: {
10
+ loading: "Plugins werden geladen…",
11
+ error: "Plugins konnten nicht geladen werden.",
12
+ empty: "Keine Plugins mit Funktionsschaltern installiert.",
13
+ retry: "Erneut laden",
14
+ notFound: "Dieses Plugin ist nicht installiert.",
15
+ },
16
+ plugin: {
17
+ switchLabel: (label) => `${label} aktiv`,
18
+ open: "Öffnen",
19
+ openLabel: (label) => `${label} öffnen`,
20
+ },
21
+ chips: {
22
+ enabled: "aktiv",
23
+ disabled: "aus",
24
+ configured: "konfiguriert",
25
+ notConfigured: (labels) => `nicht konfiguriert: ${labels}`,
26
+ },
27
+ detail: {
28
+ back: "Zurück zu Hartl Services Plugins",
29
+ },
30
+ sections: {
31
+ features: "Funktionen",
32
+ settings: "Einstellungen",
33
+ actions: "Aktionen",
34
+ },
35
+ hints: {
36
+ pluginDisabled: "Das Plugin ist ausgeschaltet. Alle Funktionen sind inaktiv, ihre Einstellungen bleiben erhalten.",
37
+ parentDisabled: (parentLabel) => `Inaktiv, solange „${parentLabel}“ ausgeschaltet ist.`,
38
+ saving: "Wird gespeichert…",
39
+ },
40
+ gate: {
41
+ disabled: "Diese Funktion ist unter Einstellungen → Hartl Services Plugins ausgeschaltet.",
42
+ disabledNamed: (label) => `„${label}“ ist unter Einstellungen → Hartl Services Plugins ausgeschaltet.`,
43
+ openHub: "Zu Hartl Services Plugins",
44
+ },
45
+ settings: {
46
+ missing: (labels) => `Nicht konfiguriert: ${labels}`,
47
+ save: "Einstellungen speichern",
48
+ secretPlaceholder: "gesetzt",
49
+ secretClear: "Löschen",
50
+ secretClearPending: "Wird beim Speichern gelöscht; danach gilt der Wert aus der Konfiguration, falls vorhanden.",
51
+ secretClearUndo: "Nicht löschen",
52
+ secretClearLabel: (label) => `${label} löschen`,
53
+ secretClearUndoLabel: (label) => `${label} nicht löschen`,
54
+ invalidates: (labels) => `Beim Ändern muss ${labels} neu eingegeben werden.`,
55
+ required: "Pflichtfeld",
56
+ invalidNumber: "Bitte eine gültige Zahl eingeben.",
57
+ outOfRange: "Der Wert liegt außerhalb des erlaubten Bereichs.",
58
+ numberMin: (min) => `Mindestens ${min}.`,
59
+ numberMax: (max) => `Höchstens ${max}.`,
60
+ numberRange: (min, max) => `Zwischen ${min} und ${max}.`,
61
+ selectPlaceholder: "Bitte auswählen",
62
+ clearHint: "Ein leeres Feld speichern entfernt den gespeicherten Wert (dann gilt Konfiguration oder Standard).",
63
+ source: {
64
+ stored: "gespeichert",
65
+ config: "aus Konfiguration",
66
+ default: "Standard",
67
+ none: "nicht gesetzt",
68
+ },
69
+ },
70
+ actions: {
71
+ confirmTitle: (label) => `${label}?`,
72
+ confirm: "Ausführen",
73
+ cancel: "Abbrechen",
74
+ runningSince: (time) => `läuft seit ${time}`,
75
+ lastCompleted: (time) => `Zuletzt erfolgreich: ${time}`,
76
+ lastFailed: (time) => `Zuletzt fehlgeschlagen: ${time}`,
77
+ },
78
+ toasts: {
79
+ saved: "Einstellung wurde gespeichert.",
80
+ saveFailed: "Einstellung konnte nicht gespeichert werden.",
81
+ conflict: "Die Einstellungen wurden zwischenzeitlich geändert und neu geladen.",
82
+ actionCompleted: (label) => `„${label}“ wurde ausgeführt.`,
83
+ actionStarted: (label) => `„${label}“ wurde gestartet und läuft im Hintergrund.`,
84
+ actionFailed: (label) => `„${label}“ ist fehlgeschlagen.`,
85
+ actionAlreadyRunning: (label) => `„${label}“ läuft bereits; der Fortschritt wird angezeigt.`,
86
+ },
87
+ errors: {
88
+ unknown: "Unbekannter Fehler",
89
+ },
90
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Entry of `@hartl-services/medusa-base/admin-components` (built by
3
+ * `tsconfig.admin-components.json` into `dist/admin-components`). Not an
4
+ * Admin extension: routes and widgets live in `routes/`.
5
+ *
6
+ * Other plugins compose these into their own Admin page. Everything here
7
+ * uses the host Admin's React, React Query, React Router and `@medusajs/ui`.
8
+ */
9
+ export { PluginPageShell } from "./features/plugin-features/components/PluginPageShell";
10
+ export { PluginFeatureSections } from "./features/plugin-features/components/PluginFeatureSections";
11
+ export { PluginFeatureDisabledNotice, PluginFeatureGate, } from "./features/plugin-features/components/PluginFeatureGate";
12
+ export { usePluginFeature } from "./features/plugin-features/hooks/usePluginFeatures";
13
+ export { usePluginFeatureEnabled, type PluginFeatureEnabledState, } from "./features/plugin-features/hooks/usePluginFeatureEnabled";
14
+ export { pluginFeatureKeys } from "./features/plugin-features/queries/pluginFeatureKeys";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Entry of `@hartl-services/medusa-base/admin-components` (built by
3
+ * `tsconfig.admin-components.json` into `dist/admin-components`). Not an
4
+ * Admin extension: routes and widgets live in `routes/`.
5
+ *
6
+ * Other plugins compose these into their own Admin page. Everything here
7
+ * uses the host Admin's React, React Query, React Router and `@medusajs/ui`.
8
+ */
9
+ export { PluginPageShell } from "./features/plugin-features/components/PluginPageShell";
10
+ export { PluginFeatureSections } from "./features/plugin-features/components/PluginFeatureSections";
11
+ export { PluginFeatureDisabledNotice, PluginFeatureGate, } from "./features/plugin-features/components/PluginFeatureGate";
12
+ export { usePluginFeature } from "./features/plugin-features/hooks/usePluginFeatures";
13
+ export { usePluginFeatureEnabled, } from "./features/plugin-features/hooks/usePluginFeatureEnabled";
14
+ export { pluginFeatureKeys } from "./features/plugin-features/queries/pluginFeatureKeys";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Extracts a human-readable message from an unknown thrown value / failed
3
+ * response. Returns `undefined` when nothing usable is found so callers can
4
+ * fall back to a localized "unknown error" string instead of a hard-coded one.
5
+ */
6
+ export declare const getErrorMessage: (error: unknown) => string | undefined;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Extracts a human-readable message from an unknown thrown value / failed
3
+ * response. Returns `undefined` when nothing usable is found so callers can
4
+ * fall back to a localized "unknown error" string instead of a hard-coded one.
5
+ */
6
+ export const getErrorMessage = (error) => {
7
+ if (error instanceof Error && error.message) {
8
+ return error.message;
9
+ }
10
+ if (typeof error !== "object" || error === null) {
11
+ return undefined;
12
+ }
13
+ const candidate = error;
14
+ for (const value of [
15
+ candidate.response?.data?.message,
16
+ candidate.response?.data?.error,
17
+ candidate.body?.message,
18
+ candidate.body?.error,
19
+ candidate.message,
20
+ candidate.statusText,
21
+ ]) {
22
+ if (typeof value === "string" && value.trim()) {
23
+ return value;
24
+ }
25
+ }
26
+ return undefined;
27
+ };
@@ -0,0 +1,2 @@
1
+ import Medusa from "@medusajs/js-sdk";
2
+ export declare const sdk: Medusa;
@@ -0,0 +1,13 @@
1
+ import Medusa from "@medusajs/js-sdk";
2
+ // The Admin's vite build defines these globals; `typeof` keeps the module
3
+ // loadable where they are not defined (e.g. unit tests).
4
+ export const sdk = new Medusa({
5
+ baseUrl: (typeof __BACKEND_URL__ === "undefined" ? undefined : __BACKEND_URL__) ?? "/",
6
+ auth: {
7
+ type: (typeof __AUTH_TYPE__ === "undefined" ? undefined : __AUTH_TYPE__) ??
8
+ "session",
9
+ jwtTokenStorageKey: (typeof __JWT_TOKEN_STORAGE_KEY__ === "undefined"
10
+ ? undefined
11
+ : __JWT_TOKEN_STORAGE_KEY__) || undefined,
12
+ },
13
+ });
@@ -0,0 +1,25 @@
1
+ import type { MedusaContainer } from "@medusajs/framework/types";
2
+ import type { PluginActionProgressInput } from "./types";
3
+ /** Stored shape of a run's progress. */
4
+ export type PluginActionProgress = {
5
+ current: number;
6
+ total: number | null;
7
+ label: string | null;
8
+ };
9
+ /** Longest stored progress label; longer labels are cut. */
10
+ export declare const PLUGIN_ACTION_PROGRESS_LABEL_MAX_LENGTH = 200;
11
+ /**
12
+ * Validates and normalizes reported progress: `current` and `total` are
13
+ * non-negative integers (`total` may be unknown), a blank label is `null`
14
+ * and a long one is cut. Throws `INVALID_DATA`.
15
+ */
16
+ export declare function normalizeActionProgress(progress: PluginActionProgressInput): PluginActionProgress;
17
+ /**
18
+ * Stores the progress of a running action (shown in the Admin while it
19
+ * runs). Call it from the action's workflow with `input.run_id`. Without a
20
+ * `run_id` (e.g. the same workflow run from a script) it does nothing. An
21
+ * unknown or finished run, a failed write and invalid progress (negative
22
+ * or non-integer numbers) are logged as a warning and dropped; progress
23
+ * never fails the action.
24
+ */
25
+ export declare function reportPluginActionProgress(container: MedusaContainer, runId: string | null | undefined, progress: PluginActionProgressInput): Promise<void>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLUGIN_ACTION_PROGRESS_LABEL_MAX_LENGTH = void 0;
4
+ exports.normalizeActionProgress = normalizeActionProgress;
5
+ exports.reportPluginActionProgress = reportPluginActionProgress;
6
+ const utils_1 = require("@medusajs/framework/utils");
7
+ const module_access_1 = require("./module-access");
8
+ /** Longest stored progress label; longer labels are cut. */
9
+ exports.PLUGIN_ACTION_PROGRESS_LABEL_MAX_LENGTH = 200;
10
+ const isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
11
+ /**
12
+ * Validates and normalizes reported progress: `current` and `total` are
13
+ * non-negative integers (`total` may be unknown), a blank label is `null`
14
+ * and a long one is cut. Throws `INVALID_DATA`.
15
+ */
16
+ function normalizeActionProgress(progress) {
17
+ if (!isCount(progress?.current)) {
18
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Fortschritt: current muss eine nicht-negative ganze Zahl sein.");
19
+ }
20
+ const total = progress.total ?? null;
21
+ if (total !== null && !isCount(total)) {
22
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, "Fortschritt: total muss eine nicht-negative ganze Zahl oder null sein.");
23
+ }
24
+ const label = typeof progress.label === "string" && progress.label.trim()
25
+ ? progress.label.trim().slice(0, exports.PLUGIN_ACTION_PROGRESS_LABEL_MAX_LENGTH)
26
+ : null;
27
+ return { current: progress.current, total, label };
28
+ }
29
+ const warn = (container, message) => {
30
+ try {
31
+ container.resolve(utils_1.ContainerRegistrationKeys.LOGGER).warn(message);
32
+ }
33
+ catch {
34
+ // Progress is informational; logging must not fail the action.
35
+ }
36
+ };
37
+ /**
38
+ * Stores the progress of a running action (shown in the Admin while it
39
+ * runs). Call it from the action's workflow with `input.run_id`. Without a
40
+ * `run_id` (e.g. the same workflow run from a script) it does nothing. An
41
+ * unknown or finished run, a failed write and invalid progress (negative
42
+ * or non-integer numbers) are logged as a warning and dropped; progress
43
+ * never fails the action.
44
+ */
45
+ async function reportPluginActionProgress(container, runId, progress) {
46
+ let normalized;
47
+ try {
48
+ normalized = normalizeActionProgress(progress);
49
+ }
50
+ catch (error) {
51
+ warn(container, `Ungültiger Fortschritt${runId ? ` für Aktionslauf "${runId}"` : ""} verworfen: ${error instanceof Error ? error.message : String(error)}`);
52
+ return;
53
+ }
54
+ if (!runId) {
55
+ return;
56
+ }
57
+ try {
58
+ const updated = await (0, module_access_1.resolveBaseModule)(container).reportPluginActionRunProgress(runId, normalized);
59
+ if (!updated) {
60
+ warn(container, `Fortschritt für Aktionslauf "${runId}" verworfen: Lauf unbekannt oder nicht mehr aktiv.`);
61
+ }
62
+ }
63
+ catch (error) {
64
+ warn(container, `Fortschritt für Aktionslauf "${runId}" konnte nicht gespeichert werden: ${error instanceof Error ? error.message : String(error)}`);
65
+ }
66
+ }
@@ -1,11 +1,12 @@
1
1
  import type { PluginFeatureDescriptor } from "./types";
2
2
  /**
3
3
  * Checks key format, unique feature keys, existing parents and the single
4
- * allowed nesting level. Throws `INVALID_DATA`.
4
+ * allowed nesting level, plus the optional settings, actions and Admin page.
5
+ * Throws `INVALID_DATA`.
5
6
  */
6
7
  export declare function validatePluginFeatureDescriptor(descriptor: PluginFeatureDescriptor): void;
7
8
  /**
8
9
  * Declares a plugin's feature switches. Validates at import time so a broken
9
10
  * declaration fails the boot instead of a request.
10
11
  */
11
- export declare function definePluginFeatures<const K extends string>(descriptor: PluginFeatureDescriptor<K>): PluginFeatureDescriptor<K>;
12
+ export declare function definePluginFeatures<const K extends string, const S extends string = never, const A extends string = never>(descriptor: PluginFeatureDescriptor<K, S, A>): PluginFeatureDescriptor<K, S, A>;
@@ -4,10 +4,190 @@ exports.validatePluginFeatureDescriptor = validatePluginFeatureDescriptor;
4
4
  exports.definePluginFeatures = definePluginFeatures;
5
5
  const utils_1 = require("@medusajs/framework/utils");
6
6
  const KEY_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
7
+ const SETTING_TYPES = new Set([
8
+ "string",
9
+ "url",
10
+ "number",
11
+ "boolean",
12
+ "secret",
13
+ "select",
14
+ ]);
15
+ const ACTION_MODES = new Set(["sync", "background"]);
16
+ const ADMIN_PAGE_PREFIX = "/settings/hartl-services/";
17
+ const BASE_PAGE_PREFIX = "/settings/hartl-services/plugin/";
18
+ const ADMIN_PAGE_PATTERN = /^\/settings\/hartl-services(\/[a-z0-9][a-z0-9_-]*)+$/;
19
+ const isHttpUrl = (value) => {
20
+ try {
21
+ const url = new URL(value);
22
+ return url.protocol === "http:" || url.protocol === "https:";
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ };
7
28
  const invalid = (pluginKey, message) => new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Ungültige Funktionsschalter für Plugin "${pluginKey}": ${message}`);
29
+ const hasText = (value) => typeof value === "string" && value.trim().length > 0;
30
+ const defaultMatchesType = (setting) => {
31
+ const value = setting.default;
32
+ switch (setting.type) {
33
+ case "string":
34
+ return hasText(value);
35
+ case "url":
36
+ return typeof value === "string" && isHttpUrl(value);
37
+ case "number":
38
+ return typeof value === "number" && Number.isFinite(value);
39
+ case "boolean":
40
+ return typeof value === "boolean";
41
+ case "select":
42
+ return (typeof value === "string" &&
43
+ (setting.options ?? []).some((option) => option.value === value));
44
+ default:
45
+ return false;
46
+ }
47
+ };
48
+ const withinBounds = (setting) => typeof setting.default !== "number" ||
49
+ ((setting.min === undefined || setting.default >= setting.min) &&
50
+ (setting.max === undefined || setting.default <= setting.max));
51
+ const isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
52
+ /** `options` only (and required) for select, `min`/`max` only for number. */
53
+ const validateTypeExtras = (pluginKey, setting) => {
54
+ if (setting.type === "select") {
55
+ const options = setting.options;
56
+ if (!Array.isArray(options) || options.length === 0) {
57
+ throw invalid(pluginKey, `Auswahl "${setting.key}" braucht mindestens eine Option.`);
58
+ }
59
+ const values = new Set();
60
+ for (const option of options) {
61
+ if (!hasText(option?.value) || !hasText(option?.label)) {
62
+ throw invalid(pluginKey, `Auswahl "${setting.key}" hat eine Option ohne value oder label.`);
63
+ }
64
+ if (values.has(option.value)) {
65
+ throw invalid(pluginKey, `Auswahl "${setting.key}" hat die Option "${option.value}" doppelt.`);
66
+ }
67
+ values.add(option.value);
68
+ }
69
+ }
70
+ else if (setting.options !== undefined) {
71
+ throw invalid(pluginKey, `Einstellung "${setting.key}": options gibt es nur bei "select".`);
72
+ }
73
+ if (setting.type !== "number") {
74
+ if (setting.min !== undefined || setting.max !== undefined) {
75
+ throw invalid(pluginKey, `Einstellung "${setting.key}": min/max gibt es nur bei "number".`);
76
+ }
77
+ return;
78
+ }
79
+ for (const [name, bound] of [
80
+ ["min", setting.min],
81
+ ["max", setting.max],
82
+ ]) {
83
+ if (bound !== undefined && !isFiniteNumber(bound)) {
84
+ throw invalid(pluginKey, `Einstellung "${setting.key}": ${name} muss eine endliche Zahl sein.`);
85
+ }
86
+ }
87
+ if (setting.min !== undefined &&
88
+ setting.max !== undefined &&
89
+ setting.min > setting.max) {
90
+ throw invalid(pluginKey, `Einstellung "${setting.key}": min ist größer als max.`);
91
+ }
92
+ };
93
+ /**
94
+ * `invalidates` lists secrets of the same plugin that a change of this
95
+ * setting deletes (e.g. an API key that must not go to a new URL).
96
+ */
97
+ const validateInvalidates = (pluginKey, setting, settings) => {
98
+ const invalidates = setting.invalidates;
99
+ if (invalidates === undefined) {
100
+ return;
101
+ }
102
+ if (!Array.isArray(invalidates) || invalidates.length === 0) {
103
+ throw invalid(pluginKey, `Einstellung "${setting.key}": invalidates muss eine nicht-leere Liste sein.`);
104
+ }
105
+ const seen = new Set();
106
+ for (const key of invalidates) {
107
+ const target = settings.find((candidate) => candidate.key === key);
108
+ if (typeof key !== "string" || !target || target.type !== "secret") {
109
+ throw invalid(pluginKey, `Einstellung "${setting.key}": invalidates nennt "${String(key)}", das keine geheime Einstellung dieses Plugins ist.`);
110
+ }
111
+ if (key === setting.key || seen.has(key)) {
112
+ throw invalid(pluginKey, `Einstellung "${setting.key}": invalidates nennt "${key}" doppelt oder sich selbst.`);
113
+ }
114
+ seen.add(key);
115
+ }
116
+ };
117
+ const validateSettings = (pluginKey, settings) => {
118
+ const seen = new Set();
119
+ for (const setting of settings) {
120
+ if (!KEY_PATTERN.test(setting.key)) {
121
+ throw invalid(pluginKey, `Einstellungs-Key "${setting.key}" hat ein ungültiges Format.`);
122
+ }
123
+ if (seen.has(setting.key)) {
124
+ throw invalid(pluginKey, `Einstellungs-Key "${setting.key}" ist doppelt.`);
125
+ }
126
+ seen.add(setting.key);
127
+ if (!hasText(setting.label)) {
128
+ throw invalid(pluginKey, `Einstellung "${setting.key}" hat kein label.`);
129
+ }
130
+ if (!SETTING_TYPES.has(setting.type)) {
131
+ throw invalid(pluginKey, `Einstellung "${setting.key}" hat einen unbekannten Typ.`);
132
+ }
133
+ if (setting.required !== undefined && typeof setting.required !== "boolean") {
134
+ throw invalid(pluginKey, `Einstellung "${setting.key}": required muss boolean sein.`);
135
+ }
136
+ validateTypeExtras(pluginKey, setting);
137
+ validateInvalidates(pluginKey, setting, settings);
138
+ if (setting.default === undefined) {
139
+ continue;
140
+ }
141
+ if (setting.type === "secret") {
142
+ throw invalid(pluginKey, `Geheime Einstellung "${setting.key}" darf keinen Standardwert haben.`);
143
+ }
144
+ if (!defaultMatchesType(setting)) {
145
+ throw invalid(pluginKey, `Standardwert von Einstellung "${setting.key}" passt nicht zum Typ "${setting.type}".`);
146
+ }
147
+ if (!withinBounds(setting)) {
148
+ throw invalid(pluginKey, `Standardwert von Einstellung "${setting.key}" liegt außerhalb von min/max.`);
149
+ }
150
+ }
151
+ };
152
+ const validateActions = (pluginKey, actions) => {
153
+ const seen = new Set();
154
+ for (const action of actions) {
155
+ if (!KEY_PATTERN.test(action.key)) {
156
+ throw invalid(pluginKey, `Aktions-Key "${action.key}" hat ein ungültiges Format.`);
157
+ }
158
+ if (seen.has(action.key)) {
159
+ throw invalid(pluginKey, `Aktions-Key "${action.key}" ist doppelt.`);
160
+ }
161
+ seen.add(action.key);
162
+ if (!hasText(action.label)) {
163
+ throw invalid(pluginKey, `Aktion "${action.key}" hat kein label.`);
164
+ }
165
+ if (!hasText(action.workflow_id)) {
166
+ throw invalid(pluginKey, `Aktion "${action.key}" hat keine workflow_id.`);
167
+ }
168
+ if (!ACTION_MODES.has(action.mode)) {
169
+ throw invalid(pluginKey, `Aktion "${action.key}" hat einen unbekannten Modus.`);
170
+ }
171
+ if (action.confirm !== undefined && !hasText(action.confirm)) {
172
+ throw invalid(pluginKey, `Aktion "${action.key}": confirm ist leer.`);
173
+ }
174
+ }
175
+ };
176
+ const validateAdminPage = (pluginKey, path) => {
177
+ if (path === undefined) {
178
+ return;
179
+ }
180
+ if (typeof path !== "string" ||
181
+ !path.startsWith(ADMIN_PAGE_PREFIX) ||
182
+ path.startsWith(BASE_PAGE_PREFIX) ||
183
+ !ADMIN_PAGE_PATTERN.test(path)) {
184
+ throw invalid(pluginKey, `admin_page "${String(path)}" muss unter "${ADMIN_PAGE_PREFIX}<plugin>" liegen und darf nicht mit "${BASE_PAGE_PREFIX}" beginnen.`);
185
+ }
186
+ };
8
187
  /**
9
188
  * Checks key format, unique feature keys, existing parents and the single
10
- * allowed nesting level. Throws `INVALID_DATA`.
189
+ * allowed nesting level, plus the optional settings, actions and Admin page.
190
+ * Throws `INVALID_DATA`.
11
191
  */
12
192
  function validatePluginFeatureDescriptor(descriptor) {
13
193
  const pluginKey = String(descriptor.plugin_key);
@@ -17,6 +197,17 @@ function validatePluginFeatureDescriptor(descriptor) {
17
197
  if (typeof descriptor.label !== "string" || !descriptor.label.trim()) {
18
198
  throw invalid(pluginKey, "label fehlt.");
19
199
  }
200
+ if (descriptor.switchable !== undefined &&
201
+ typeof descriptor.switchable !== "boolean") {
202
+ throw invalid(pluginKey, "switchable muss boolean sein.");
203
+ }
204
+ if (descriptor.default_enabled !== undefined &&
205
+ typeof descriptor.default_enabled !== "boolean") {
206
+ throw invalid(pluginKey, "default_enabled muss boolean sein.");
207
+ }
208
+ if (descriptor.switchable === false && descriptor.default_enabled === false) {
209
+ throw invalid(pluginKey, "Ein nicht abschaltbares Plugin (switchable: false) kann nicht default_enabled: false haben.");
210
+ }
20
211
  const byKey = new Map();
21
212
  for (const feature of descriptor.features) {
22
213
  if (!KEY_PATTERN.test(feature.key)) {
@@ -28,6 +219,9 @@ function validatePluginFeatureDescriptor(descriptor) {
28
219
  if (typeof feature.label !== "string" || !feature.label.trim()) {
29
220
  throw invalid(pluginKey, `Feature "${feature.key}" hat kein label.`);
30
221
  }
222
+ if (typeof feature.default_enabled !== "boolean") {
223
+ throw invalid(pluginKey, `Feature "${feature.key}": default_enabled muss boolean sein.`);
224
+ }
31
225
  byKey.set(feature.key, feature);
32
226
  }
33
227
  for (const feature of descriptor.features) {
@@ -42,6 +236,9 @@ function validatePluginFeatureDescriptor(descriptor) {
42
236
  throw invalid(pluginKey, `Feature "${feature.key}" ist zu tief verschachtelt; erlaubt ist eine Ebene.`);
43
237
  }
44
238
  }
239
+ validateSettings(pluginKey, descriptor.settings ?? []);
240
+ validateActions(pluginKey, descriptor.actions ?? []);
241
+ validateAdminPage(pluginKey, descriptor.admin_page);
45
242
  }
46
243
  /**
47
244
  * Declares a plugin's feature switches. Validates at import time so a broken
@@ -12,7 +12,8 @@ export type PluginFeatureStates = {
12
12
  features: PluginFeatureState[];
13
13
  };
14
14
  /**
15
- * Configured and effective values of every declared feature.
15
+ * Configured and effective values of every declared feature. A descriptor
16
+ * with `switchable: false` is always enabled.
16
17
  * `effective(f) = pluginEnabled && configured(f) && effective(parent(f))`.
17
18
  * Stored keys the descriptor no longer declares are ignored; declared keys
18
19
  * without a stored value fall back to their default.
@@ -5,15 +5,19 @@ exports.resolveEffectivePluginFeatures = resolveEffectivePluginFeatures;
5
5
  exports.validateFeatureUpdate = validateFeatureUpdate;
6
6
  const utils_1 = require("@medusajs/framework/utils");
7
7
  /**
8
- * Configured and effective values of every declared feature.
8
+ * Configured and effective values of every declared feature. A descriptor
9
+ * with `switchable: false` is always enabled.
9
10
  * `effective(f) = pluginEnabled && configured(f) && effective(parent(f))`.
10
11
  * Stored keys the descriptor no longer declares are ignored; declared keys
11
12
  * without a stored value fall back to their default.
12
13
  */
13
14
  function resolvePluginFeatureStates(descriptor, stored) {
14
- const pluginEnabled = stored
15
- ? stored.enabled
16
- : descriptor.default_enabled ?? true;
15
+ // A plugin that cannot be switched off is always on, whatever is stored.
16
+ const pluginEnabled = descriptor.switchable === false
17
+ ? true
18
+ : stored
19
+ ? stored.enabled
20
+ : descriptor.default_enabled ?? true;
17
21
  const configured = new Map();
18
22
  for (const feature of descriptor.features) {
19
23
  const value = stored?.features[feature.key];
@@ -6,6 +6,13 @@ import type { PluginFeatureDescriptor } from "./types";
6
6
  * several keys = all must be enabled. Read failures propagate.
7
7
  */
8
8
  export declare function isPluginFeatureEnabled<K extends string>(container: MedusaContainer, descriptor: PluginFeatureDescriptor<K>, ...keys: K[]): Promise<boolean>;
9
+ /**
10
+ * Like `isPluginFeatureEnabled`, for another plugin known only by its key
11
+ * (no import of that plugin): finds its descriptor among the registered
12
+ * modules. An unknown or uninstalled plugin key is `false`, as is an
13
+ * undeclared feature key. Read failures propagate.
14
+ */
15
+ export declare function isPluginFeatureEnabledByKey(container: MedusaContainer, pluginKey: string, ...featureKeys: string[]): Promise<boolean>;
9
16
  /**
10
17
  * Middleware for `defineMiddlewares`. A disabled plugin or feature answers
11
18
  * with `NOT_FOUND` (404); all given keys must be enabled.
@@ -1,15 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isPluginFeatureEnabled = isPluginFeatureEnabled;
4
+ exports.isPluginFeatureEnabledByKey = isPluginFeatureEnabledByKey;
4
5
  exports.requirePluginFeature = requirePluginFeature;
5
6
  const utils_1 = require("@medusajs/framework/utils");
6
- const constants_1 = require("./constants");
7
- const resolveReader = (container) => {
8
- if (!container.hasRegistration(constants_1.PLUGIN_FEATURES_MODULE)) {
9
- throw new utils_1.MedusaError(utils_1.MedusaError.Types.UNEXPECTED_STATE, "@hartl-services/medusa-base ist nicht in medusa-config registriert.");
10
- }
11
- return container.resolve(constants_1.PLUGIN_FEATURES_MODULE);
12
- };
7
+ const discover_plugin_features_1 = require("../utils/discover-plugin-features");
8
+ const module_access_1 = require("./module-access");
9
+ const resolveReader = (container) => (0, module_access_1.resolveBaseModule)(container);
13
10
  const allEnabled = (effective, keys) => effective.enabled && keys.every((key) => effective.features[key] === true);
14
11
  /**
15
12
  * For subscribers, jobs and workflow steps. No keys = plugin switch only;
@@ -19,6 +16,19 @@ async function isPluginFeatureEnabled(container, descriptor, ...keys) {
19
16
  const effective = await resolveReader(container).retrieveEffectiveFeatures(descriptor);
20
17
  return allEnabled(effective, keys);
21
18
  }
19
+ /**
20
+ * Like `isPluginFeatureEnabled`, for another plugin known only by its key
21
+ * (no import of that plugin): finds its descriptor among the registered
22
+ * modules. An unknown or uninstalled plugin key is `false`, as is an
23
+ * undeclared feature key. Read failures propagate.
24
+ */
25
+ async function isPluginFeatureEnabledByKey(container, pluginKey, ...featureKeys) {
26
+ const descriptor = (0, discover_plugin_features_1.findPluginFeatureDescriptor)(container, pluginKey);
27
+ if (!descriptor) {
28
+ return false;
29
+ }
30
+ return isPluginFeatureEnabled(container, descriptor, ...featureKeys);
31
+ }
22
32
  // Keyed by the request object, so one request reads each plugin's toggles once
23
33
  // even when several gate middlewares match it.
24
34
  const requestMemo = new WeakMap();
@@ -1,6 +1,9 @@
1
1
  export { PLUGIN_FEATURES_MODULE } from "./constants";
2
2
  export { definePluginFeatures } from "./descriptor";
3
3
  export { resolveEffectivePluginFeatures } from "./effective-features";
4
- export { isPluginFeatureEnabled, requirePluginFeature } from "./enforcement";
5
- export type { EffectivePluginFeatures, PluginFeatureDefinition, PluginFeatureDescriptor, PluginFeatureProvider, } from "./types";
4
+ export { isPluginFeatureEnabled, isPluginFeatureEnabledByKey, requirePluginFeature, } from "./enforcement";
5
+ export { getPluginSettings } from "./settings";
6
+ export { getShopMasterData, shopMasterDataDescriptor, type ResolvedShopMasterData, type ShopMasterData, type ShopMasterDataKey, } from "./shop-master-data";
7
+ export { reportPluginActionProgress, type PluginActionProgress, } from "./action-progress";
8
+ export type { BasePluginOptions, EffectivePluginFeatures, PluginActionDefinition, PluginActionMode, PluginActionProgressInput, PluginActionWorkflowInput, PluginFeatureDefinition, PluginFeatureDescriptor, PluginFeatureProvider, PluginSettingDefinition, PluginSettingOption, PluginSettingsFallback, PluginSettingType, PluginSettingValue, ResolvedPluginSettings, } from "./types";
6
9
  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.requirePluginFeature = exports.isPluginFeatureEnabled = exports.resolveEffectivePluginFeatures = exports.definePluginFeatures = exports.PLUGIN_FEATURES_MODULE = void 0;
3
+ exports.reportPluginActionProgress = exports.shopMasterDataDescriptor = exports.getShopMasterData = exports.getPluginSettings = exports.requirePluginFeature = exports.isPluginFeatureEnabledByKey = exports.isPluginFeatureEnabled = 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");
@@ -9,4 +9,12 @@ var effective_features_1 = require("./effective-features");
9
9
  Object.defineProperty(exports, "resolveEffectivePluginFeatures", { enumerable: true, get: function () { return effective_features_1.resolveEffectivePluginFeatures; } });
10
10
  var enforcement_1 = require("./enforcement");
11
11
  Object.defineProperty(exports, "isPluginFeatureEnabled", { enumerable: true, get: function () { return enforcement_1.isPluginFeatureEnabled; } });
12
+ Object.defineProperty(exports, "isPluginFeatureEnabledByKey", { enumerable: true, get: function () { return enforcement_1.isPluginFeatureEnabledByKey; } });
12
13
  Object.defineProperty(exports, "requirePluginFeature", { enumerable: true, get: function () { return enforcement_1.requirePluginFeature; } });
14
+ var settings_1 = require("./settings");
15
+ Object.defineProperty(exports, "getPluginSettings", { enumerable: true, get: function () { return settings_1.getPluginSettings; } });
16
+ var shop_master_data_1 = require("./shop-master-data");
17
+ Object.defineProperty(exports, "getShopMasterData", { enumerable: true, get: function () { return shop_master_data_1.getShopMasterData; } });
18
+ Object.defineProperty(exports, "shopMasterDataDescriptor", { enumerable: true, get: function () { return shop_master_data_1.shopMasterDataDescriptor; } });
19
+ var action_progress_1 = require("./action-progress");
20
+ Object.defineProperty(exports, "reportPluginActionProgress", { enumerable: true, get: function () { return action_progress_1.reportPluginActionProgress; } });
@@ -0,0 +1,3 @@
1
+ import type { MedusaContainer } from "@medusajs/framework/types";
2
+ /** Resolves the base module or fails with a clear `UNEXPECTED_STATE`. */
3
+ export declare const resolveBaseModule: <T>(container: MedusaContainer) => T;