@hartl-services/medusa-base 1.1.0 → 2.1.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 (50) hide show
  1. package/.medusa/server/src/admin/index.js +3 -1
  2. package/.medusa/server/src/admin/index.mjs +3 -1
  3. package/.medusa/server/src/api/customer-verification-request-throttle.js +50 -0
  4. package/.medusa/server/src/api/middlewares.js +31 -1
  5. package/.medusa/server/src/api/store/customer-verification/resend/route.js +17 -0
  6. package/.medusa/server/src/documents/utils/document-merchant.js +5 -5
  7. package/.medusa/server/src/email/application/handle-customer-verification-event.js +341 -0
  8. package/.medusa/server/src/email/application/resend-customer-verification.js +65 -0
  9. package/.medusa/server/src/email/application/start-customer-verification-snapshot.js +21 -0
  10. package/.medusa/server/src/email/application/throttle-verification-request.js +49 -0
  11. package/.medusa/server/src/email/customer-verification-snapshot.js +144 -0
  12. package/.medusa/server/src/email/domain/business-keys.js +12 -2
  13. package/.medusa/server/src/email/domain/customer-verification-resend.js +37 -0
  14. package/.medusa/server/src/email/domain/event-schemas.js +18 -2
  15. package/.medusa/server/src/email/plugin-features.js +9 -2
  16. package/.medusa/server/src/email/rendering/default-templates.js +20 -1
  17. package/.medusa/server/src/email/rendering/resolve-shop-configuration.js +5 -1
  18. package/.medusa/server/src/email/types/email-kinds.js +14 -2
  19. package/.medusa/server/src/email/types/template-data.js +9 -1
  20. package/.medusa/server/src/modules/documents/domain/merchant.js +2 -33
  21. package/.medusa/server/src/modules/documents/domain/options.js +1 -7
  22. package/.medusa/server/src/modules/email/migrations/Migration20261001090000.js +16 -0
  23. package/.medusa/server/src/modules/email/service.js +30 -8
  24. package/.medusa/server/src/public/shop-master-data.js +6 -6
  25. package/.medusa/server/src/shop-config/build-food-shop-config.js +17 -6
  26. package/.medusa/server/src/shop-config/options.js +1 -15
  27. package/.medusa/server/src/subscribers/customer-verification.js +23 -0
  28. package/.medusa/server/src/workflows/documents/issue-cancellation-after-refund.js +2 -3
  29. package/.medusa/server/src/workflows/documents/issue-document.js +3 -4
  30. package/.medusa/server/src/workflows/documents/issue-packing-list.js +2 -2
  31. package/.medusa/server/src/workflows/email/dispatch-email-message.js +4 -2
  32. package/README.md +154 -49
  33. package/dist/documents/types/documents.d.ts +0 -22
  34. package/dist/email/customer-verification-snapshot.d.ts +53 -0
  35. package/dist/email/customer-verification-snapshot.js +143 -0
  36. package/dist/email/types/email-kinds.d.ts +9 -1
  37. package/dist/email/types/email-kinds.js +13 -1
  38. package/dist/email/types/shop-configuration.d.ts +8 -0
  39. package/dist/email/types/template-data.d.ts +9 -0
  40. package/dist/email/types/template-data.js +8 -0
  41. package/dist/modules/documents/domain/options.d.ts +1 -9
  42. package/dist/modules/documents/domain/options.js +0 -6
  43. package/dist/public/shop-master-data.d.ts +4 -5
  44. package/dist/public/shop-master-data.js +5 -5
  45. package/dist/shop-config/build-food-shop-config.js +16 -5
  46. package/dist/shop-config/options.d.ts +4 -6
  47. package/dist/shop-config/options.js +0 -14
  48. package/package.json +2 -2
  49. package/dist/modules/documents/domain/merchant.d.ts +0 -22
  50. package/dist/modules/documents/domain/merchant.js +0 -85
package/README.md CHANGED
@@ -68,7 +68,7 @@ plugins: [
68
68
  // E-Mail, Documents and the /files gateway: see the sections below
69
69
  // (or let the Shop-Config-Preset fill them in).
70
70
  // email: { jwtSecret, shop },
71
- // documents: { merchant, numbering, pdf, htmlFactory },
71
+ // documents: { numbering, pdf, htmlFactory },
72
72
  // files: { gateway },
73
73
  },
74
74
  },
@@ -83,7 +83,7 @@ every module of the plugin; each module reads only its own namespace:
83
83
  |---|---|---|
84
84
  | `settingsSecret` | `plugin_features` module | Key material for stored secret settings. Set it from an environment variable of the shop; unset or empty, the base uses `projectConfig.http.cookieSecret`. |
85
85
  | `email` | `email` module | `{ jwtSecret, shop }`: branding, URLs and the JWT secret of the e-mail links (see "E-Mail"). Without `email.shop` the module boots but sends nothing. |
86
- | `documents` | `documents` module | `{ merchant?, numbering?, pdf?, htmlFactory? }` (see "Dokumente"). Defaults: numbering time zone `Europe/Vienna`, PDF executable `google-chrome`. |
86
+ | `documents` | `documents` module | `{ numbering?, pdf?, htmlFactory? }` (see "Dokumente"). Defaults: numbering time zone `Europe/Vienna`, PDF executable `google-chrome`. |
87
87
  | `files` | `/files` routes | `{ gateway: { signingSecret } }`, returned by `createS3CacheStorageConfig` (see "Dateispeicher (S3)"). |
