@hartl-services/medusa-base 2.7.0 → 2.9.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 (54) hide show
  1. package/.medusa/server/src/admin/admin-components/features/plugin-features/components/PluginSettingsForm.js +12 -9
  2. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +5 -1
  3. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +20 -4
  4. package/.medusa/server/src/admin/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +5 -1
  5. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +20 -4
  6. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.d.ts +0 -5
  7. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/plugin-view.js +0 -5
  8. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.d.ts +4 -0
  9. package/.medusa/server/src/admin/admin-components/features/plugin-features/lib/settings-layout.js +4 -0
  10. package/.medusa/server/src/admin/admin-components/i18n/texts.d.ts +1 -2
  11. package/.medusa/server/src/admin/admin-components/i18n/texts.js +1 -2
  12. package/.medusa/server/src/admin/index.js +142 -87
  13. package/.medusa/server/src/admin/index.mjs +144 -89
  14. package/.medusa/server/src/api/admin/email/templates/[kind]/send-test/route.js +7 -1
  15. package/.medusa/server/src/customer-auth/plugin-features.js +10 -10
  16. package/.medusa/server/src/email/application/warn-credential-expiry.js +77 -0
  17. package/.medusa/server/src/email/config/index.js +8 -2
  18. package/.medusa/server/src/email/config/notification-module.js +28 -2
  19. package/.medusa/server/src/email/domain/business-keys.js +5 -2
  20. package/.medusa/server/src/email/domain/failure-classification.js +9 -1
  21. package/.medusa/server/src/email/plugin-features.js +123 -1
  22. package/.medusa/server/src/email/rendering/default-templates.js +16 -1
  23. package/.medusa/server/src/email/rendering/render-email.js +2 -1
  24. package/.medusa/server/src/email/types/email-kinds.js +2 -1
  25. package/.medusa/server/src/email/types/template-data.js +8 -1
  26. package/.medusa/server/src/jobs/email-credential-expiry.js +27 -0
  27. package/.medusa/server/src/modules/email/domain/message-state.js +3 -1
  28. package/.medusa/server/src/modules/email/migrations/Migration20261011120000.js +20 -0
  29. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +27 -6
  30. package/.medusa/server/src/modules/plugin-features/service.js +4 -3
  31. package/.medusa/server/src/providers/smtp/delivery-settings.js +75 -0
  32. package/.medusa/server/src/providers/smtp/graph-transport.js +150 -0
  33. package/.medusa/server/src/providers/smtp/service.js +105 -37
  34. package/.medusa/server/src/providers/smtp/types.js +7 -1
  35. package/.medusa/server/src/public/descriptor.js +24 -3
  36. package/.medusa/server/src/shared/lib/dates.js +8 -2
  37. package/.medusa/server/src/shop-config/build-food-shop-config.js +3 -5
  38. package/README.md +100 -65
  39. package/dist/customer-auth/plugin-features.js +9 -9
  40. package/dist/email/config/index.d.ts +6 -1
  41. package/dist/email/config/index.js +7 -1
  42. package/dist/email/config/notification-module.d.ts +25 -0
  43. package/dist/email/config/notification-module.js +27 -1
  44. package/dist/email/types/email-kinds.d.ts +2 -1
  45. package/dist/email/types/email-kinds.js +1 -0
  46. package/dist/email/types/template-data.d.ts +10 -0
  47. package/dist/email/types/template-data.js +7 -0
  48. package/dist/public/descriptor.js +23 -2
  49. package/dist/public/types.d.ts +14 -4
  50. package/dist/shared/lib/dates.d.ts +2 -0
  51. package/dist/shared/lib/dates.js +7 -1
  52. package/dist/shop-config/build-food-shop-config.d.ts +1 -1
  53. package/dist/shop-config/build-food-shop-config.js +2 -4
  54. package/package.json +2 -2
package/README.md CHANGED
@@ -50,7 +50,7 @@ packing lists) and S3 file storage, and the turnkey `medusa-config.ts` preset
50
50
  `@hartl-services/medusa-base/config` and Sentry's `register()` from
51
51
  `@hartl-services/medusa-base/instrumentation` (see "Config helpers" and
52
52
  "Sentry instrumentation").
53
- - Sends the shop's transactional e-mails over SMTP (module `email`, see
53
+ - Sends the shop's transactional e-mails over SMTP or Microsoft 365, chosen in the Admin (module `email`, see
54
54
  "E-Mail"), issues invoices, cancellation documents and packing lists as
55
55
  private PDFs (module `documents`, see "Dokumente") and stores files in
56
56
  S3-compatible storage behind the `/files` gateway (see "Dateispeicher (S3)").
@@ -234,13 +234,18 @@ All optional; the Admin response always carries them (`null`, `[]` or
234
234
  unchanged, e.g. by the plugin switch of a switchable plugin). The Admin locks
235
235
  the switch of the last enabled top-level feature with a hint. The Admin DTO
236
236
  field is `require_enabled_feature: boolean` (`false` when not declared).
237
- - `setting_groups`: `{ name, description?, note?, requires_feature? }` per
237
+ - `setting_groups`: `{ name, description?, note?, requires_feature?, visible_when? }` per
238
238
  settings group. `name` equals the `group` of at least one setting and is
239
239
  unique; `requires_feature` is a declared feature key, the Admin dims the group
240
240
  (still editable) while that feature is not effectively enabled.
241
+ `visible_when: { setting, equals }` names a `select` or `boolean` setting
242
+ outside the group and one of its values: the group applies only while that
243
+ setting has the value. The Admin hides it otherwise (by the form's current
244
+ value), and its required settings do not count as `missing`; stored values
245
+ are kept. The Admin DTO carries `visible_when` or `null`.
241
246
  - Per setting: `hint` (short text under the field, shown instead of
242
247
  `description`), `placeholder` (not for `boolean`, `select`, `image`,
243
- `color`), `width` (`"full"` default or `"half"`: consecutive half settings
248
+ `color`, `date`), `width` (`"full"` default or `"half"`: consecutive half settings
244
249
  share a two-column row; not for `boolean`) and `section` (sub-heading inside
245
250
  the group, shared by consecutive settings with the same value).