88
88
 
89
89
  `options: {}` is valid: the base then only provides the feature toggles,
@@ -125,6 +125,7 @@ dashboard uses.
125
125
  | `POST` | `/admin/plugin-features/:plugin_key/settings` | Admin user | `AdminPluginFeaturesResponse` |
126
126
  | `POST` | `/admin/plugin-features/:plugin_key/actions/:action_key` | Admin user | `AdminPluginActionRunResponse` (200 sync, 202 background), 409 `AdminPluginActionConflictResponse` while it runs |
127
127
  | `GET` | `/admin/plugin-features/:plugin_key/actions/:action_key/runs` | Admin user | `AdminPluginActionRunsResponse` (`?limit=`, default 1, max 20) |
128
+ | `POST` | `/store/customer-verification/resend` | publishable key | 202 `ResendCustomerVerificationResponse` (`{ accepted: true }`) for every address, 404 while `customer_verification` is off; see "Kontobestätigung per E-Mail" |
128
129
 
129
130
  Every authenticated Admin user may change toggles and settings and run
130
131
  actions (no RBAC by design). Admin plugin DTOs carry the toggles plus
@@ -272,7 +273,9 @@ Code on the cart and checkout hot path (cart middlewares, cart workflow hooks,
272
273
  `orderCreated` handling, cart subscribers) must never read toggles. It stays
273
274
  fail-safe and independent of the database state of this plugin. Toggles
274
275
  control visibility and reachability of features, not the integrity of
275
- existing data.
276
+ existing data. Login is not the cart path: the E-Mail plugin's
277
+ `customer_verification` toggle does switch customer logins, through an
278
+ in-memory copy of the toggle (see "Kontobestätigung per E-Mail").
276
279
 
277
280
  ## Runtime settings
278
281
 
@@ -480,15 +483,13 @@ settings like any plugin's:
480
483
  | `iban`, `bic` | string | no |
481
484
  | `company_register_number` (Firmenbuchnummer), `company_register_court` (Firmenbuchgericht) | string | no |
482
485
 
483
- Server-side code reads them with a plugin's own medusa-config values as
484
- fallback (stored in the Admin wins per key):
486
+ Server-side code reads them exactly as stored in the Admin; there is no
487
+ medusa-config fallback:
485
488
 
486
489
  ```ts
487
490
  import { getShopMasterData } from "@hartl-services/medusa-base"
488
491
 
489
- const { values, configured, missing } = await getShopMasterData(container, {
490
- legal_name: options.merchant?.legalName,
491
- })
492
+ const { values, configured, missing } = await getShopMasterData(container)
492
493
  if (!configured) {
493
494
  throw new MedusaError(
494
495
  MedusaError.Types.UNEXPECTED_STATE,
@@ -834,8 +835,11 @@ Options of the `email` namespace:
834
835
  `projectConfig.http.jwtSecret`.
835
836
  - `shop` — a `ShopEmailConfiguration` object: `displayName`,
836
837
  `affiliatePortalUrl`, the `passwordResetUrl`/`inviteUrl` builders and the
837
- `orderNotificationEmail` mailbox. `layout` and `renderers` are optional
838
- overrides, see "Shop configuration and default templates".
838
+ `orderNotificationEmail` mailbox. `verifyAccountUrl` (optional builder,
839
+ like `passwordResetUrl`; the mail appends `#token=`) is the storefront page
840
+ that confirms an account, needed while `customer_verification` is on.
841
+ `layout` and `renderers` are optional overrides, see "Shop configuration and
842
+ default templates".
839
843
 
840
844
  Without `email.shop` the module still boots, but it cannot dispatch: no mail
841
845
  is sent (the dispatch configuration is requested lazily and fails with
@@ -884,7 +888,8 @@ feature is switched on again.
884
888
  | --------------------- | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------- |
885
889
  | (plugin switch) | E-Mail | on | every route under `/admin/email/*` (mail log, resends, terms versions; 404); jobs `email-reconcile-attempts`, `email-reconcile-pending-messages`, `email-retry-messages`; every feature below | checkout and orders; already queued messages stay stored |
886
890
  | `order_mails` | Bestell-Mails | on | subscribers `order-placed`, `email-shipment-created`, `email-order-canceled`, `return-requested`, `return-received`, `documents-shipment-invoice-assignment-decided`, `documents-order-cancellation-ready`, `documents-refund-ready` | jobs, Admin routes, already created messages |
887
- | `account_mails` | Konto-Mails | on | subscribers `password-reset`, `user-invite` | jobs, Admin routes, already created messages |
891
+ | `account_mails` | Konto-Mails | on | subscribers `password-reset`, `user-invite`, `customer-verification` (the last one only with `customer_verification` on as well) | jobs, Admin routes, already created messages |
892
+ | `customer_verification` | Kontobestätigung per E-Mail | off | Medusa's e-mail verification of customer logins (see "Kontobestätigung per E-Mail"), route `POST /store/customer-verification/resend` (404 while off), subscriber `customer-verification`; child of `account_mails` | checkout and orders; confirmed accounts stay confirmed, already created messages |
888
893
  | `affiliate_reminders` | Affiliate-Erinnerungen | on | subscriber `affiliate-consignment-reminder-due` | jobs, Admin routes, already created messages |
889
894
 
890
895
  With the plugin switched off the three jobs return immediately, so nothing
@@ -904,6 +909,111 @@ and gates the Terms PDF versions and mail log sections below it on the
904
909
  plugin switch, showing `PluginFeatureDisabledNotice` in their place while it
905
910
  is off.
906
911
 
912
+ ### Kontobestätigung per E-Mail
913
+
914
+ Feature `customer_verification` (child of `account_mails`, default **off**)
915
+ makes customers confirm their e-mail address through a link before their
916
+ first login. It builds on Medusa 2.19's own e-mail verification
917
+ (`projectConfig.http.authVerificationsPerActor`) and adds the mail, the
918
+ toggle and a resend route.
919
+
920
+ How it works while the feature is on:
921
+
922
+ 1. A login (`POST /auth/customer/emailpass`) of an account whose address is
923
+ not confirmed answers `200 { verification_required: true, verification,
924
+ token }`. `token` is an actorless JWT, valid only for the verification
925
+ calls.
926
+ 2. The storefront calls Medusa's `POST /auth/verification/request`
927
+ (`{ code_provider: "token", entity_id: <e-mail>, entity_type: "email" }`)
928
+ with that token. Medusa emits `auth.verification_requested`; the
929
+ `customer-verification` subscriber sends the mail "Bestätige deine
930
+ E-Mail-Adresse" (kind `auth.verification`) to `entity_id`.
931
+ 3. The link is `<STOREFRONT_URL>/<cc>/verify-account#token=<token>`
932
+ (`verifyAccountUrl` plus the fragment, never a query string). The
933
+ storefront page calls `POST /auth/verification/confirm`
934
+ (`{ code: <token>, code_provider: "token" }`); the next login returns a
935
+ normal token.
936
+ 4. The token is valid for 15 minutes (Medusa's default `ttl_seconds` of the
937
+ token provider). Every new request issues a new token and invalidates the
938
+ previous one.
939
+ 5. `POST /store/customer-verification/resend` (`{ email }`, publishable key)
940
+ asks for a new mail without a login. It always answers 202
941
+ `{ accepted: true }`, whether or not an account exists, is confirmed or
942
+ was asked too recently, so addresses cannot be probed. The answer is sent
943
+ before any lookup (equal timing), the work runs detached. A mail follows
944
+ only for an existing, unconfirmed customer account whose last request is
945
+ older than 60 seconds. Admin users and addresses without a customer
946
+ account are ignored. A failure is logged with a fixed message and the
947
+ error class only (never the address), not answered. While the feature is
948
+ off the route answers 404.
949
+ 6. The same 60-second limit per account applies to Medusa's own
950
+ `POST /auth/verification/request`, which the storefront calls at every
951
+ blocked login: a base middleware answers a request within a minute of the
952
+ previous one with `201 { verification }` (Medusa's shape, without the
953
+ stored token hash) and sends nothing, so repeated logins cannot mail-bomb
954
+ an address; the earlier link stays valid. It applies only to the customer
955
+ account's own `emailpass` address while the feature is on, and fails open
956
+ (the request goes on to Medusa) if the check itself fails.
957
+
958
+ Accounts that exist before the feature is switched on need no migration: they
959
+ have no confirmation row, so their next login is answered with
960
+ `verification_required` and the storefront sends them the mail. Social or
961
+ other providers are not affected, only `emailpass` for `customer`.
962
+
963
+ The mail is sent only while the plugin switch, `account_mails` and
964
+ `customer_verification` are on. It is built from the event like the
965
+ password-reset mail: only the token's hash (the business key) is stored, never
966
+ the token or the link, and neither the generic retry job nor an Admin resend
967
+ can touch it (the Event Bus retry and the 60-second resend are the retry paths).
968
+ Medusa takes `entity_id` of the verification request from the request body
969
+ without comparing it, so the subscriber sends only to an address that is the
970
+ identity's own `emailpass` address and only for a customer actor.
971
+
972
+ **Switching at run time.** The preset sets
973
+ `projectConfig.http.authVerificationsPerActor` to an object whose `customer`
974
+ is a getter: Medusa reads it on every login, and the getter answers from an
975
+ in-memory copy of the effective toggle. The copy is loaded when the
976
+ application starts (the E-Mail module's `onApplicationStart`) and, while
977
+ an unref'd timer refreshes it every 30 seconds, also without traffic (stopped
978
+ at shutdown); a read that finds the copy older than 30 seconds additionally
979
+ starts a background refresh (stale-while-revalidate; the getter is synchronous
980
+ and never throws). A change in the Admin therefore reaches a running instance
981
+ within about 30 seconds plus the read (each instance has its own copy); the
982
+ resend route, the request throttle and the subscriber read
983
+ the toggle itself and follow at once. A failed read keeps the last value and
984
+ logs a warning; before the first successful read the copy is off. The copy
985
+ lives on `globalThis` because the preset (`dist`) and the plugin code Medusa
986
+ loads (`.medusa/server`) are compiled separately. Only the preset
987
+ (`buildFoodShopConfig` / `defineFoodShopConfig`) sets the getter; a shop with a
988
+ hand-written config must set `authVerificationsPerActor` itself (a fixed list,
989
+ changed by a restart) for the toggle to have any effect on logins.
990
+
991
+ Login is not on the cart path, so the cart rule above is not affected.
992
+
993
+ Things to know when switching it on:
994
+
995
+ - Customers who are already logged in stay logged in until their session or
996
+ JWT expires: Medusa checks the confirmation at login and token generation
997
+ without an actor, and a token refresh of an existing actor skips it.
998
+ Only their next login asks for the confirmation.
999
+ - A password reset does not confirm the address; the customer still
1000
+ confirms through the link at the next login.
1001
+ - The storefront must not store the actorless `token` of a
1002
+ `verification_required` response as the customer session. It is only for
1003
+ the verification request, and it carries no customer.
1004
+
1005
+ Storefront contract (DTOs, schema and path from the Contracts package,
1006
+ `types/customer-verification-store`, `schemas/customer-verification`,
1007
+ `paths/customer-verification`):
1008
+
1009
+ - Read `GET /store/plugin-features/email` and show the confirmation flow only
1010
+ while `features.customer_verification` is `true`.
1011
+ - Handle `verification_required` on login and registration: request the mail
1012
+ with the actorless token, show "check your inbox" and offer "Bestätigungsmail
1013
+ erneut senden" (`customerVerificationStorePaths.post.resend`).
1014
+ - Serve `/<cc>/verify-account`, read `token` from `location.hash`, confirm it
1015
+ and then send the customer to the login.
1016
+
907
1017
  ### Ownership
908
1018
 
909
1019
  - Documents owns document issuance and publishes immutable document facts.
@@ -946,6 +1056,7 @@ installed Medusa 2.19 exports:
946
1056
  | `order.return_requested` | `{ order_id: string; return_id: string }` |
947
1057
  | `order.return_received` | `{ order_id: string; return_id: string }` |
948
1058
  | `auth.password_reset` | `{ entity_id: string; actor_type: string; token: string; metadata?: Record<string, unknown> }` |
1059
+ | `auth.verification_requested` | `{ entity_id: string; entity_type: string; code_provider: string; auth_identity_id: string; code: string; expires_at: Date \| string; metadata?: Record<string, unknown> }`; not strict, `code` is missing for an already confirmed identity (ignored) |
949
1060
 
950
1061
  The shared immutable Documents artifact is:
951
1062
 
@@ -1039,8 +1150,8 @@ has at most 35 characters. Email runtime-validates every contract before use.
1039
1150
 
1040
1151
  - The accepted kinds are exactly `order.received`, `order.received.admin`, `order.shipped`,
1041
1152
  `order.canceled`, `order.return_requested`, `order.return_received`,
1042
- `order.refunded`, `auth.password_reset`, and
1043
- `affiliate.consignment_report_due`.
1153
+ `order.refunded`, `auth.password_reset`, `auth.verification`,
1154
+ `affiliate.consignment_report_due` and `user.invite`.
1044
1155
  - Every automatic message has a deterministic business key.
1045
1156
  - Every placed Order creates independent customer and Shop intents. The customer
1046
1157
  intent honors `no_notification`; the Shop intent always targets the static
@@ -1062,10 +1173,11 @@ has at most 35 characters. Email runtime-validates every contract before use.
1062
1173
  including any invoice attachment; refund email has no Documents-disabled
1063
1174
  native fallback.
1064
1175
  - Recipients, rendered content, attachment bytes, SMTP responses, credentials,
1065
- reset tokens, and reset URLs are never logged.
1066
- - Reset tokens and reset URLs are never persisted by Email or Notification.
1067
- - Password-reset messages cannot be retried by the generic retry job or resent
1068
- by an Admin.
1176
+ reset and verification tokens, and their URLs are never logged.
1177
+ - Reset and verification tokens and URLs are never persisted by Email or
1178
+ Notification.
1179
+ - Password-reset and account-confirmation messages cannot be retried by the
1180
+ generic retry job or resent by an Admin.
1069
1181
  - Unknown transport outcomes are never automatically resent.
1070
1182
  - Shop branding is the configured `displayName` plus the `logo` setting of
1071
1183
  the E-Mail plugin and the company details of the Shop-Stammdaten; a shop overrides the default layout or
@@ -1174,6 +1286,14 @@ without `iat`) and a note that the mail can be ignored. The lifetime is
1174
1286
  passed as the ephemeral `valid_minutes` next to `reset_url` and is never