246
251
 
@@ -338,7 +343,7 @@ in-memory copy of the toggle (see "Kontobestätigung per E-Mail").
338
343
 
339
344
  Configuration a plugin reads at RUN time (for example an external system's
340
345
  URL and API key) is declared in the descriptor and edited on the plugin's
341
- Admin page. Boot-time provider configuration (SMTP, S3, Sentry, Redis) and
346
+ Admin page. Boot-time provider configuration (S3, Sentry, Redis) and
342
347
  function-valued options stay in `medusa-config.ts`.
343
348
 
344
349
  ```ts
@@ -383,7 +388,8 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
383
388
  through Medusa's `/admin/uploads` into the public File Module storage and
384
389
  previews; replacing or removing it leaves the old file in storage), `color`
385
390
  (a hex color `#rrggbb`, stored lowercase; the Admin shows a hex field with a
386
- color picker and checks the format before saving). An optional `group`
391
+ color picker and checks the format before saving), `date` (a calendar day
392
+ `YYYY-MM-DD`; the Admin shows `AdminDatePicker`). An optional `group`
387
393
  (non-empty text) is a section heading: the Admin starts a new section with
388
394
  that heading whenever the group changes from one setting to the next. Keys
389
395
  follow the feature key format and are unique. A `secret` has no default;
@@ -394,8 +400,8 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
394
400
  `api_url` so a stored key is never sent to a new host. `definePluginFeatures`
395
401
  throws at import time otherwise (also for an `invalidates` entry that is not
396
402
  a secret of the plugin). The Admin renders a `select` as a dropdown, gives
397
- number inputs their bounds and shows "Beim Ändern muss … neu eingegeben
398
- werden" under a setting with `invalidates`.
403
+ number inputs their bounds and shows "Wird gelöscht, wenn … sich ändert"
404
+ under each secret named in an `invalidates`.
399
405
  - Precedence per key: stored in the Admin > `pluginSettingsFallback`
400
406
  (medusa-config) > declared default > not set. A value that does not fit the
401
407
  declared type is skipped. A plugin is `configured` when every required
@@ -1326,7 +1332,7 @@ have no dependency on Medusa; the subpath does not load `@sentry/node`.
1326
1332
  | `resolvePublicOrigin(value, name, allowHttp)`, `shopUrl(origin, path)` | Validate a bare HTTP(S) origin; build an absolute URL that cannot leave the origin |
1327
1333
  | `resolveStripe(values)` | Checks the `sk_` key and the `whsec_` webhook secret |
1328
1334
  | `resolveRedisEventBus(environment, phase)` | `{ url } \| undefined`: optional in development, required at runtime elsewhere, never used in a build, authenticated `rediss://` in production |
1329
- | `createSmtpNotificationModuleConfig`, `resolveSmtp`, `SMTP_ENVIRONMENT_VARIABLES`, `SMTP_BUILD_PLACEHOLDERS`, `defineShopEmailConfiguration` | SMTP notification provider and E-Mail configuration (see "E-Mail") |
1335
+ | `createEmailNotificationModuleConfig`, `defineShopEmailConfiguration` (deprecated: `createSmtpNotificationModuleConfig`, `resolveSmtp`, `SMTP_ENVIRONMENT_VARIABLES`, `SMTP_BUILD_PLACEHOLDERS`) | Mail notification provider and E-Mail configuration (see "E-Mail") |
1330
1336
  | `createS3CacheStorageConfig`, `createS3CacheFileModuleConfig`, `resolveS3Credentials`, `resolveExpirationStrategy`, `S3_BUILD_PLACEHOLDERS` | S3 file storage (see "Dateispeicher (S3)") |
1331
1337
  | `createDefaultDocumentHtmlFactory`, `DOCUMENTS_BUILD_PLACEHOLDERS` | Documents markup and build placeholder (see "Dokumente") |
1332
1338
 
@@ -1373,36 +1379,32 @@ this export (or `sentry-logger`) lists `@sentry/node` in its own
1373
1379
  ## E-Mail
1374
1380
 
1375
1381
  The base owns transactional e-mail for the shop: logical email messages,
1376
- attempts, rendering, the Medusa Notification integration, SMTP transport,
1377
- retries and e-mail-specific Admin operations. It is the `email` module, its
1378
- SMTP notification provider (`providers/smtp`), its subscribers, jobs and
1382
+ attempts, rendering, the Medusa Notification integration, the SMTP and
1383
+ Microsoft 365 transports, retries and e-mail-specific Admin operations. It is
1384
+ the `email` module, its notification provider (`providers/smtp`, id `smtp`),
1385
+ its subscribers, jobs and
1379
1386
  workflows, and the Admin page `/settings/hartl-services/email`. There is no
1380
1387
  separate e-mail package since 1.0.0.
1381
1388
 
1382
1389
  ### Configuration
1383
1390
 
1384
- The SMTP notification provider is a `modules` entry built by
1385
- `createSmtpNotificationModuleConfig` (from
1386
- `@hartl-services/medusa-base/config`); the branding and rendering
1391
+ The notification provider is a `modules` entry built by
1392
+ `createEmailNotificationModuleConfig` (from
1393
+ `@hartl-services/medusa-base/config`); which service it sends through and the
1394
+ credentials are set in the Admin (see "Versand"). The branding and rendering
1387
1395
  configuration goes into the `email` namespace of the base options:
1388
1396
 