1175
1287
  persisted.
1176
1288
 
1289
+ The account confirmation (`auth.verification`) has the subject "Bestätige
1290
+ deine E-Mail-Adresse", a "E-Mail-Adresse bestätigen" button, the link as
1291
+ copyable text, a note that the link is only valid for a limited time (a new
1292
+ one comes with the next login or "Bestätigungsmail erneut senden") and a note
1293
+ that the mail can be ignored. The link comes from `verifyAccountUrl` plus
1294
+ `#token=`; without that builder the message fails with `render_failed`. See
1295
+ "Kontobestätigung per E-Mail".
1296
+
1177
1297
  The affiliate reminder (`affiliate.consignment_report_due`) greets the
1178
1298
  affiliate by `affiliate_display_name`, names the report month ("September
1179
1299
  2026", Austrian month names) and the due date ("10.10.2026") as calendar
@@ -1260,9 +1380,10 @@ process persists success, and a conscious resend can produce a duplicate.
1260
1380
 
1261
1381
  Email transport retries and Event Bus retries are separate. Once an ordinary
1262
1382
  message outcome is persisted, its subscriber completes and the Email retry job
1263
- owns any `retry_pending` work. Password-reset token and URL data remain
1264
- ephemeral; password reset is excluded from the generic retry job and Admin
1265
- resend, and retryable handling stays bounded by the original Event Bus event.
1383
+ owns any `retry_pending` work. Password-reset and verification token and URL
1384
+ data remain ephemeral; both kinds are excluded from the generic retry job and
1385
+ Admin resend, and retryable handling stays bounded by the original Event Bus
1386
+ event.
1266
1387
 
1267
1388
  ### Admin API
1268
1389
 
@@ -1461,12 +1582,7 @@ export default defineConfig({
1461
1582
  },
1462
1583
  {
1463
1584
  resolve: "@hartl-services/medusa-tax-compliance",
1464
- options: {
1465
- merchant: {
1466
- countryCode: process.env.MERCHANT_COUNTRY_CODE!,
1467
- vatNumber: process.env.MERCHANT_VAT_NUMBER!,
1468
- },
1469
- },
1585
+ options: {},
1470
1586
  },
1471
1587
  ],
1472
1588
  });