1389
1397
  ```ts
1390
- import {
1391
- createSmtpNotificationModuleConfig,
1392
- resolveSmtp,
1393
- SMTP_ENVIRONMENT_VARIABLES,
1394
- } from "@hartl-services/medusa-base/config";
1395
-
1396
- // Validates the seven SMTP_* variables (see "SMTP environment helpers").
1397
- const smtp = resolveSmtp(
1398
- Object.fromEntries(
1399
- SMTP_ENVIRONMENT_VARIABLES.map((name) => [name, process.env[name] ?? ""]),
1400
- ) as Record<(typeof SMTP_ENVIRONMENT_VARIABLES)[number], string>,
1401
- process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test",
1402
- );
1398
+ import { createEmailNotificationModuleConfig } from "@hartl-services/medusa-base/config";
1403
1399
 
1404
1400
  export default defineConfig({
1405
- modules: [createSmtpNotificationModuleConfig(smtp)],
1401
+ modules: [
1402
+ createEmailNotificationModuleConfig({
1403
+ // Permits "Verschlüsselung: Keine" (a local SMTP server); never in production.
1404
+ allowInsecure:
1405
+ process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test",
1406
+ }),
1407
+ ],
1406
1408
  plugins: [
1407
1409
  {
1408
1410
  resolve: "@hartl-services/medusa-base",
@@ -1456,24 +1458,73 @@ affiliate-reminder and partner-application e-mails come from
1456
1458
  `@hartl-services/medusa-food-supplements-contracts/events/documents` and
1457
1459
  `/events/affiliate`, so installing the base does not pull in Affiliate.
1458
1460
 
1459
- Environment variables (all seven required outside development/test; at least
1460
- one of `SMTP_SECURE`/`SMTP_REQUIRE_TLS` must be `true` outside
1461
- development/test):
1462
-
1463
- ```text
1464
- SMTP_HOST=
1465
- SMTP_PORT=
1466
- SMTP_SECURE=
1467
- SMTP_REQUIRE_TLS=
1468
- SMTP_USER=
1469
- SMTP_PASSWORD=
1470
- SMTP_FROM=
1471
- ```
1461
+ The provider reads no environment variables (since 2.9.0; the former
1462
+ `SMTP_*` variables are ignored).
1472
1463
 
1473
1464
  Set `REDIS_URL` to use `@medusajs/event-bus-redis` instead of Medusa's local
1474
1465
  Event Bus; production requires an authenticated `rediss://` URL. See "Event
1475
1466
  Bus" below for retry/backoff behavior.
1476
1467
 
1468
+ ### Versand
1469
+
1470
+ The page E-Mail → "Versand" (`/settings/hartl-services/email/delivery`, linked
1471
+ under "Verwaltung") edits the plugin's settings. It stays reachable while the
1472
+ E-Mail plugin is off, because the login-code mail is still sent then.
1473
+
1474
+ | Key | Type | Required |
1475
+ |---|---|---|
1476
+ | `provider` (Versand über, group "Anbieter") | select `smtp` \| `microsoft365` | yes |
1477
+ | `smtp_host`, `smtp_port` (default 587), `smtp_security` (`starttls` default, `tls`, `none`), `smtp_from` (Absender, `Name <address>` allowed) | string / number / select / string | yes, while SMTP |
1478
+ | `smtp_user`, `smtp_password` (secret) | string / secret | no (no login without a user) |
1479
+ | `microsoft_tenant_id`, `microsoft_client_id`, `microsoft_client_secret` (secret), `microsoft_client_secret_expires_on` (date), `microsoft_mailbox` (Postfach) | string / secret / date | yes, while Microsoft 365 |
1480
+
1481
+ The groups "SMTP" and "Microsoft 365" are shown and required only for the
1482
+ selected provider (`visible_when`). Changing the host deletes the SMTP
1483
+ password; changing tenant or client id deletes the client secret.
1484
+
1485
+ The provider reads the settings on every send, never at boot; a shop without
1486
+ them boots and its mails fail as `not_configured` (retried like a connection
1487
+ failure, then `failed`; a log warning names the missing keys). `none`
1488
+ encryption is accepted only with `allowInsecure`. A rejected login (SMTP
1489
+ `EAUTH`, a Graph token or 401/403 error) is `auth_failed` and not retried.
1490
+ The test mail of the template editor shows both as their own message.
1491
+
1492
+ **Microsoft 365** sends with Microsoft Graph `POST /users/{mailbox}/sendMail`
1493
+ and an app-only token (client credentials, cached in memory per tenant,
1494
+ client and secret). Graph has one body, so the mail goes out as HTML; it is
1495
+ saved to the mailbox's Sent Items. A request above 4 MB (large PDF
1496
+ attachments) fails as `invalid_content`. 429 and 5xx are temporary, other 4xx
1497
+ permanent. Setup:
1498
+
1499
+ 1. Microsoft Entra admin center → App registrations → New registration
1500
+ (single tenant, no redirect URI). Note the Application (client) ID and
1501
+ Directory (tenant) ID.
1502
+ 2. Certificates & secrets → New client secret (at most 24 months); copy the
1503
+ value and its expiry date.
1504
+ 3. Do **not** grant the Graph application permission `Mail.Send` in Entra: it
1505
+ would allow sending as every mailbox. Grant it in Exchange Online, limited
1506
+ to the shop's mailbox (RBAC for Applications), in Exchange Online
1507
+ PowerShell:
1508
+
1509
+ ```powershell
1510
+ New-ServicePrincipal -AppId <client-id> -ObjectId <enterprise-app-object-id> -DisplayName "Shop Mail"
1511
+ New-ManagementScope -Name "Shop Mailbox" -RecipientRestrictionFilter "PrimarySmtpAddress -eq 'shop@example.com'"
1512
+ New-ManagementRoleAssignment -App <client-id> -Role "Application Mail.Send" -CustomResourceScope "Shop Mailbox"
1513
+ ```
1514
+
1515
+ (`<enterprise-app-object-id>` is the object id under Enterprise
1516
+ applications, not the one of the app registration.)
1517
+ 4. In the Admin: E-Mail → Versand → Anbieter "Microsoft 365", enter tenant
1518
+ id, client id, secret, its expiry date and the mailbox; send a test mail
1519
+ from a template.
1520
+
1521
+ Seven days before "Secret gültig bis" (shop time zone) the daily job
1522
+ `email-credential-expiry` (07:00 server time, while the E-Mail plugin is on
1523
+ and Microsoft 365 is selected) sends the mail `system.credential_expiry.admin`
1524
+ to the e-mail of the Shop-Stammdaten, once per client id and expiry date (a
1525
+ log warning when the Shop-Stammdaten have no e-mail). A new secret with a new
1526
+ date warns again.
1527
+
1477
1528
  ### Feature toggles
1478
1529
 
1479
1530
  The E-Mail module declares its toggles to `@hartl-services/medusa-base` under the
@@ -1996,30 +2047,14 @@ configuration (blank `displayName`, a non-function `layout` or renderer, an
1996
2047
  unknown renderer kind) fails at startup and not at the first send. Custom
1997
2048
  renderers must escape dynamic values in `bodyHtml` themselves.
1998
2049
 
1999
- ### SMTP environment helpers
2000
-
2001
- `@hartl-services/medusa-base/config` exports helpers for the SMTP
2002
- deployment variables:
2003
-
2004
- - `SMTP_ENVIRONMENT_VARIABLES` — the seven variable names.
2005
- - `SMTP_BUILD_PLACEHOLDERS` — placeholder values for all seven variables so
2006
- `medusa build` can load `medusa-config.ts` without deployment secrets. Use
2007
- them only while building.
2008
- - `resolveSmtp(values, allowInsecure)` — validates the values and returns the
2009
- `ResolvedSmtpConfiguration` for `createSmtpNotificationModuleConfig`. It
2010
- rejects a port outside 1–65535, a non-`"true"`/`"false"` boolean and an
2011
- invalid `SMTP_FROM`. `allowInsecure` permits plain SMTP (development, test,
2012
- build); otherwise `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`.
2013
-
2014
- Runtime requires `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE`, `SMTP_REQUIRE_TLS`,
2015
- `SMTP_USER`, `SMTP_PASSWORD`, and `SMTP_FROM`. `SMTP_FROM` is a bare address
2016
- or carries the sender's display name: `Example GmbH <info@example.com>` (quote
2017
- the value in a `.env` file). `SMTP_SECURE=true` selects
2018
- implicit TLS; `SMTP_REQUIRE_TLS=true` requires STARTTLS. Outside development
2019
- and test, at least one must be true. Certificate verification is never
2020
- disabled. Development and test may set both to `false` for an isolated local
2021
- SMTP server. SMTP endpoints and credentials never belong in the shop
2022
- configuration.
2050
+ ### Deprecated SMTP helpers
2051
+
2052
+ `createSmtpNotificationModuleConfig(smtp)`, `resolveSmtp`,
2053
+ `SMTP_ENVIRONMENT_VARIABLES` and `SMTP_BUILD_PLACEHOLDERS` remain exported
2054
+ for shops that still read `SMTP_*` in their `medusa-config.ts`. The values
2055
+ given to `createSmtpNotificationModuleConfig` only fill SMTP settings that are
2056
+ not stored in the Admin (the Admin status does not count them as configured).
2057
+ Use `createEmailNotificationModuleConfig` and the "Versand" page instead.
2023
2058
 
2024
2059
  ### Delivery Semantics
2025
2060
 