@@ -1487,10 +1603,9 @@ Options of the `documents` namespace (all optional; `{}` is valid):
1487
1603
  rejected at startup when invalid.
1488
1604
  - `pdf.executablePath` — Chromium/Chrome executable path or command name.
1489
1605
  Defaults to `"google-chrome"`.
1490
- - `merchant` — optional fallback for the shop master data (see "Merchant
1491
- data" below), shape `DocumentMerchantOption`:
1492
- `{ countryCode, legalName, registeredAddress: { address1, address2?, city,
1493
- countryCode, postalCode }, vatNumber? }`.
1606
+
1607
+ The sender (company, address, UID, contact, bank) is not an option: it comes
1608
+ only from the Shop-Stammdaten (see "Merchant data" below).
1494
1609
 
1495
1610
  The `documents` module is always registered with the base (no `documentsEnabled`
1496
1611
  switch); the Admin switches it off at run time through the feature toggles
@@ -1557,8 +1672,8 @@ Each render reads it and passes it to the `htmlFactory` as `input.merchant`
1557
1672
  (`DocumentMerchant`: `legal_name`, `address_line1`, `address_line2`,
1558
1673
  `postal_code`, `city`, `country_code`, `vat_number`, `email`, `phone`,
1559
1674
  `website`, `iban`, `bic`, `company_register_number`,
1560
- `company_register_court`; unset optional fields are `null`). Per field, a
1561
- value stored in the Admin wins over the `merchant` option.
1675
+ `company_register_court`; unset optional fields are `null`). There is no
1676
+ medusa-config fallback: what is stored in the Admin is what is printed.
1562
1677
 
1563
1678
  - `merchant` is not part of the stored snapshot: a regenerated document
1564
1679
  prints the master data current at regeneration time.
@@ -1638,8 +1753,8 @@ a toggle.
1638
1753
  ### Shop integration
1639
1754
 
1640
1755
  The demo shop (`apps/backend`) gets everything from the preset (see
1641
- "Shop-Config-Preset"): `documents.merchant` is its `merchant` option,
1642
- `documents.numbering.timeZone` its `timeZone` option and
1756
+ "Shop-Config-Preset"): `documents.numbering.timeZone` is its `timeZone`
1757
+ option, `documents.htmlFactory` its optional `documentHtmlFactory` and
1643
1758
  `documents.pdf.executablePath` the `DOCUMENT_PDF_EXECUTABLE` variable. The
1644
1759
  development UID `ATU00000000` is a placeholder: Documents never prints it,
1645
1760
  and Tax Compliance does not transmit it to FinanzOnline. Enter the real
@@ -2307,17 +2422,6 @@ pnpm add @hartl-services/medusa-base \
2307
2422
  import { defineFoodShopConfig } from "@hartl-services/medusa-base/shop-config"
2308
2423
 
2309
2424
  module.exports = defineFoodShopConfig({
2310
- merchant: {
2311
- countryCode: "at",
2312
- legalName: "Example Shop GmbH",
2313
- registeredAddress: {
2314
- address1: "Musterstrasse 1",
2315
- city: "Wien",
2316
- countryCode: "at",
2317
- postalCode: "1010",
2318
- },
2319
- vatNumber: "ATU12345678",
2320
- },
2321
2425
  displayName: "EXAMPLE SHOP",
2322
2426
  orderNotificationEmail: "orders@example.com",
2323
2427
  timeZone: "Europe/Vienna",
@@ -2358,11 +2462,10 @@ validate the environment; a missing variable fails with the variable's name.
2358
2462
 
2359
2463
  | Option | Meaning |
2360
2464
  | --- | --- |
2361
- | `merchant` | `DocumentMerchantOption` (`countryCode`, `legalName`, `registeredAddress`, optional `vatNumber`). Fallback for the Shop-Stammdaten (Admin, "Hartl Services Plugins") that documents and tax compliance read per field; `legalName` is also the seeded store name. Country codes are 2-letter lowercase. |
2362
- | `displayName` | Shop name in the default e-mail templates. |
2465
+ | `displayName` | Shop name in the default e-mail templates (a shop's initial seed may use it as store name). Company name, address and UID are no options: documents and tax compliance read them only from the Shop-Stammdaten (Admin, "Hartl Services Plugins"). |
2363
2466
  | `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
2364
2467
  | `timeZone` | IANA time zone (validated with `Intl`). Chooses the annual document-number period, the printed dates on documents and the affiliate consignment reminder time zone. |
2365
- | `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
2468
+ | `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` and `<STOREFRONT_URL>/<cc>/verify-account` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
2366
2469
  | `documentHtmlFactory` | Optional `DocumentHtmlFactory` that replaces the default document markup, typically `createDefaultDocumentHtmlFactory({ timeZone, theme })` from `/config` with the shop's CSS and watermark (see "Layout, logo, payment terms and shop theme"). Must be a function. |
2367
2470
  | `loadEnvFile` | Default `true`: calls `loadEnv(NODE_ENV \|\| "development", process.cwd())`. Pass `false` when the environment is already complete (tests, isolated child processes). |
2368
2471
 
@@ -2402,4 +2505,6 @@ Not configurable on purpose: the plugin order (base first), the Redis event
2402
2505
  bus job policy (3 attempts, exponential backoff from 1 s), S3 cache defaults
2403
2506
  (15 min cleanup, 1 h TTL, absolute expiration), Stripe immediate capture with
2404
2507
  automatic payment methods, the 24 h session lifetime, Admin path `/app`,
2405
- consignment reminders due on day 10, and the Documents module always being part of the base.
2508
+ consignment reminders due on day 10, the Documents module always being part of the base, and
2509
+ `http.authVerificationsPerActor.customer` (a getter that follows the E-Mail `customer_verification` toggle, see
2510
+ "Kontobestätigung per E-Mail").
@@ -144,28 +144,6 @@ export type DocumentMerchant = {
144
144
  company_register_number: string | null;
145
145
  company_register_court: string | null;
146
146
  };
147
- /**
148
- * Optional `merchant` plugin option: used per field only where the shop
149
- * master data in the base plugin has no value (same shape as the demo shop's
150
- * `merchant` constant in medusa-config).
151
- */
152
- export type DocumentMerchantOption = {
153
- countryCode: string;
154
- legalName: string;
155
- registeredAddress: {
156
- address1: string;
157
- address2?: string;
158
- city: string;
159
- countryCode: string;
160
- postalCode: string;
161
- };
162
- vatNumber?: string;
163
- };
164
- /**
165
- * The Documents settings (Admin -> "Hartl Services Plugins" -> Dokumente) a
166
- * render needs, read when a document is rendered. Not part of the stored
167
- * snapshot: a regenerated document shows the current values.
168
- */
169
147
  export type DocumentRenderSettings = {
170
148
  /**
171
149
  * The uploaded logo as a `data:` URL, ready for `<img src>`; `null` when no
@@ -0,0 +1,53 @@
1
+ /**
2
+ * In-memory copy of the effective `customer_verification` toggle for the one
3
+ * place that cannot await it: Medusa reads
4
+ * `projectConfig.http.authVerificationsPerActor.customer` synchronously on
5
+ * every login. The shop preset puts `customerVerificationActors()` behind a
6
+ * getter there, so a toggle change in the Admin takes effect without a
7
+ * restart.
8
+ *
9
+ * - Boot: `startCustomerVerificationSnapshot` loads the toggle once.
10
+ * - Afterwards an unref'd timer refreshes every 30 s, so a change reaches an
11
+ * idle instance too. A read that finds the copy older than 30 s (a blocked
12
+ * timer, say) also starts one refresh in the background
13
+ * (stale-while-revalidate) and still answers from the copy. A change thus
14
+ * reaches a running instance within about 30 s plus the duration of the
15
+ * read.
16
+ * - A failed read keeps the last value (initially "off") and is logged; the
17
+ * next attempt waits another 30 s.
18
+ *
19
+ * The state lives on `globalThis`: the preset (`dist`) and the Medusa-loaded
20
+ * plugin code (`.medusa/server`) are compiled separately and would otherwise
21
+ * each hold their own copy. Login is not on the cart path.
22
+ */
23
+ export declare const CUSTOMER_VERIFICATION_REFRESH_MS = 30000;
24
+ export type CustomerVerificationActor = Readonly<{
25
+ auth_provider: string;
26
+ entity_type: string;
27
+ }>;
28
+ /** Reads the toggle once; never rejects. Concurrent calls share one read. */
29
+ export declare const refreshCustomerVerificationSnapshot: () => Promise<void>;
30
+ /** Stops the periodic refresh (application shutdown). The value stays. */
31
+ export declare const stopCustomerVerificationSnapshot: () => void;
32
+ /**
33
+ * Registers the reader, loads the toggle once and keeps it fresh every 30 s
34
+ * (application start). `read` returns the effective toggle; `warn` is the
35
+ * container logger. Starting again replaces the reader and the timer.
36
+ */
37
+ export declare const startCustomerVerificationSnapshot: (input: Readonly<{
38
+ read: () => Promise<boolean>;
39
+ warn: (message: string) => void;
40
+ }>) => Promise<void>;
41
+ /** Synchronous; starts a background refresh when the copy is stale. Never throws. */
42
+ export declare const isCustomerVerificationEnabled: () => boolean;
43
+ /**
44
+ * The value of `authVerificationsPerActor.customer`: Medusa's e-mail
45
+ * verification for the `emailpass` provider while the toggle is on, none
46
+ * while it is off. A fresh array per call.
47
+ */
48
+ export declare const customerVerificationActors: () => CustomerVerificationActor[];
49
+ /**
50
+ * Test hook: sets the value and counts as just refreshed, so it holds for
51
+ * 30 s without a reader; `null` forgets reader and value.
52
+ */
53
+ export declare const setCustomerVerificationEnabledForTests: (enabled: boolean | null) => void;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setCustomerVerificationEnabledForTests = exports.customerVerificationActors = exports.isCustomerVerificationEnabled = exports.startCustomerVerificationSnapshot = exports.stopCustomerVerificationSnapshot = exports.refreshCustomerVerificationSnapshot = exports.CUSTOMER_VERIFICATION_REFRESH_MS = void 0;
4
+ /**
5
+ * In-memory copy of the effective `customer_verification` toggle for the one
6
+ * place that cannot await it: Medusa reads
7
+ * `projectConfig.http.authVerificationsPerActor.customer` synchronously on
8
+ * every login. The shop preset puts `customerVerificationActors()` behind a
9
+ * getter there, so a toggle change in the Admin takes effect without a
10
+ * restart.
11
+ *
12
+ * - Boot: `startCustomerVerificationSnapshot` loads the toggle once.
13
+ * - Afterwards an unref'd timer refreshes every 30 s, so a change reaches an
14
+ * idle instance too. A read that finds the copy older than 30 s (a blocked
15
+ * timer, say) also starts one refresh in the background
16
+ * (stale-while-revalidate) and still answers from the copy. A change thus
17
+ * reaches a running instance within about 30 s plus the duration of the
18
+ * read.
19
+ * - A failed read keeps the last value (initially "off") and is logged; the
20
+ * next attempt waits another 30 s.
21
+ *
22
+ * The state lives on `globalThis`: the preset (`dist`) and the Medusa-loaded
23
+ * plugin code (`.medusa/server`) are compiled separately and would otherwise
24
+ * each hold their own copy. Login is not on the cart path.
25
+ */
26
+ exports.CUSTOMER_VERIFICATION_REFRESH_MS = 30_000;
27
+ const STATE_KEY = Symbol.for("@hartl-services/medusa-base/customer-verification-snapshot");
28
+ const state = () => {
29
+ const holder = globalThis;
30
+ holder[STATE_KEY] ??= {
31
+ enabled: false,
32
+ refreshedAt: null,
33
+ refreshing: null,
34
+ timer: null,
35
+ read: null,
36
+ warn: null,
37
+ };
38
+ return holder[STATE_KEY];
39
+ };
40
+ const logFailure = (current, error) => {
41
+ try {
42
+ current.warn?.(`Customer verification toggle could not be read, keeping "${current.enabled ? "on" : "off"}": ${error instanceof Error ? error.message : "unknown error"}`);
43
+ }
44
+ catch {
45
+ // A broken logger must not turn a failed refresh into a throw.
46
+ }
47
+ };
48
+ /** Reads the toggle once; never rejects. Concurrent calls share one read. */
49
+ const refreshCustomerVerificationSnapshot = () => {
50
+ const current = state();
51
+ if (!current.read) {
52
+ return Promise.resolve();
53
+ }
54
+ if (current.refreshing) {
55
+ return current.refreshing;
56
+ }
57
+ const read = current.read;
58
+ // Started from a microtask so the assignment below always precedes `finally`.
59
+ const refreshing = Promise.resolve().then(async () => {
60
+ try {
61
+ current.enabled = (await read()) === true;
62
+ }
63
+ catch (error) {
64
+ logFailure(current, error);
65
+ }
66
+ finally {
67
+ current.refreshedAt = Date.now();
68
+ current.refreshing = null;
69
+ }
70
+ });
71
+ current.refreshing = refreshing;
72
+ return refreshing;
73
+ };
74
+ exports.refreshCustomerVerificationSnapshot = refreshCustomerVerificationSnapshot;
75
+ const stopTimer = (current) => {
76
+ if (current.timer) {
77
+ clearInterval(current.timer);
78
+ current.timer = null;
79
+ }
80
+ };
81
+ /** Stops the periodic refresh (application shutdown). The value stays. */
82
+ const stopCustomerVerificationSnapshot = () => {
83
+ stopTimer(state());
84
+ };
85
+ exports.stopCustomerVerificationSnapshot = stopCustomerVerificationSnapshot;
86
+ /**
87
+ * Registers the reader, loads the toggle once and keeps it fresh every 30 s
88
+ * (application start). `read` returns the effective toggle; `warn` is the
89
+ * container logger. Starting again replaces the reader and the timer.
90
+ */
91
+ const startCustomerVerificationSnapshot = (input) => {
92
+ const current = state();
93
+ current.read = input.read;
94
+ current.warn = input.warn;
95
+ stopTimer(current);
96
+ current.timer = setInterval(() => {
97
+ void (0, exports.refreshCustomerVerificationSnapshot)();
98
+ }, exports.CUSTOMER_VERIFICATION_REFRESH_MS);
99
+ // An idle timer must not keep the process (or a test run) alive.
100
+ current.timer.unref();
101
+ return (0, exports.refreshCustomerVerificationSnapshot)();
102
+ };
103
+ exports.startCustomerVerificationSnapshot = startCustomerVerificationSnapshot;
104
+ /** Synchronous; starts a background refresh when the copy is stale. Never throws. */
105
+ const isCustomerVerificationEnabled = () => {
106
+ const current = state();
107
+ if (current.read &&
108
+ !current.refreshing &&
109
+ (current.refreshedAt === null ||
110
+ Date.now() - current.refreshedAt >= exports.CUSTOMER_VERIFICATION_REFRESH_MS)) {
111
+ void (0, exports.refreshCustomerVerificationSnapshot)();
112
+ }
113
+ return current.enabled;
114
+ };
115
+ exports.isCustomerVerificationEnabled = isCustomerVerificationEnabled;
116
+ /**
117
+ * The value of `authVerificationsPerActor.customer`: Medusa's e-mail
118
+ * verification for the `emailpass` provider while the toggle is on, none
119
+ * while it is off. A fresh array per call.
120
+ */
121
+ const customerVerificationActors = () => (0, exports.isCustomerVerificationEnabled)()
122
+ ? [{ auth_provider: "emailpass", entity_type: "email" }]
123
+ : [];
124
+ exports.customerVerificationActors = customerVerificationActors;
125
+ /**
126
+ * Test hook: sets the value and counts as just refreshed, so it holds for
127
+ * 30 s without a reader; `null` forgets reader and value.
128
+ */
129
+ const setCustomerVerificationEnabledForTests = (enabled) => {
130
+ const current = state();
131
+ if (enabled === null) {
132
+ stopTimer(current);
133
+ current.enabled = false;
134
+ current.refreshedAt = null;
135
+ current.refreshing = null;
136
+ current.read = null;
137
+ current.warn = null;
138
+ return;
139
+ }
140
+ current.enabled = enabled;
141
+ current.refreshedAt = Date.now();
142
+ };
143
+ exports.setCustomerVerificationEnabledForTests = setCustomerVerificationEnabledForTests;
@@ -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", "user.invite"];
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", "user.invite", "auth.verification"];
3
3
  export declare const EmailKindSchema: z.ZodEnum<{
4
4
  "order.received": "order.received";
5
5
  "order.received.admin": "order.received.admin";
@@ -11,5 +11,13 @@ export declare const EmailKindSchema: z.ZodEnum<{
11
11
  "auth.password_reset": "auth.password_reset";
12
12
  "affiliate.consignment_report_due": "affiliate.consignment_report_due";
13
13
  "user.invite": "user.invite";
14
+ "auth.verification": "auth.verification";
14
15
  }>;
15
16
  export type EmailKind = (typeof EMAIL_KINDS)[number];
17
+ /**
18
+ * Kinds whose link carries a one-time token that is never persisted: the
19
+ * subscriber builds the mail from the event, so neither the generic retry
20
+ * job, the automatic dispatch nor an Admin resend may touch them.
21
+ */
22
+ export declare const EPHEMERAL_EMAIL_KINDS: readonly ["auth.password_reset", "auth.verification"];
23
+ export declare const isEphemeralEmailKind: (kind: EmailKind) => boolean;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EmailKindSchema = exports.EMAIL_KINDS = void 0;
3
+ exports.isEphemeralEmailKind = exports.EPHEMERAL_EMAIL_KINDS = exports.EmailKindSchema = exports.EMAIL_KINDS = void 0;
4
4
  const zod_1 = require("@medusajs/framework/zod");
5
5
  exports.EMAIL_KINDS = [
6
6
  "order.received",
@@ -13,5 +13,17 @@ exports.EMAIL_KINDS = [
13
13
  "auth.password_reset",
14
14
  "affiliate.consignment_report_due",
15
15
  "user.invite",
16
+ "auth.verification",
16
17
  ];
17
18
  exports.EmailKindSchema = zod_1.z.enum(exports.EMAIL_KINDS);
19
+ /**
20
+ * Kinds whose link carries a one-time token that is never persisted: the
21
+ * subscriber builds the mail from the event, so neither the generic retry
22
+ * job, the automatic dispatch nor an Admin resend may touch them.
23
+ */
24
+ exports.EPHEMERAL_EMAIL_KINDS = [
25
+ "auth.password_reset",
26
+ "auth.verification",
27
+ ];
28
+ const isEphemeralEmailKind = (kind) => exports.EPHEMERAL_EMAIL_KINDS.includes(kind);
29
+ exports.isEphemeralEmailKind = isEphemeralEmailKind;
@@ -49,6 +49,14 @@ export type ShopEmailConfiguration = Readonly<{
49
49
  passwordResetUrl: (input: Readonly<{
50
50
  locale: string | null;
51
51
  }>) => string;
52
+ /**
53
+ * Storefront page that confirms an account's e-mail address; the mail
54
+ * appends `#token=<token>`. Only needed while the E-Mail plugin's
55
+ * `customer_verification` feature is used; a mail cannot be built without it.
56
+ */
57
+ verifyAccountUrl?: (input: Readonly<{
58
+ locale: string | null;
59
+ }>) => string;
52
60
  renderers?: Partial<EmailRendererMap>;
53
61
  /**
54
62
  * IANA time zone of dates in the default templates (e.g. the order time in