@@ -7,32 +7,32 @@ const PASSWORD_RULES = "Passwort-Regeln";
7
7
  exports.customerAuthPluginFeatures = (0, descriptor_1.definePluginFeatures)({
8
8
  plugin_key: customer_auth_1.CUSTOMER_AUTH_PLUGIN_KEY,
9
9
  label: "Kunden-Login",
10
- description: "Welche Anmeldemethoden Kunden im Shop nutzen können. Mindestens eine Methode muss eingeschaltet bleiben, sonst kann sich niemand mehr anmelden; Admin-Benutzer melden sich unabhängig davon mit E-Mail und Passwort an. Eine Änderung wirkt innerhalb von etwa 30 Sekunden im Backend, die Storefront übernimmt sie nach ihrem Zwischenspeicher (etwa eine Minute). Die Passwort-Regeln gelten nur für Kunden, solange „E-Mail + Passwort“ eingeschaltet ist; Admin-Benutzer behalten mindestens 15 Zeichen, die bei der ersten Anmeldung eines noch nicht angelegten Admin-Benutzers (Einladung) geprüft werden.",
10
+ description: "Wie sich Kunden anmelden. Mindestens eine Methode bleibt eingeschaltet; Admin-Benutzer melden sich immer mit E-Mail und Passwort an.",
11
11
  summary: "Anmeldemethoden und Passwort-Regeln für Kunden.",
12
12
  icon: "ShieldCheck",
13
13
  switchable: false,
14
14
  features_heading: "Anmeldemethoden",
15
- features_note: "Wirkt nach ca. 30 Sekunden im Backend, in der Storefront nach ca. 1 Minute.",
15
+ features_note: "Wirkt nach ca. 1 Minute.",
16
16
  all_off_warning: "Keine Methode eingeschaltet: Kunden können sich nicht mehr anmelden.",
17
17
  require_enabled_feature: true,
18
18
  features: [
19
19
  {
20
20
  key: customer_auth_1.CUSTOMER_AUTH_FEATURES.emailPassword,
21
21
  label: "E-Mail + Passwort",
22
- description: "Anmeldung, Registrierung und Passwort-Zurücksetzen. Aus: Das Backend lehnt diese Wege für Kunden ab; bestehende Konten melden sich dann nur noch mit den anderen eingeschalteten Methoden an.",
22
+ description: "Anmeldung, Registrierung und Passwort zurücksetzen.",
23
23
  default_enabled: true,
24
24
  },
25
25
  {
26
26
  key: customer_auth_1.CUSTOMER_AUTH_FEATURES.emailOtp,
27
27
  label: "Anmeldecode per E-Mail",
28
- description: "Kunden fordern einen sechsstelligen Code an, der 10 Minuten gültig ist, und melden sich damit an; für neue Adressen entsteht dabei ein Kundenkonto. Ein bestehendes Passwort-Konto derselben Adresse wird nur verknüpft, wenn die Passwort-Anmeldung aus ist oder die Adresse bestätigt wurde. Die Code-Mail geht unabhängig von den Schaltern des E-Mail-Plugins hinaus. Aus: Code-Anfrage und Code-Anmeldung sind nicht möglich.",
28
+ description: "Anmeldung mit einem 6-stelligen Code per E-Mail; für neue Adressen entsteht ein Konto.",
29
29
  default_enabled: false,
30
30
  },
31
31
  ],
32
32
  setting_groups: [
33
33
  {
34
34
  name: PASSWORD_RULES,
35
- description: "Gelten nur für Kunden mit „E-Mail + Passwort“, bei Registrierung und Passwort-Zurücksetzen. Admin-Benutzer behalten mindestens 15 Zeichen.",
35
+ description: "Für Kunden mit „E-Mail + Passwort“. Admin-Benutzer: mindestens 15 Zeichen.",
36
36
  requires_feature: customer_auth_1.CUSTOMER_AUTH_FEATURES.emailPassword,
37
37
  },
38
38
  ],
@@ -40,8 +40,8 @@ exports.customerAuthPluginFeatures = (0, descriptor_1.definePluginFeatures)({
40
40
  {
41
41
  key: "password_min_length",
42
42
  label: "Mindestlänge",
43
- description: "Wie viele Zeichen ein Kundenpasswort mindestens haben muss (8 bis 128). NIST SP 800-63B Rev. 4, die US-Richtlinie für digitale Identität, empfiehlt mindestens 15 Zeichen, wenn das Passwort der einzige Faktor ist. Die Regeln gelten nur, solange „E-Mail + Passwort“ eingeschaltet ist, und nur für Kunden; Admin-Benutzer behalten mindestens 15 Zeichen. Bestehende Passwörter bleiben gültig, die Regeln greifen bei Registrierung und Passwort-Zurücksetzen. Grenze: Wer sich über den Admin-Weg registriert, kann 15 Zeichen ohne Zusammensetzungsregeln wählen, auch wenn der Shop mehr verlangt; das schwächt nur das eigene Passwort.",
44
- hint: "8 bis 128 Zeichen. Empfohlen: mindestens 15 (NIST SP 800-63B).",
43
+ description: "Gilt bei Registrierung und Passwort-Zurücksetzen; bestehende Passwörter bleiben gültig.",
44
+ hint: "8 bis 128 Zeichen. Empfohlen: 15 (NIST SP 800-63B).",
45
45
  group: PASSWORD_RULES,
46
46
  width: "half",
47
47
  type: "number",
@@ -54,7 +54,7 @@ exports.customerAuthPluginFeatures = (0, descriptor_1.definePluginFeatures)({
54
54
  key: "password_require_mixed_case",
55
55
  label: "Groß- und Kleinbuchstaben verlangen",
56
56
  description: "Mindestens ein Groß- und ein Kleinbuchstabe.",
57
- hint: "NIST rät von Zusammensetzungsregeln ab: Sie führen zu vorhersehbaren Passwörtern wie „Passwort1!“. Länge schützt besser.",
57
+ hint: "NIST rät von Zeichenregeln ab, Länge schützt besser.",
58
58
  group: PASSWORD_RULES,
59
59
  type: "boolean",
60
60
  default: false,
@@ -70,7 +70,7 @@ exports.customerAuthPluginFeatures = (0, descriptor_1.definePluginFeatures)({
70
70
  {
71
71
  key: "password_require_symbol",
72
72
  label: "Sonderzeichen verlangen",
73
- description: "Mindestens ein Sonderzeichen (kein Buchstabe, keine Ziffer, kein Leerzeichen).",
73
+ description: "Mindestens ein Sonderzeichen.",
74
74
  group: PASSWORD_RULES,
75
75
  type: "boolean",
76
76
  default: false,
@@ -1,15 +1,20 @@
1
1
  import type { ResolvedSmtpConfiguration, ShopEmailConfiguration } from "../types";
2
- export { createSmtpNotificationModuleConfig } from "./notification-module";
2
+ export { createEmailNotificationModuleConfig, createSmtpNotificationModuleConfig, } from "./notification-module";
3
3
  export declare const defineShopEmailConfiguration: <Configuration extends ShopEmailConfiguration>(configuration: Configuration) => Configuration;
4
4
  export type { ResolvedSmtpConfiguration, ShopEmailConfiguration, } from "../types";
5
+ /** @deprecated The provider is set in the Admin (E-Mail > Versand). */
5
6
  export declare const SMTP_ENVIRONMENT_VARIABLES: readonly ["SMTP_HOST", "SMTP_PORT", "SMTP_SECURE", "SMTP_USER", "SMTP_PASSWORD", "SMTP_FROM", "SMTP_REQUIRE_TLS"];
6
7
  /**
8
+ * @deprecated The provider is set in the Admin (E-Mail > Versand).
9
+ *
7
10
  * `medusa build` loads the configuration without deployment secrets. These
8
11
  * placeholders only satisfy the required-variable checks of a build; they
9
12
  * never reach a running server.
10
13
  */
11
14
  export declare const SMTP_BUILD_PLACEHOLDERS: Readonly<Record<string, string>>;
12
15
  /**
16
+ * @deprecated The provider is set in the Admin (E-Mail > Versand).
17
+ *
13
18
  * Validates the SMTP environment values and returns the transport
14
19
  * configuration. `allowInsecure` permits plain SMTP for development, tests
15
20
  * and builds; otherwise `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`.
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveSmtp = exports.SMTP_BUILD_PLACEHOLDERS = exports.SMTP_ENVIRONMENT_VARIABLES = exports.defineShopEmailConfiguration = exports.createSmtpNotificationModuleConfig = void 0;
3
+ exports.resolveSmtp = exports.SMTP_BUILD_PLACEHOLDERS = exports.SMTP_ENVIRONMENT_VARIABLES = exports.defineShopEmailConfiguration = exports.createSmtpNotificationModuleConfig = exports.createEmailNotificationModuleConfig = void 0;
4
4
  const config_1 = require("../../public/config");
5
5
  const sender_1 = require("../domain/sender");
6
6
  var notification_module_1 = require("./notification-module");
7
+ Object.defineProperty(exports, "createEmailNotificationModuleConfig", { enumerable: true, get: function () { return notification_module_1.createEmailNotificationModuleConfig; } });
7
8
  Object.defineProperty(exports, "createSmtpNotificationModuleConfig", { enumerable: true, get: function () { return notification_module_1.createSmtpNotificationModuleConfig; } });
8
9
  const defineShopEmailConfiguration = (configuration) => configuration;
9
10
  exports.defineShopEmailConfiguration = defineShopEmailConfiguration;
11
+ /** @deprecated The provider is set in the Admin (E-Mail > Versand). */
10
12
  exports.SMTP_ENVIRONMENT_VARIABLES = [
11
13
  "SMTP_HOST",
12
14
  "SMTP_PORT",
@@ -17,6 +19,8 @@ exports.SMTP_ENVIRONMENT_VARIABLES = [
17
19
  "SMTP_REQUIRE_TLS",
18
20
  ];
19
21
  /**
22
+ * @deprecated The provider is set in the Admin (E-Mail > Versand).
23
+ *
20
24
  * `medusa build` loads the configuration without deployment secrets. These
21
25
  * placeholders only satisfy the required-variable checks of a build; they
22
26
  * never reach a running server.
@@ -31,6 +35,8 @@ exports.SMTP_BUILD_PLACEHOLDERS = {
31
35
  SMTP_USER: "build-user",
32
36
  };
33
37
  /**
38
+ * @deprecated The provider is set in the Admin (E-Mail > Versand).
39
+ *
34
40
  * Validates the SMTP environment values and returns the transport
35
41
  * configuration. `allowInsecure` permits plain SMTP for development, tests
36
42
  * and builds; otherwise `SMTP_SECURE` or `SMTP_REQUIRE_TLS` must be `true`.
@@ -1,4 +1,29 @@
1
1
  import type { ResolvedSmtpConfiguration } from "../types";
2
+ /**
3
+ * The Notification Module with the shop's mail provider. Provider and
4
+ * credentials are set in the Admin (E-Mail > Versand); `allowInsecure`
5
+ * (development and tests only) permits SMTP without encryption there.
6
+ */
7
+ export declare const createEmailNotificationModuleConfig: (options?: Readonly<{
8
+ allowInsecure?: boolean;
9
+ }>) => {
10
+ resolve: string;
11
+ options: {
12
+ providers: {
13
+ id: string;
14
+ options: {
15
+ allowInsecure: boolean;
16
+ channels: string[];
17
+ };
18
+ resolve: string;
19
+ }[];
20
+ };
21
+ };
22
+ /**
23
+ * @deprecated Use `createEmailNotificationModuleConfig` and set the provider
24
+ * in the Admin (E-Mail > Versand). The SMTP values given here only fill the
25
+ * SMTP settings that are not stored in the Admin.
26
+ */
2
27
  export declare const createSmtpNotificationModuleConfig: (smtp: ResolvedSmtpConfiguration) => {
3
28
  resolve: string;
4
29
  options: {
@@ -1,6 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createSmtpNotificationModuleConfig = void 0;
3
+ exports.createSmtpNotificationModuleConfig = exports.createEmailNotificationModuleConfig = void 0;
4
+ /**
5
+ * The Notification Module with the shop's mail provider. Provider and
6
+ * credentials are set in the Admin (E-Mail > Versand); `allowInsecure`
7
+ * (development and tests only) permits SMTP without encryption there.
8
+ */
9
+ const createEmailNotificationModuleConfig = (options = {}) => ({
10
+ resolve: "@medusajs/medusa/notification",
11
+ options: {
12
+ providers: [
13
+ {
14
+ id: "smtp",
15
+ options: {
16
+ allowInsecure: options.allowInsecure === true,
17
+ channels: ["email"],
18
+ },
19
+ resolve: "@hartl-services/medusa-base/providers/smtp",
20
+ },
21
+ ],
22
+ },
23
+ });
24
+ exports.createEmailNotificationModuleConfig = createEmailNotificationModuleConfig;
25
+ /**
26
+ * @deprecated Use `createEmailNotificationModuleConfig` and set the provider
27
+ * in the Admin (E-Mail > Versand). The SMTP values given here only fill the
28
+ * SMTP settings that are not stored in the Admin.
29
+ */
4
30
  const createSmtpNotificationModuleConfig = (smtp) => ({
5
31
  resolve: "@medusajs/medusa/notification",
6
32
  options: {
@@ -1,5 +1,5 @@
1
1
  import { z } from "@medusajs/framework/zod";
2
- export declare const EMAIL_KINDS: readonly ["order.received", "order.received.admin", "order.shipped", "order.canceled", "order.return_requested", "order.return_received", "order.refunded", "auth.password_reset", "affiliate.consignment_report_due", "affiliate.application_submitted.admin", "user.invite", "auth.verification", "auth.login_code"];
2
+ export declare const EMAIL_KINDS: readonly ["order.received", "order.received.admin", "order.shipped", "order.canceled", "order.return_requested", "order.return_received", "order.refunded", "auth.password_reset", "affiliate.consignment_report_due", "affiliate.application_submitted.admin", "user.invite", "auth.verification", "auth.login_code", "system.credential_expiry.admin"];
3
3
  export declare const EmailKindSchema: z.ZodEnum<{
4
4
  "order.received": "order.received";
5
5
  "order.received.admin": "order.received.admin";
@@ -14,6 +14,7 @@ export declare const EmailKindSchema: z.ZodEnum<{
14
14
  "user.invite": "user.invite";
15
15
  "auth.verification": "auth.verification";
16
16
  "auth.login_code": "auth.login_code";
17
+ "system.credential_expiry.admin": "system.credential_expiry.admin";
17
18
  }>;
18
19
  export type EmailKind = (typeof EMAIL_KINDS)[number];
19
20
  /**
@@ -16,6 +16,7 @@ exports.EMAIL_KINDS = [
16
16
  "user.invite",
17
17
  "auth.verification",
18
18
  "auth.login_code",
19
+ "system.credential_expiry.admin",
19
20
  ];
20
21
  exports.EmailKindSchema = zod_1.z.enum(exports.EMAIL_KINDS);
21
22
  /**
@@ -376,6 +376,11 @@ export declare const EmailPersistedDataSchemas: {
376
376
  locale: z.ZodNullable<z.ZodString>;
377
377
  }, z.core.$strict>;
378
378
  readonly "auth.login_code": z.ZodObject<{}, z.core.$strict>;
379
+ readonly "system.credential_expiry.admin": z.ZodObject<{
380
+ provider: z.ZodLiteral<"microsoft365">;
381
+ client_id: z.ZodString;
382
+ expires_on: z.ZodString;
383
+ }, z.core.$strict>;
379
384
  };
380
385
  export declare const EmailRenderDataSchemas: {
381
386
  readonly "auth.password_reset": z.ZodObject<{
@@ -619,6 +624,11 @@ export declare const EmailRenderDataSchemas: {
619
624
  company_name: z.ZodNullable<z.ZodString>;
620
625
  message: z.ZodNullable<z.ZodString>;
621
626
  }, z.core.$strict>;
627
+ readonly "system.credential_expiry.admin": z.ZodObject<{
628
+ provider: z.ZodLiteral<"microsoft365">;
629
+ client_id: z.ZodString;
630
+ expires_on: z.ZodString;
631
+ }, z.core.$strict>;
622
632
  };
623
633
  export type EmailPersistedDataMap = {
624
634
  readonly [Kind in EmailKind]: Readonly<z.infer<(typeof EmailPersistedDataSchemas)[Kind]>>;
@@ -233,6 +233,13 @@ exports.EmailPersistedDataSchemas = {
233
233
  .strict(),
234
234
  // The code itself is never stored: it only exists in the render context.
235
235
  "auth.login_code": zod_1.z.object({}).strict(),
236
+ "system.credential_expiry.admin": zod_1.z
237
+ .object({
238
+ provider: zod_1.z.literal("microsoft365"),
239
+ client_id: NonEmptyStringSchema,
240
+ expires_on: affiliate_1.CalendarDateSchema,
241
+ })
242
+ .strict(),
236
243
  };
237
244
  exports.EmailRenderDataSchemas = {
238
245
  ...exports.EmailPersistedDataSchemas,
@@ -15,6 +15,7 @@ const SETTING_TYPES = new Set([
15
15
  "select",
16
16
  "image",
17
17
  "color",
18
+ "date",
18
19
  ]);
19
20
  const HEX_COLOR_PATTERN = /^#[0-9a-f]{6}$/;
20
21
  const ACTION_MODES = new Set(["sync", "background"]);
@@ -24,7 +25,7 @@ const ADMIN_PAGE_PATTERN = /^\/settings\/hartl-services(\/[a-z0-9][a-z0-9_-]*)+$
24
25
  const ICON_NAMES = new Set(plugin_icons_1.PLUGIN_ICON_NAMES);
25
26
  const SETTING_WIDTHS = new Set(["full", "half"]);
26
27
  /** Types an input placeholder makes no sense for. */
27
- const NO_PLACEHOLDER_TYPES = new Set(["boolean", "select", "image", "color"]);
28
+ const NO_PLACEHOLDER_TYPES = new Set(["boolean", "select", "image", "color", "date"]);
28
29
  const invalid = (pluginKey, message) => new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_DATA, `Ungültige Funktionsschalter für Plugin "${pluginKey}": ${message}`);
29
30
  const defaultMatchesType = (setting) => {
30
31
  const value = setting.default;
@@ -36,6 +37,8 @@ const defaultMatchesType = (setting) => {
36
37
  return typeof value === "string" && (0, lib_1.isHttpUrl)(value);
37
38
  case "color":
38
39
  return typeof value === "string" && HEX_COLOR_PATTERN.test(value);
40
+ case "date":
41
+ return typeof value === "string" && (0, lib_1.isCalendarDate)(value);
39
42
  case "number":
40
43
  return (typeof value === "number" &&
41
44
  Number.isFinite(value) &&
@@ -222,9 +225,26 @@ const validateAdminPage = (pluginKey, path) => {
222
225
  throw invalid(pluginKey, `admin_page "${String(path)}" muss unter "${ADMIN_PAGE_PREFIX}<plugin>" liegen und darf nicht mit "${BASE_PAGE_PREFIX}" beginnen.`);
223
226
  }
224
227
  };
228
+ /**
229
+ * `visible_when` points at a select or boolean setting outside the group and
230
+ * `equals` is one of its possible values.
231
+ */
232
+ const validateVisibleWhen = (pluginKey, groupName, condition, settings) => {
233
+ if (condition === undefined) {
234
+ return;
235
+ }
236
+ const { setting: key, equals } = (condition ?? {});
237
+ const target = settings.find((setting) => setting.key === key);
238
+ const fits = target?.type === "select"
239
+ ? (target.options ?? []).some((option) => option.value === equals)
240
+ : target?.type === "boolean" && typeof equals === "boolean";
241
+ if (!target || target.group === groupName || !fits) {
242
+ throw invalid(pluginKey, `Einstellungsgruppe "${groupName}": visible_when muss auf eine Auswahl oder einen Schalter außerhalb der Gruppe mit einem ihrer Werte zeigen.`);
243
+ }
244
+ };
225
245
  /**
226
246
  * Metadata per settings group: unique names, each used by a setting's
227
- * `group`, `requires_feature` a declared feature key.
247
+ * `group`, `requires_feature` a declared feature key, `visible_when` valid.
228
248
  */
229
249
  const validateSettingGroups = (pluginKey, groups, settings, featureKeys) => {
230
250
  if (groups === undefined) {
@@ -255,6 +275,7 @@ const validateSettingGroups = (pluginKey, groups, settings, featureKeys) => {
255
275
  !featureKeys.has(group.requires_feature)) {
256
276
  throw invalid(pluginKey, `Einstellungsgruppe "${group.name}": requires_feature "${String(group.requires_feature)}" ist kein Feature dieses Plugins.`);
257
277
  }
278
+ validateVisibleWhen(pluginKey, group.name, group.visible_when, settings);
258
279
  }
259
280
  };
260
281
  /** Optional texts and icon of the plugin's Admin page. */
@@ -36,8 +36,9 @@ export type PluginFeatureDefinition<K extends string = string> = {
36
36
  /**
37
37
  * `image`: public http(s) URL of an image the Admin uploads (PNG/JPEG).
38
38
  * `color`: lowercase hex color `#rrggbb`.
39
+ * `date`: calendar day `YYYY-MM-DD`.
39
40
  */
40
- export type PluginSettingType = "string" | "url" | "number" | "boolean" | "secret" | "select" | "image" | "color";
41
+ export type PluginSettingType = "string" | "url" | "number" | "boolean" | "secret" | "select" | "image" | "color" | "date";
41
42
  /** One allowed value of a `"select"` setting. */
42
43
  export type PluginSettingOption = {
43
44
  readonly value: string;
@@ -76,7 +77,7 @@ export type PluginSettingDefinition<S extends string = string> = {
76
77
  * Short text under the field; the Admin shows `hint ?? description`.
77
78
  */
78
79
  readonly hint?: string;
79
- /** Input placeholder; not for boolean, select, image and color. */
80
+ /** Input placeholder; not for boolean, select, image, color and date. */
80
81
  readonly placeholder?: string;
81
82
  /**
82
83
  * `"half"`: consecutive half-width settings share one two-column row.
@@ -90,7 +91,7 @@ export type PluginSettingDefinition<S extends string = string> = {
90
91
  readonly section?: string;
91
92
  };
92
93
  /** Metadata of one settings group, keyed by the `group` name of its settings. */
93
- export type PluginSettingGroupDefinition<K extends string = string> = {
94
+ export type PluginSettingGroupDefinition<K extends string = string, S extends string = string> = {
94
95
  /** Equals the `group` of at least one setting; unique per plugin. */
95
96
  readonly name: string;
96
97
  /** Explanatory text under the group heading. */
@@ -102,6 +103,15 @@ export type PluginSettingGroupDefinition<K extends string = string> = {
102
103
  * (still editable) while the feature is not effectively enabled.
103
104
  */
104
105
  readonly requires_feature?: K;
106
+ /**
107
+ * The group applies only while the select or boolean setting `setting`
108
+ * (of another group) resolves to `equals`. Otherwise the Admin hides it and
109
+ * its required settings do not count as missing; stored values are kept.
110
+ */
111
+ readonly visible_when?: {
112
+ readonly setting: S;
113
+ readonly equals: string | boolean;
114
+ };
105
115
  };
106
116
  export type PluginActionMode = "sync" | "background";
107
117
  export type PluginActionDefinition<A extends string = string> = {
@@ -161,7 +171,7 @@ export type PluginFeatureDescriptor<K extends string = string, S extends string
161
171
  */
162
172
  readonly require_enabled_feature?: true;
163
173
  /** Metadata for the groups used in `settings[].group`. */
164
- readonly setting_groups?: readonly PluginSettingGroupDefinition<K>[];
174
+ readonly setting_groups?: readonly PluginSettingGroupDefinition<K, S>[];
165
175
  };
166
176
  /** Values a plugin received from medusa-config; the base shows them as source "config". */
167
177
  export type PluginSettingsFallback<S extends string = string> = Partial<Record<S, PluginSettingValue | null | undefined>>;
@@ -10,3 +10,5 @@ export declare const toWireDate: (value: Date | string) => string;
10
10
  export declare const toOptionalWireDate: (value: Date | string | null | undefined) => string | null;
11
11
  /** Whether the value is an IANA time zone this runtime knows ("Europe/Vienna"). */
12
12
  export declare const isValidTimeZone: (value: unknown) => value is string;
13
+ /** Whether the value is an existing calendar day written `yyyy-mm-dd`. */
14
+ export declare const isCalendarDate: (value: unknown) => value is string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isValidTimeZone = exports.toOptionalWireDate = exports.toWireDate = exports.toIsoDate = exports.startOfUtcDate = exports.toValidDate = void 0;
3
+ exports.isCalendarDate = exports.isValidTimeZone = exports.toOptionalWireDate = exports.toWireDate = exports.toIsoDate = exports.startOfUtcDate = exports.toValidDate = void 0;
4
4
  const utils_1 = require("@medusajs/framework/utils");
5
5
  /** A valid `Date`; throws `INVALID_DATA` for an unparsable value. */
6
6
  const toValidDate = (value, message = "Invalid date.") => {
@@ -40,3 +40,9 @@ const isValidTimeZone = (value) => {
40
40
  }
41
41
  };
42
42
  exports.isValidTimeZone = isValidTimeZone;
43
+ /** Whether the value is an existing calendar day written `yyyy-mm-dd`. */
44
+ const isCalendarDate = (value) => typeof value === "string" &&
45
+ /^\d{4}-\d{2}-\d{2}$/.test(value) &&
46
+ !Number.isNaN(Date.parse(`${value}T00:00:00Z`)) &&
47
+ new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
48
+ exports.isCalendarDate = isCalendarDate;