@saasicat/nest 1.0.0-rc.2 → 1.0.0-rc.21

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 (154) hide show
  1. package/README.md +44 -8
  2. package/dist/.build-stamp +1 -1
  3. package/dist/_entries.cjs +10214 -4796
  4. package/dist/admin/index.d.cts +5 -4
  5. package/dist/admin/index.d.ts +5 -4
  6. package/dist/admin/index.js +13 -9
  7. package/dist/{admin-resources.module-CpvzcfCc.d.cts → admin-resources.module-B4XNvdH-.d.cts} +1 -2
  8. package/dist/{admin-resources.module-DF5_Q7RD.d.ts → admin-resources.module-CWqdI9BV.d.ts} +1 -2
  9. package/dist/{admin-stats.service-TYiSUwIR.d.ts → admin-stats.service-CsLNt19Z.d.ts} +11 -7
  10. package/dist/{admin-stats.service-DfKupywR.d.cts → admin-stats.service-DrZrU8IJ.d.cts} +11 -7
  11. package/dist/{aggregation-B3CD0v_d.d.cts → aggregation-ChpZDh0g.d.ts} +39 -6
  12. package/dist/{aggregation-BFzW07DE.d.ts → aggregation-OpnhwzF5.d.cts} +39 -6
  13. package/dist/billing/index.d.cts +448 -108
  14. package/dist/billing/index.d.ts +448 -108
  15. package/dist/billing/index.js +108 -41
  16. package/dist/catalog/index.d.cts +74 -45
  17. package/dist/catalog/index.d.ts +74 -45
  18. package/dist/catalog/index.js +21 -13
  19. package/dist/catalog.module-C9O7BxRq.d.ts +180 -0
  20. package/dist/catalog.module-T-I5Ricz.d.cts +180 -0
  21. package/dist/checkout-offer/index.d.cts +17 -22
  22. package/dist/checkout-offer/index.d.ts +17 -22
  23. package/dist/checkout-offer/index.js +21 -7
  24. package/dist/checkout-offer.module-DFhGy6JO.d.cts +58 -0
  25. package/dist/checkout-offer.module-x7SP_dX4.d.ts +58 -0
  26. package/dist/checkout-offer.service-BeHgYikI.d.ts +242 -0
  27. package/dist/checkout-offer.service-CCVxG9b2.d.cts +242 -0
  28. package/dist/chunk-34XVPR6P.js +222 -0
  29. package/dist/{chunk-AOQJEYOL.js → chunk-3QVGA6JX.js} +3 -1
  30. package/dist/chunk-3RLWML2S.js +896 -0
  31. package/dist/{chunk-TXC3LHHB.js → chunk-4SDSYV4A.js} +21 -13
  32. package/dist/chunk-55QEDAMX.js +946 -0
  33. package/dist/chunk-5CKC7O52.js +140 -0
  34. package/dist/chunk-6PJE7EWO.js +185 -0
  35. package/dist/chunk-7NJKVC5X.js +353 -0
  36. package/dist/{chunk-RUNZ3X4C.js → chunk-AOS2JPDD.js} +1263 -185
  37. package/dist/{chunk-WOVQPXV4.js → chunk-CQ2ZZMTD.js} +702 -725
  38. package/dist/{chunk-NA6O63B7.js → chunk-F5X66HF5.js} +5 -5
  39. package/dist/chunk-FEQZCWUL.js +174 -0
  40. package/dist/{chunk-VIJ5NJWC.js → chunk-G6EZWECL.js} +107 -31
  41. package/dist/chunk-G7RQO2XO.js +0 -0
  42. package/dist/{chunk-N3L3AICU.js → chunk-GPQWGA6B.js} +5 -6
  43. package/dist/chunk-GZTL64QK.js +7 -0
  44. package/dist/{chunk-SABTXESR.js → chunk-J7NJRK3K.js} +17 -1
  45. package/dist/{chunk-SEPN52AD.js → chunk-JBCC6C3M.js} +566 -307
  46. package/dist/{chunk-I7GTA3RX.js → chunk-JQRA724W.js} +1 -1
  47. package/dist/{chunk-WXYJHZCN.js → chunk-JVAEKTJ4.js} +2 -14
  48. package/dist/chunk-KPEMTBOP.js +26 -0
  49. package/dist/chunk-LF6J4YYN.js +301 -0
  50. package/dist/{chunk-LTT736P3.js → chunk-LLYVYRGJ.js} +228 -387
  51. package/dist/{chunk-6Z7JR4EW.js → chunk-M47BNEY2.js} +1897 -633
  52. package/dist/{chunk-KFT5AIIH.js → chunk-NDLC5GYK.js} +206 -24
  53. package/dist/{chunk-O2J2HDXA.js → chunk-OOTEXP47.js} +3 -8
  54. package/dist/chunk-QPVDCKYS.js +702 -0
  55. package/dist/{chunk-NHVDCYK5.js → chunk-R5YCJBHY.js} +71 -106
  56. package/dist/{chunk-XBYAFEOR.js → chunk-S33SO5XX.js} +1 -1
  57. package/dist/chunk-SZ7RFPXA.js +10 -0
  58. package/dist/chunk-TBBZWZQT.js +98 -0
  59. package/dist/chunk-TOEFN7DN.js +295 -0
  60. package/dist/{chunk-7ZEGFL42.js → chunk-WHJSYKNC.js} +6 -6
  61. package/dist/chunk-WOEJ6K7M.js +55 -0
  62. package/dist/{chunk-AU3OOREM.js → chunk-XSSWYPP5.js} +16 -3
  63. package/dist/contract-line-item-money-B0Z3Ogur.d.cts +43 -0
  64. package/dist/contract-line-item-money-B0Z3Ogur.d.ts +43 -0
  65. package/dist/discovery/index.d.cts +2 -2
  66. package/dist/discovery/index.d.ts +2 -2
  67. package/dist/discovery/index.js +6 -6
  68. package/dist/{discovery.scanner-9cMqF95o.d.cts → discovery.scanner-DXKc6JkV.d.cts} +1 -1
  69. package/dist/{discovery.scanner-9cMqF95o.d.ts → discovery.scanner-DXKc6JkV.d.ts} +1 -1
  70. package/dist/{enforce-quota.interceptor-Df0e3OWe.d.cts → enforce-quota.interceptor-BFyVdWe8.d.cts} +6 -6
  71. package/dist/{enforce-quota.interceptor-DW3etVyI.d.ts → enforce-quota.interceptor-GaBXboGx.d.ts} +6 -6
  72. package/dist/entitlement/index.d.cts +27 -4
  73. package/dist/entitlement/index.d.ts +27 -4
  74. package/dist/entitlement/index.js +13 -8
  75. package/dist/index.d.cts +209 -28
  76. package/dist/index.d.ts +209 -28
  77. package/dist/index.js +285 -163
  78. package/dist/issuer-identity.check-CBO0vcsq.d.cts +197 -0
  79. package/dist/issuer-identity.check-Cz9CEMkZ.d.ts +197 -0
  80. package/dist/{module-options-COSEb0VW.d.ts → module-options-DmQ3G4sZ.d.ts} +135 -41
  81. package/dist/{module-options-BG3MzQva.d.cts → module-options-qAOwinHR.d.cts} +135 -41
  82. package/dist/payment-callback.service-CR84Xw1Z.d.cts +125 -0
  83. package/dist/payment-callback.service-CR84Xw1Z.d.ts +125 -0
  84. package/dist/payments/index.cjs +4 -0
  85. package/dist/payments/index.d.cts +187 -0
  86. package/dist/payments/index.d.ts +187 -0
  87. package/dist/payments/index.js +141 -0
  88. package/dist/payments.module-BdUq9ot_.d.cts +57 -0
  89. package/dist/payments.module-CQGv_VG7.d.ts +57 -0
  90. package/dist/plan-catalog-source-DWe-BGY1.d.cts +21 -0
  91. package/dist/plan-catalog-source-DWe-BGY1.d.ts +21 -0
  92. package/dist/{plan-resolution-Cgo_TR1H.d.cts → plan-resolution-CwUy_OqC.d.cts} +14 -0
  93. package/dist/{plan-resolution-Cgo_TR1H.d.ts → plan-resolution-CwUy_OqC.d.ts} +14 -0
  94. package/dist/{plan-versions.service-DChhbK6h.d.ts → plan-versions.service-C0N-Em2W.d.ts} +38 -64
  95. package/dist/{plan-versions.service-TPEtbN0r.d.cts → plan-versions.service-DARLh-qI.d.cts} +38 -64
  96. package/dist/platform/index.d.cts +52 -23
  97. package/dist/platform/index.d.ts +52 -23
  98. package/dist/platform/index.js +57 -36
  99. package/dist/promo/index.d.cts +23 -8
  100. package/dist/promo/index.d.ts +23 -8
  101. package/dist/promo/index.js +15 -7
  102. package/dist/{promo.module-DtvIycpt.d.cts → promo.module-CKkECmU8.d.cts} +9 -3
  103. package/dist/{promo.module-Z8h1PNX-.d.ts → promo.module-DuIJdK3U.d.ts} +9 -3
  104. package/dist/promo.service-BYBu6dy3.d.cts +215 -0
  105. package/dist/promo.service-CFjfF0Xm.d.ts +215 -0
  106. package/dist/registration/index.d.cts +104 -47
  107. package/dist/registration/index.d.ts +104 -47
  108. package/dist/registration/index.js +15 -9
  109. package/dist/subscriber/index.cjs +4 -0
  110. package/dist/subscriber/index.d.cts +28 -0
  111. package/dist/subscriber/index.d.ts +28 -0
  112. package/dist/subscriber/index.js +16 -0
  113. package/dist/subscriber.service-5W6Gi-1B.d.cts +68 -0
  114. package/dist/subscriber.service-5W6Gi-1B.d.ts +68 -0
  115. package/dist/subscription-contract/index.d.cts +4 -2
  116. package/dist/subscription-contract/index.d.ts +4 -2
  117. package/dist/subscription-contract/index.js +13 -5
  118. package/dist/{subscription-contract.module-fDFdQ0jm.d.ts → subscription-contract.module-BfIRpc1o.d.ts} +8 -2
  119. package/dist/{subscription-contract.module-CEK8HS5-.d.cts → subscription-contract.module-DTMuUJYd.d.cts} +8 -2
  120. package/dist/{subscription-contract.service-hD87MKyg.d.cts → subscription-contract.service-CPZrTM9C.d.ts} +32 -3
  121. package/dist/{subscription-contract.service-hD87MKyg.d.ts → subscription-contract.service-CfNApLuP.d.cts} +32 -3
  122. package/dist/tenant-billing.controller-BLqbsONe.d.ts +812 -0
  123. package/dist/tenant-billing.controller-DKbAqTbM.d.cts +812 -0
  124. package/dist/tenant-billing.module-ClbjzocC.d.cts +364 -0
  125. package/dist/tenant-billing.module-DohgJJ2m.d.ts +364 -0
  126. package/dist/tenant-billing.tokens-G-1xOlMr.d.cts +129 -0
  127. package/dist/tenant-billing.tokens-G-1xOlMr.d.ts +129 -0
  128. package/dist/testing/index.d.cts +44 -17
  129. package/dist/testing/index.d.ts +44 -17
  130. package/dist/testing/index.js +189 -40
  131. package/package.json +24 -4
  132. package/dist/catalog.module-BDDO6iwq.d.cts +0 -103
  133. package/dist/catalog.module-BTnMsE6u.d.ts +0 -103
  134. package/dist/checkout-offer.module-CMtiQJTT.d.ts +0 -39
  135. package/dist/checkout-offer.module-CVpWrzbe.d.cts +0 -39
  136. package/dist/checkout-offer.service-BLOv2HOo.d.cts +0 -49
  137. package/dist/checkout-offer.service-BLOv2HOo.d.ts +0 -49
  138. package/dist/chunk-57V6ZTI6.js +0 -22
  139. package/dist/chunk-BSK6YBLI.js +0 -87
  140. package/dist/chunk-DQKCK7DX.js +0 -217
  141. package/dist/chunk-VXEYLNIB.js +0 -632
  142. package/dist/chunk-WUDYIPYH.js +0 -715
  143. package/dist/define-saasicat-DROBe-b1.d.ts +0 -69
  144. package/dist/define-saasicat-vGBCkFys.d.cts +0 -69
  145. package/dist/promo.service-BJbKAmw3.d.cts +0 -112
  146. package/dist/promo.service-BJbKAmw3.d.ts +0 -112
  147. package/dist/tenant-billing.controller-BQ67mlmI.d.ts +0 -438
  148. package/dist/tenant-billing.controller-DS8kB8a7.d.cts +0 -438
  149. package/dist/tenant-billing.module-BznC3XtW.d.cts +0 -299
  150. package/dist/tenant-billing.module-wUbsQSFG.d.ts +0 -299
  151. /package/dist/{chunk-2FR6ZL7R.js → chunk-BHZOH2DY.js} +0 -0
  152. /package/dist/{chunk-2SLTRKXC.js → chunk-DFOW3JVO.js} +0 -0
  153. /package/dist/{chunk-DBZGV3HC.js → chunk-DU56UKEH.js} +0 -0
  154. /package/dist/{chunk-OYNHOY45.js → chunk-GAUEVIAE.js} +0 -0
@@ -1,61 +1,86 @@
1
1
  import {
2
2
  PromoCodesModule
3
- } from "./chunk-TXC3LHHB.js";
3
+ } from "./chunk-4SDSYV4A.js";
4
4
  import {
5
5
  SubscriptionContractModule
6
- } from "./chunk-AU3OOREM.js";
6
+ } from "./chunk-XSSWYPP5.js";
7
7
  import {
8
8
  AdminManifestModule,
9
9
  AdminManifestService,
10
10
  AdminModule,
11
11
  AdminResourcesModule,
12
12
  AdminStatsModule,
13
- MfaService,
14
13
  SuperAdminGuard
15
- } from "./chunk-LTT736P3.js";
14
+ } from "./chunk-LLYVYRGJ.js";
16
15
  import {
17
16
  FEATURE_GUARD_MARKER,
18
17
  PublicCatalogModule,
19
18
  REQUIRE_FEATURE_KEY,
20
19
  SubscriptionBundleModule,
21
20
  TenantBillingModule,
22
- isPlatformFeatureGuard
23
- } from "./chunk-6Z7JR4EW.js";
21
+ catalogSource,
22
+ isPlatformFeatureGuard,
23
+ loadPlanCatalogFromFile
24
+ } from "./chunk-M47BNEY2.js";
24
25
  import {
25
- CatalogModule
26
- } from "./chunk-WOVQPXV4.js";
26
+ CatalogModule,
27
+ WebAuditLogger,
28
+ actorTagOf,
29
+ defaultActorFromRequest
30
+ } from "./chunk-CQ2ZZMTD.js";
31
+ import {
32
+ MfaService
33
+ } from "./chunk-34XVPR6P.js";
27
34
  import {
28
35
  CheckoutOfferModule
29
- } from "./chunk-WUDYIPYH.js";
36
+ } from "./chunk-TOEFN7DN.js";
30
37
  import {
31
- SaaSiCatPublicRoute
32
- } from "./chunk-YSC46BGM.js";
38
+ DiscoveryModule
39
+ } from "./chunk-S33SO5XX.js";
33
40
  import {
34
41
  EntitlementModule
35
- } from "./chunk-7ZEGFL42.js";
42
+ } from "./chunk-WHJSYKNC.js";
36
43
  import {
37
- LimitExceededError
38
- } from "./chunk-VIJ5NJWC.js";
44
+ LimitExceededError,
45
+ cancellationHasLanded
46
+ } from "./chunk-G6EZWECL.js";
39
47
  import {
40
- PLAN_CATALOG_TOKEN,
41
- PlanCatalogModule
42
- } from "./chunk-DQKCK7DX.js";
43
- import {
44
- codedError
45
- } from "./chunk-57V6ZTI6.js";
48
+ findPlan
49
+ } from "./chunk-TBBZWZQT.js";
46
50
  import {
47
- asProvider
48
- } from "./chunk-SABTXESR.js";
49
- import {
50
- DiscoveryModule
51
- } from "./chunk-XBYAFEOR.js";
51
+ SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
52
+ } from "./chunk-VBRYPU7V.js";
52
53
  import {
53
54
  ENFORCE_QUOTA_KEY
54
55
  } from "./chunk-QUZU2M33.js";
56
+ import {
57
+ PaymentsModule
58
+ } from "./chunk-QPVDCKYS.js";
59
+ import {
60
+ PaymentGatewayRegistry
61
+ } from "./chunk-7NJKVC5X.js";
62
+ import {
63
+ RLS_BYPASS_PORT_TOKEN
64
+ } from "./chunk-SZ7RFPXA.js";
65
+ import {
66
+ SaaSiCatPublicRoute
67
+ } from "./chunk-YSC46BGM.js";
68
+ import {
69
+ PLAN_CATALOG_SETTINGS_TOKEN,
70
+ PLAN_CATALOG_SOURCE_TOKEN,
71
+ PlanCatalogModule
72
+ } from "./chunk-FEQZCWUL.js";
73
+ import {
74
+ asProvider,
75
+ codedError
76
+ } from "./chunk-J7NJRK3K.js";
55
77
  import {
56
78
  __name
57
79
  } from "./chunk-SHUYVCID.js";
58
80
 
81
+ // src/core/email.tokens.ts
82
+ var EMAIL_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/EmailPort");
83
+
59
84
  // src/setup/setup.tokens.ts
60
85
  var SETUP_PROVISIONING_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/SetupProvisioningPort");
61
86
  var SETUP_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/SetupConfig");
@@ -352,9 +377,9 @@ SetupModule = _ts_decorate3([
352
377
  Module({})
353
378
  ], SetupModule);
354
379
 
355
- // src/platform/enforcement-chain.check.ts
356
- import { Inject as Inject2, Injectable as Injectable2, Optional } from "@nestjs/common";
357
- import { DiscoveryService, MetadataScanner } from "@nestjs/core";
380
+ // src/settings/settings-change-notifier.ts
381
+ import { Inject as Inject2, Injectable as Injectable2, Logger, Optional } from "@nestjs/common";
382
+ import { diffSettings } from "@saasicat/core";
358
383
  function _ts_decorate4(decorators, target, key, desc) {
359
384
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
360
385
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -372,6 +397,527 @@ function _ts_param3(paramIndex, decorator) {
372
397
  };
373
398
  }
374
399
  __name(_ts_param3, "_ts_param");
400
+ var MAIL_SEND_TIMEOUT_MS = 3e4;
401
+ function withinTime(promise, ms, to) {
402
+ return new Promise((resolve, reject) => {
403
+ const timer = setTimeout(() => reject(new Error(`no answer from the email port within ${ms} ms for ${to}`)), ms);
404
+ timer.unref();
405
+ promise.then((value) => {
406
+ clearTimeout(timer);
407
+ resolve(value);
408
+ }, (error) => {
409
+ clearTimeout(timer);
410
+ reject(error instanceof Error ? error : new Error(String(error)));
411
+ });
412
+ });
413
+ }
414
+ __name(withinTime, "withinTime");
415
+ var SettingsChangeNotifier = class _SettingsChangeNotifier {
416
+ static {
417
+ __name(this, "SettingsChangeNotifier");
418
+ }
419
+ settings;
420
+ email;
421
+ logger = new Logger(_SettingsChangeNotifier.name);
422
+ constructor(settings, email = null) {
423
+ this.settings = settings;
424
+ this.email = email;
425
+ }
426
+ /** The addresses `config/saas.yaml` names under `notifications.settingsChanged`. */
427
+ get addresses() {
428
+ return this.settings.notifications?.settingsChanged ?? [];
429
+ }
430
+ /**
431
+ * Which of the two it is, said once at boot — and only where the question
432
+ * arises. An installation naming nobody asked for in-app only, and a line
433
+ * about it would be a warning at a correct configuration.
434
+ */
435
+ reportModeAtBoot() {
436
+ if (this.addresses.length === 0) return;
437
+ if (this.email) {
438
+ this.logger.log(`Settings changes are mailed to ${this.addresses.length} address(es) from config/saas.yaml#notifications.settingsChanged, and recorded in the application.`);
439
+ return;
440
+ }
441
+ this.logger.warn(`config/saas.yaml#notifications.settingsChanged names ${this.addresses.length} address(es), but no email port is bound (\`adapters.email\`). Changes are recorded in the application only; nobody is mailed.`);
442
+ }
443
+ /**
444
+ * Mails everybody the change concerns: the addresses the file names now,
445
+ * and — when the list itself is what moved — the ones it named before. An
446
+ * address taken off the list is told that once; otherwise whoever can edit
447
+ * the file could point the channel elsewhere and the people it pointed at
448
+ * would go quiet without a word, which is the failure this exists to
449
+ * prevent. Never throws: a mail that fails is logged.
450
+ */
451
+ async notify(change) {
452
+ const addresses = recipientsOf(this.addresses, change.previous);
453
+ if (addresses.length === 0) return {
454
+ kind: "nobody-to-tell"
455
+ };
456
+ if (!this.email) return {
457
+ kind: "no-email-port",
458
+ addresses
459
+ };
460
+ const message = describeChange(this.settings.app.name, change);
461
+ const failed = [];
462
+ for (const to of addresses) {
463
+ try {
464
+ await withinTime(this.email.send({
465
+ to,
466
+ ...message
467
+ }), MAIL_SEND_TIMEOUT_MS, to);
468
+ } catch (error) {
469
+ failed.push(to);
470
+ this.logger.error(`The settings-change mail to ${to} could not be sent; the change is recorded in the application regardless.`, error instanceof Error ? error.stack : String(error));
471
+ }
472
+ }
473
+ return {
474
+ kind: "sent",
475
+ addresses,
476
+ failed
477
+ };
478
+ }
479
+ };
480
+ SettingsChangeNotifier = _ts_decorate4([
481
+ Injectable2(),
482
+ _ts_param3(0, Inject2(PLAN_CATALOG_SETTINGS_TOKEN)),
483
+ _ts_param3(1, Optional()),
484
+ _ts_param3(1, Inject2(EMAIL_PORT_TOKEN)),
485
+ _ts_metadata3("design:type", Function),
486
+ _ts_metadata3("design:paramtypes", [
487
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
488
+ Object
489
+ ])
490
+ ], SettingsChangeNotifier);
491
+ function recipientsOf(running, previous) {
492
+ return [
493
+ .../* @__PURE__ */ new Set([
494
+ ...running,
495
+ ...addressListIn(previous)
496
+ ])
497
+ ];
498
+ }
499
+ __name(recipientsOf, "recipientsOf");
500
+ function addressListIn(settings) {
501
+ const notifications = settings.notifications;
502
+ if (typeof notifications !== "object" || notifications === null) return [];
503
+ const list2 = notifications.settingsChanged;
504
+ return Array.isArray(list2) ? list2.filter((a) => typeof a === "string") : [];
505
+ }
506
+ __name(addressListIn, "addressListIn");
507
+ function describeChange(appName, change) {
508
+ const lines = diffSettings(change.previous, change.current).map((d) => ` ${d.path}: ${JSON.stringify(d.before)} \u2192 ${JSON.stringify(d.after)}`);
509
+ return {
510
+ subject: `[${appName}] The configuration changed at the last start`,
511
+ text: [
512
+ `${appName} started at ${change.noticedAt.toISOString()} and found its settings changed`,
513
+ `since the previous start. The new values are running. What moved:`,
514
+ "",
515
+ ...lines,
516
+ "",
517
+ `Source: ${change.source}`,
518
+ "The change is recorded in the administration under Settings until somebody acknowledges it."
519
+ ].join("\n")
520
+ };
521
+ }
522
+ __name(describeChange, "describeChange");
523
+
524
+ // src/settings/settings-fingerprint.ts
525
+ import { createHash } from "crypto";
526
+ import { canonicalJson } from "@saasicat/core";
527
+ function fingerprintOf(settings) {
528
+ return `sha256-${createHash("sha256").update(canonicalJson(settings)).digest("hex")}`;
529
+ }
530
+ __name(fingerprintOf, "fingerprintOf");
531
+
532
+ // src/settings/settings.tokens.ts
533
+ var APPLIED_SETTINGS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/AppliedSettingsPort");
534
+ var SETTINGS_SOURCE_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/SettingsSource");
535
+
536
+ // src/settings/applied-settings.recorder.ts
537
+ import { Inject as Inject3, Injectable as Injectable3, Logger as Logger2, Optional as Optional2 } from "@nestjs/common";
538
+ import { diffSettings as diffSettings2, settingsSubtreeOf } from "@saasicat/core";
539
+ function _ts_decorate5(decorators, target, key, desc) {
540
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
541
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
542
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
543
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
544
+ }
545
+ __name(_ts_decorate5, "_ts_decorate");
546
+ function _ts_metadata4(k, v) {
547
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
548
+ }
549
+ __name(_ts_metadata4, "_ts_metadata");
550
+ function _ts_param4(paramIndex, decorator) {
551
+ return function(target, key) {
552
+ decorator(target, key, paramIndex);
553
+ };
554
+ }
555
+ __name(_ts_param4, "_ts_param");
556
+ var MAIL_BOOT_BUDGET_MS = 2e3;
557
+ var RECORD_ATTEMPTS = 5;
558
+ function pause(ms) {
559
+ return new Promise((resolve) => setTimeout(resolve, ms).unref());
560
+ }
561
+ __name(pause, "pause");
562
+ var AppliedSettingsRecorder = class _AppliedSettingsRecorder {
563
+ static {
564
+ __name(this, "AppliedSettingsRecorder");
565
+ }
566
+ settings;
567
+ source;
568
+ port;
569
+ notifier;
570
+ logger = new Logger2(_AppliedSettingsRecorder.name);
571
+ constructor(settings, source, port = null, notifier = null) {
572
+ this.settings = settings;
573
+ this.source = source;
574
+ this.port = port;
575
+ this.notifier = notifier;
576
+ }
577
+ /** Whether this installation keeps a record at all. */
578
+ get recording() {
579
+ return this.port !== null;
580
+ }
581
+ async onApplicationBootstrap() {
582
+ if (!this.port) {
583
+ this.logger.warn("The applied settings are not recorded: the persistence adapter provides no `core.appliedSettings` port, so the platform cannot say when its configuration was applied or notice that it changed.");
584
+ return;
585
+ }
586
+ this.notifier?.reportModeAtBoot();
587
+ try {
588
+ const outcome = await this.record(this.port, () => /* @__PURE__ */ new Date());
589
+ this.report(outcome);
590
+ if (outcome.kind === "changed" && this.notifier) {
591
+ await this.tellSomebody(this.notifier, outcome.change);
592
+ }
593
+ } catch (error) {
594
+ this.logger.error(`The applied settings could not be recorded \u2014 the configuration in ${this.source} is running, but the record of it is stale.`, error instanceof Error ? error.stack : String(error));
595
+ }
596
+ }
597
+ /**
598
+ * Mails the change without holding the boot for it.
599
+ *
600
+ * Nest resolves `app.listen()` only once every bootstrap hook has settled,
601
+ * and an SMTP host that does not answer would otherwise turn a changed
602
+ * setting into an installation that does not start. So the boot waits a
603
+ * moment — long enough for a working adapter to hand the mail over, so
604
+ * the log reads in order — and then goes on; a slow adapter finishes on
605
+ * its own, and `notify()` bounds each send and logs what timed out.
606
+ */
607
+ async tellSomebody(notifier, change) {
608
+ await Promise.race([
609
+ notifier.notify(change),
610
+ pause(MAIL_BOOT_BUDGET_MS)
611
+ ]);
612
+ }
613
+ /**
614
+ * Compares the running settings with the record and writes what follows.
615
+ *
616
+ * `clock` is read once per attempt, after the record has been read and
617
+ * right before it is written, so the moment a row carries is the moment
618
+ * of the write that produced it. Read once when the start began, it would
619
+ * date a write by its start — and a start whose write landed after
620
+ * another's could carry the earlier moment, listing the changes in an
621
+ * order the record never went through.
622
+ */
623
+ async record(port, clock) {
624
+ const settings = settingsSubtreeOf(this.settings);
625
+ const fingerprint = fingerprintOf(settings);
626
+ for (let attempt = 1; attempt <= RECORD_ATTEMPTS; attempt++) {
627
+ const previous = await port.readApplied();
628
+ const running = {
629
+ fingerprint,
630
+ settings,
631
+ source: this.source,
632
+ appliedAt: clock()
633
+ };
634
+ const outcome = await this.reconcile(port, previous, running);
635
+ if (outcome) return outcome;
636
+ }
637
+ throw new Error(`The applied settings record moved ${RECORD_ATTEMPTS} times while this start was writing it. Either concurrent starts are running different configuration files, or the persistence adapter never confirms the guarded write: \`writeApplied\` must answer true, and \`recordChange\` the change, while the record still carries the fingerprint that was read.`);
638
+ }
639
+ /**
640
+ * What one read of the record calls for, written. `null` means the write
641
+ * found the record moved since `previous` was read, and wrote nothing.
642
+ */
643
+ async reconcile(port, previous, running) {
644
+ if (previous?.fingerprint === running.fingerprint) {
645
+ if (previous.source === running.source) return {
646
+ kind: "unchanged",
647
+ record: previous
648
+ };
649
+ const relocated = {
650
+ ...previous,
651
+ source: running.source
652
+ };
653
+ const written = await port.writeApplied(relocated, previous.fingerprint);
654
+ return written ? {
655
+ kind: "unchanged",
656
+ record: relocated
657
+ } : null;
658
+ }
659
+ if (!previous) {
660
+ const written = await port.writeApplied(running, null);
661
+ return written ? {
662
+ kind: "first-record",
663
+ record: running
664
+ } : null;
665
+ }
666
+ const change = await port.recordChange({
667
+ noticedAt: running.appliedAt,
668
+ source: running.source,
669
+ previous: previous.settings,
670
+ current: running.settings
671
+ }, running, previous.fingerprint);
672
+ return change ? {
673
+ kind: "changed",
674
+ record: running,
675
+ change
676
+ } : null;
677
+ }
678
+ report(outcome) {
679
+ switch (outcome.kind) {
680
+ case "first-record":
681
+ this.logger.log(`Applied settings recorded for the first time, from ${this.source}.`);
682
+ return;
683
+ case "changed": {
684
+ const lines = diffSettings2(outcome.change.previous, outcome.change.current).map((d) => `${d.path}: ${JSON.stringify(d.before)} \u2192 ${JSON.stringify(d.after)}`);
685
+ this.logger.warn(`The settings changed since the last start (${this.source}):
686
+ ` + lines.join("\n "));
687
+ return;
688
+ }
689
+ default:
690
+ return;
691
+ }
692
+ }
693
+ };
694
+ AppliedSettingsRecorder = _ts_decorate5([
695
+ Injectable3(),
696
+ _ts_param4(0, Inject3(PLAN_CATALOG_SETTINGS_TOKEN)),
697
+ _ts_param4(1, Inject3(SETTINGS_SOURCE_TOKEN)),
698
+ _ts_param4(2, Optional2()),
699
+ _ts_param4(2, Inject3(APPLIED_SETTINGS_PORT_TOKEN)),
700
+ _ts_param4(3, Optional2()),
701
+ _ts_param4(3, Inject3(SettingsChangeNotifier)),
702
+ _ts_metadata4("design:type", Function),
703
+ _ts_metadata4("design:paramtypes", [
704
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
705
+ String,
706
+ Object,
707
+ Object
708
+ ])
709
+ ], AppliedSettingsRecorder);
710
+
711
+ // src/settings/settings.controller.ts
712
+ import { Controller as Controller2, Get as Get2, Inject as Inject4, NotFoundException, Optional as Optional3, Param, Post as Post2, Req, UseGuards } from "@nestjs/common";
713
+ import { SETTINGS_ERROR_CODES } from "@saasicat/core";
714
+ import { diffSettings as diffSettings3, settingsSubtreeOf as settingsSubtreeOf2 } from "@saasicat/core";
715
+ function _ts_decorate6(decorators, target, key, desc) {
716
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
717
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
718
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
719
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
720
+ }
721
+ __name(_ts_decorate6, "_ts_decorate");
722
+ function _ts_metadata5(k, v) {
723
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
724
+ }
725
+ __name(_ts_metadata5, "_ts_metadata");
726
+ function _ts_param5(paramIndex, decorator) {
727
+ return function(target, key) {
728
+ decorator(target, key, paramIndex);
729
+ };
730
+ }
731
+ __name(_ts_param5, "_ts_param");
732
+ var RECENT_CHANGES = 20;
733
+ function buildSettingsController(guards) {
734
+ let GeneratedSettingsController = class GeneratedSettingsController {
735
+ static {
736
+ __name(this, "GeneratedSettingsController");
737
+ }
738
+ settings;
739
+ source;
740
+ port;
741
+ audit;
742
+ // Explicit @Inject: tsup/esbuild emit no `design:paramtypes`.
743
+ constructor(settings, source, port, audit = null) {
744
+ this.settings = settings;
745
+ this.source = source;
746
+ this.port = port;
747
+ this.audit = audit;
748
+ }
749
+ async getSettings() {
750
+ const settings = settingsSubtreeOf2(this.settings);
751
+ const fingerprint = fingerprintOf(settings);
752
+ const base = {
753
+ source: this.source,
754
+ fingerprint,
755
+ settings
756
+ };
757
+ if (!this.port) return {
758
+ ...base,
759
+ recorded: false,
760
+ appliedAt: null,
761
+ changes: []
762
+ };
763
+ const [applied, changes] = await Promise.all([
764
+ this.port.readApplied(),
765
+ this.port.listChanges({
766
+ limit: RECENT_CHANGES
767
+ })
768
+ ]);
769
+ return {
770
+ ...base,
771
+ recorded: true,
772
+ appliedAt: applied?.fingerprint === fingerprint ? applied.appliedAt.toISOString() : null,
773
+ changes: changes.map(toChangeView)
774
+ };
775
+ }
776
+ /**
777
+ * Marks a change as seen. The record survives until this is called —
778
+ * that is what makes it a record rather than a log line — and the
779
+ * acknowledgement keeps its first author: a second call changes nothing
780
+ * and answers the record as it stands.
781
+ */
782
+ async acknowledge(id, request) {
783
+ if (!this.port) {
784
+ throw new NotFoundException(codedError(SETTINGS_ERROR_CODES.SETTINGS_CHANGE_NOT_FOUND));
785
+ }
786
+ const acknowledged = await this.port.acknowledgeChange(id, this.audit?.actorTagFromRequest(request) ?? actorTagOf(defaultActorFromRequest(request)), /* @__PURE__ */ new Date());
787
+ if (!acknowledged) {
788
+ throw new NotFoundException(codedError(SETTINGS_ERROR_CODES.SETTINGS_CHANGE_NOT_FOUND));
789
+ }
790
+ await this.audit?.logFromRequest(request, "SettingsChange", id, "SETTINGS_CHANGE_ACKNOWLEDGE");
791
+ return toChangeView(acknowledged);
792
+ }
793
+ };
794
+ _ts_decorate6([
795
+ Get2("settings"),
796
+ _ts_metadata5("design:type", Function),
797
+ _ts_metadata5("design:paramtypes", []),
798
+ _ts_metadata5("design:returntype", Promise)
799
+ ], GeneratedSettingsController.prototype, "getSettings", null);
800
+ _ts_decorate6([
801
+ Post2("settings/changes/:id/acknowledge"),
802
+ _ts_param5(0, Param("id")),
803
+ _ts_param5(1, Req()),
804
+ _ts_metadata5("design:type", Function),
805
+ _ts_metadata5("design:paramtypes", [
806
+ String,
807
+ Object
808
+ ]),
809
+ _ts_metadata5("design:returntype", Promise)
810
+ ], GeneratedSettingsController.prototype, "acknowledge", null);
811
+ GeneratedSettingsController = _ts_decorate6([
812
+ Controller2("admin"),
813
+ UseGuards(...guards),
814
+ _ts_param5(0, Inject4(PLAN_CATALOG_SETTINGS_TOKEN)),
815
+ _ts_param5(1, Inject4(SETTINGS_SOURCE_TOKEN)),
816
+ _ts_param5(2, Inject4(APPLIED_SETTINGS_PORT_TOKEN)),
817
+ _ts_param5(3, Optional3()),
818
+ _ts_param5(3, Inject4(WebAuditLogger)),
819
+ _ts_metadata5("design:type", Function),
820
+ _ts_metadata5("design:paramtypes", [
821
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
822
+ String,
823
+ Object,
824
+ Object
825
+ ])
826
+ ], GeneratedSettingsController);
827
+ return GeneratedSettingsController;
828
+ }
829
+ __name(buildSettingsController, "buildSettingsController");
830
+ function toChangeView(change) {
831
+ return {
832
+ id: change.id,
833
+ noticedAt: change.noticedAt.toISOString(),
834
+ source: change.source,
835
+ differences: diffSettings3(change.previous, change.current),
836
+ acknowledgedAt: change.acknowledgedAt?.toISOString() ?? null,
837
+ acknowledgedBy: change.acknowledgedBy
838
+ };
839
+ }
840
+ __name(toChangeView, "toChangeView");
841
+
842
+ // src/settings/settings.module.ts
843
+ import { Module as Module2 } from "@nestjs/common";
844
+ function _ts_decorate7(decorators, target, key, desc) {
845
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
846
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
847
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
848
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
849
+ }
850
+ __name(_ts_decorate7, "_ts_decorate");
851
+ var SettingsModule = class _SettingsModule {
852
+ static {
853
+ __name(this, "SettingsModule");
854
+ }
855
+ static forRoot(options) {
856
+ const providers = [
857
+ {
858
+ provide: SETTINGS_SOURCE_TOKEN,
859
+ useValue: options.source
860
+ },
861
+ // Bound to null rather than left unbound: the controller and the
862
+ // recorder then read one shape, and `@Optional()` is not asked to
863
+ // paper over a provider that is sometimes there.
864
+ options.port ? asProvider(APPLIED_SETTINGS_PORT_TOKEN, options.port) : {
865
+ provide: APPLIED_SETTINGS_PORT_TOKEN,
866
+ useValue: null
867
+ },
868
+ options.email ? asProvider(EMAIL_PORT_TOKEN, options.email) : {
869
+ provide: EMAIL_PORT_TOKEN,
870
+ useValue: null
871
+ },
872
+ SettingsChangeNotifier,
873
+ AppliedSettingsRecorder,
874
+ // The acknowledgement is an operator's action and is audited like
875
+ // one. Best-effort: the logger writes nothing where the admin core
876
+ // is not in scope, and never fails the request.
877
+ WebAuditLogger
878
+ ];
879
+ return {
880
+ module: _SettingsModule,
881
+ imports: options.imports ?? [],
882
+ controllers: options.includeController === false ? [] : [
883
+ buildSettingsController(options.controller.guards)
884
+ ],
885
+ providers,
886
+ exports: [
887
+ AppliedSettingsRecorder,
888
+ SettingsChangeNotifier,
889
+ WebAuditLogger,
890
+ APPLIED_SETTINGS_PORT_TOKEN,
891
+ EMAIL_PORT_TOKEN,
892
+ SETTINGS_SOURCE_TOKEN
893
+ ]
894
+ };
895
+ }
896
+ };
897
+ SettingsModule = _ts_decorate7([
898
+ Module2({})
899
+ ], SettingsModule);
900
+
901
+ // src/platform/enforcement-chain.check.ts
902
+ import { Inject as Inject5, Injectable as Injectable4, Optional as Optional4 } from "@nestjs/common";
903
+ import { DiscoveryService, MetadataScanner } from "@nestjs/core";
904
+ function _ts_decorate8(decorators, target, key, desc) {
905
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
906
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
907
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
908
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
909
+ }
910
+ __name(_ts_decorate8, "_ts_decorate");
911
+ function _ts_metadata6(k, v) {
912
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
913
+ }
914
+ __name(_ts_metadata6, "_ts_metadata");
915
+ function _ts_param6(paramIndex, decorator) {
916
+ return function(target, key) {
917
+ decorator(target, key, paramIndex);
918
+ };
919
+ }
920
+ __name(_ts_param6, "_ts_param");
375
921
  var GUARDS_METADATA = "__guards__";
376
922
  var METHOD_METADATA = "method";
377
923
  var ENFORCEMENT_CHAIN_STATE_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/EnforcementChainState");
@@ -460,14 +1006,14 @@ Two things this cannot see, and the way out of each: a guard of your own that wr
460
1006
  return annotated;
461
1007
  }
462
1008
  };
463
- EnforcementChainCheck = _ts_decorate4([
464
- Injectable2(),
465
- _ts_param3(0, Inject2(DiscoveryService)),
466
- _ts_param3(1, Inject2(MetadataScanner)),
467
- _ts_param3(2, Optional()),
468
- _ts_param3(2, Inject2(ENFORCEMENT_CHAIN_STATE_TOKEN)),
469
- _ts_metadata3("design:type", Function),
470
- _ts_metadata3("design:paramtypes", [
1009
+ EnforcementChainCheck = _ts_decorate8([
1010
+ Injectable4(),
1011
+ _ts_param6(0, Inject5(DiscoveryService)),
1012
+ _ts_param6(1, Inject5(MetadataScanner)),
1013
+ _ts_param6(2, Optional4()),
1014
+ _ts_param6(2, Inject5(ENFORCEMENT_CHAIN_STATE_TOKEN)),
1015
+ _ts_metadata6("design:type", Function),
1016
+ _ts_metadata6("design:paramtypes", [
471
1017
  typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
472
1018
  typeof MetadataScanner === "undefined" ? Object : MetadataScanner,
473
1019
  typeof EnforcementChainState === "undefined" ? Object : EnforcementChainState
@@ -504,8 +1050,367 @@ function readGuards(target) {
504
1050
  }
505
1051
  __name(readGuards, "readGuards");
506
1052
 
1053
+ // src/platform/issuer-identity.check.ts
1054
+ import { Inject as Inject6, Injectable as Injectable5, Logger as Logger3, Optional as Optional5 } from "@nestjs/common";
1055
+ import { classifyIssuerChange, recordedIssuerIdentity } from "@saasicat/core";
1056
+ function _ts_decorate9(decorators, target, key, desc) {
1057
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1058
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1059
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1060
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1061
+ }
1062
+ __name(_ts_decorate9, "_ts_decorate");
1063
+ function _ts_metadata7(k, v) {
1064
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1065
+ }
1066
+ __name(_ts_metadata7, "_ts_metadata");
1067
+ function _ts_param7(paramIndex, decorator) {
1068
+ return function(target, key) {
1069
+ decorator(target, key, paramIndex);
1070
+ };
1071
+ }
1072
+ __name(_ts_param7, "_ts_param");
1073
+ var CONTRACTS_NAMED = 5;
1074
+ var NOT_RECORDED = "this installation records no applied settings";
1075
+ var ISSUER_BLOCK_KEYS = [
1076
+ "legalName",
1077
+ "addressLine1",
1078
+ "addressLine2",
1079
+ "postalCode",
1080
+ "city",
1081
+ "country",
1082
+ "vatId",
1083
+ "taxNumber"
1084
+ ];
1085
+ var IssuerIdentityInspector = class _IssuerIdentityInspector {
1086
+ static {
1087
+ __name(this, "IssuerIdentityInspector");
1088
+ }
1089
+ fileSettings;
1090
+ source;
1091
+ settings;
1092
+ contracts;
1093
+ rlsBypass;
1094
+ logger = new Logger3(_IssuerIdentityInspector.name);
1095
+ /** This start's answer, settled the first time anything asks for it. */
1096
+ verdict = null;
1097
+ constructor(fileSettings, source, settings = null, contracts = null, rlsBypass = null) {
1098
+ this.fileSettings = fileSettings;
1099
+ this.source = source;
1100
+ this.settings = settings;
1101
+ this.contracts = contracts;
1102
+ this.rlsBypass = rlsBypass;
1103
+ }
1104
+ /**
1105
+ * What the issuer in the file is, against the identity the record holds.
1106
+ *
1107
+ * Answered once per process, and the cached answer is the point rather than
1108
+ * the saved query: `AppliedSettingsRecorder` replaces the record moments
1109
+ * later, in a bootstrap hook, so a second comparison would find the file
1110
+ * agreeing with what this very start wrote. A reader asking afterwards —
1111
+ * `<app> doctor`, a health endpoint — would then be told that nothing had
1112
+ * moved, on a start that had just carried a correction through. What this
1113
+ * start found is a fact about this start, so it is settled when the start
1114
+ * asks and does not change afterwards.
1115
+ */
1116
+ inspect() {
1117
+ return this.verdict ??= this.compare();
1118
+ }
1119
+ /**
1120
+ * How many contracts are running, for a report that says what changing the
1121
+ * identity would cost. A query of its own rather than a field on the
1122
+ * verdict: a start needs this only to name the contracts in a refusal, and
1123
+ * no boot should pay for a number nobody reads. `null` where they are not
1124
+ * known.
1125
+ */
1126
+ async runningContractCount() {
1127
+ const running = await this.readRunning(0);
1128
+ return running.known ? running.contracts.total : null;
1129
+ }
1130
+ async compare() {
1131
+ if (!this.settings) return {
1132
+ kind: "not-compared",
1133
+ why: NOT_RECORDED
1134
+ };
1135
+ const recorded = await this.readRecorded();
1136
+ if (!recorded.read) return {
1137
+ kind: "not-compared",
1138
+ why: recorded.why
1139
+ };
1140
+ const change = classifyIssuerChange(recordedIssuerIdentity(recorded.settings), this.fileSettings.issuer);
1141
+ if (change.kind !== "undeclared") return {
1142
+ kind: "settled",
1143
+ change
1144
+ };
1145
+ const running = await this.readRunning();
1146
+ return {
1147
+ kind: "refused",
1148
+ change,
1149
+ running: running.known ? running.contracts : null,
1150
+ refusal: refusalFor(change, running, this.source, recorded.settings?.issuer)
1151
+ };
1152
+ }
1153
+ /**
1154
+ * The settings of the last start, or what stopped this from reading them.
1155
+ *
1156
+ * A read that fails while the file names an issuer takes the boot down: a
1157
+ * guard that cannot see is not a guard that saw nothing, and an installation
1158
+ * that puts a legal entity on every contract it concludes should not conclude
1159
+ * one while nothing can tell whether that entity is still the recorded one.
1160
+ *
1161
+ * Where the file names no issuer, the same failure is only a warning. There
1162
+ * is then no entity to conclude a contract on behalf of, and the promise the
1163
+ * record makes — an installation whose record cannot be kept still runs the
1164
+ * right configuration — is worth more than a boot refused over a comparison
1165
+ * with nothing on one side. What that leaves uncaught is narrow and worth
1166
+ * naming: an operator who removes the issuer block while contracts run, in
1167
+ * the same start where the record cannot be read.
1168
+ */
1169
+ async readRecorded() {
1170
+ try {
1171
+ const record = await this.settings.readApplied();
1172
+ return {
1173
+ read: true,
1174
+ settings: record?.settings ?? null
1175
+ };
1176
+ } catch (error) {
1177
+ const why = `the applied settings could not be read: ${messageOf(error)}`;
1178
+ if (this.fileSettings.issuer) {
1179
+ throw new Error(`The issuer named in ${this.source} could not be compared with the one this installation recorded, because ${why}`, {
1180
+ cause: error
1181
+ });
1182
+ }
1183
+ return {
1184
+ read: false,
1185
+ why
1186
+ };
1187
+ }
1188
+ }
1189
+ /**
1190
+ * The contracts a refusal is about. Best effort on purpose: the refusal is
1191
+ * already decided, and losing it because the list behind it could not be
1192
+ * read would replace a message an operator can act on with one they cannot.
1193
+ * Where they are not known, the message says which of the two it is —
1194
+ * "nothing here writes contracts" and "the query failed" send an operator
1195
+ * looking in different places.
1196
+ */
1197
+ async readRunning(named = CONTRACTS_NAMED) {
1198
+ if (!this.contracts) {
1199
+ return {
1200
+ known: false,
1201
+ why: "This installation writes no contracts."
1202
+ };
1203
+ }
1204
+ try {
1205
+ const listed = await this.withBypass(() => this.contracts.listRunningIssuers(named));
1206
+ return {
1207
+ known: true,
1208
+ contracts: listed
1209
+ };
1210
+ } catch (error) {
1211
+ const why = `The contracts still running could not be read: ${messageOf(error)}`;
1212
+ this.logger.warn(why);
1213
+ return {
1214
+ known: false,
1215
+ why
1216
+ };
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Inside the bypass frame where one is bound, and plainly where none is.
1221
+ *
1222
+ * An installation without row-level security binds a port that only calls
1223
+ * through; one that has it cannot be read platform-wide without this.
1224
+ * `@Optional()` rather than required because a hand-wired application may
1225
+ * have no `AdminModule` in scope, and a boot check is not the place to
1226
+ * refuse to construct over a message.
1227
+ */
1228
+ withBypass(read) {
1229
+ return this.rlsBypass ? this.rlsBypass.runWithBypass(read) : read();
1230
+ }
1231
+ /**
1232
+ * The line a start that continues is worth, and nothing where it is worth
1233
+ * none. Public because the hook beside this class calls it; not an offer.
1234
+ *
1235
+ * @internal
1236
+ */
1237
+ report(verdict) {
1238
+ if (verdict.kind === "not-compared") {
1239
+ this.logger.warn(`The issuer in the configuration is compared with nothing \u2014 ${verdict.why} \u2014 so a start that names another legal entity than the last one is not refused.`);
1240
+ return;
1241
+ }
1242
+ const change = verdict.change;
1243
+ if (change.kind === "first-naming") {
1244
+ this.logger.log(`The issuer of every contract concluded from now on is '${change.identity.legalName}'.`);
1245
+ return;
1246
+ }
1247
+ if (change.kind === "corrected") {
1248
+ this.logger.log(`The issuer's ${listFields(change.moved)} changed as a declared correction of the same legal entity: ${change.reason}. Contracts already concluded keep the copy they name; what is issued from now on carries the corrected identity.`);
1249
+ }
1250
+ }
1251
+ };
1252
+ IssuerIdentityInspector = _ts_decorate9([
1253
+ Injectable5(),
1254
+ _ts_param7(0, Inject6(PLAN_CATALOG_SETTINGS_TOKEN)),
1255
+ _ts_param7(1, Inject6(SETTINGS_SOURCE_TOKEN)),
1256
+ _ts_param7(2, Optional5()),
1257
+ _ts_param7(2, Inject6(APPLIED_SETTINGS_PORT_TOKEN)),
1258
+ _ts_param7(3, Optional5()),
1259
+ _ts_param7(3, Inject6(SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN)),
1260
+ _ts_param7(4, Optional5()),
1261
+ _ts_param7(4, Inject6(RLS_BYPASS_PORT_TOKEN)),
1262
+ _ts_metadata7("design:type", Function),
1263
+ _ts_metadata7("design:paramtypes", [
1264
+ typeof PlanCatalogSettings === "undefined" ? Object : PlanCatalogSettings,
1265
+ String,
1266
+ Object,
1267
+ Object,
1268
+ Object
1269
+ ])
1270
+ ], IssuerIdentityInspector);
1271
+ var IssuerIdentityCheck = class {
1272
+ static {
1273
+ __name(this, "IssuerIdentityCheck");
1274
+ }
1275
+ inspector;
1276
+ constructor(inspector) {
1277
+ this.inspector = inspector;
1278
+ }
1279
+ async onModuleInit() {
1280
+ const verdict = await this.inspector.inspect();
1281
+ if (verdict.kind === "refused") throw new Error(verdict.refusal);
1282
+ this.inspector.report(verdict);
1283
+ }
1284
+ };
1285
+ IssuerIdentityCheck = _ts_decorate9([
1286
+ Injectable5(),
1287
+ _ts_metadata7("design:type", Function),
1288
+ _ts_metadata7("design:paramtypes", [
1289
+ typeof IssuerIdentityInspector === "undefined" ? Object : IssuerIdentityInspector
1290
+ ])
1291
+ ], IssuerIdentityCheck);
1292
+ function refusalFor(change, running, source, recordedIssuer) {
1293
+ return [
1294
+ `The issuer in ${source} is not the legal entity this installation recorded.`,
1295
+ ` recorded: ${describe2(change.recorded)}`,
1296
+ ` in the file: ${change.current ? describe2(change.current) : "no issuer is named at all"}`,
1297
+ faultSentence(change.fault),
1298
+ contractsSentence(running),
1299
+ ...wayOut(change, source, recordedIssuer)
1300
+ ].join("\n");
1301
+ }
1302
+ __name(refusalFor, "refusalFor");
1303
+ function wayOut(change, source, recordedIssuer) {
1304
+ if (change.fault.kind === "names-no-issuer") {
1305
+ return [
1306
+ `Write the issuer block again as this installation recorded it \u2014 replacing the one in ${source} where there is still one there, because a second \`issuer:\` key is a file YAML refuses to read. Where that entity has since been corrected, declare the correction beside the corrected values. A declaration on its own cannot help here: there is nothing in the file for it to be about.`,
1307
+ issuerBlockFor(recordedIssuer)
1308
+ ];
1309
+ }
1310
+ return [
1311
+ "Moving a contract to another legal entity is a transfer, not an edit of a setting. Where this is the same entity under a new name, or with a tax identifier that was wrong or missing, declare it beside the values it replaces and start again:",
1312
+ declarationFor(change.recorded, change.moved)
1313
+ ];
1314
+ }
1315
+ __name(wayOut, "wayOut");
1316
+ function issuerBlockFor(recordedIssuer) {
1317
+ const source = recordedIssuer && typeof recordedIssuer === "object" && !Array.isArray(recordedIssuer) ? recordedIssuer : {};
1318
+ const lines = ISSUER_BLOCK_KEYS.filter((key) => typeof source[key] === "string").map((key) => ` ${key}: ${JSON.stringify(source[key])}`);
1319
+ return [
1320
+ "issuer:",
1321
+ ...lines
1322
+ ].join("\n");
1323
+ }
1324
+ __name(issuerBlockFor, "issuerBlockFor");
1325
+ function faultSentence(fault) {
1326
+ switch (fault.kind) {
1327
+ case "absent":
1328
+ return "`issuer.correctionOf` declares nothing, so nothing says this is the same entity.";
1329
+ case "names-no-issuer":
1330
+ return "The file names no issuer for a declaration to be about, so there is no entity here for the recorded one to be the same as.";
1331
+ case "names-another-value":
1332
+ return `\`issuer.correctionOf.${fault.field}\` names ${quote(fault.declared)}, which is not what the record holds (${quote(fault.recorded)}). A declaration names the values it replaces, so one left over from a correction already applied does not cover this one.`;
1333
+ case "leaves-a-field-out":
1334
+ return `\`issuer.correctionOf\` says nothing about \`${fault.field}\`, which the file moves away from ${quote(fault.recorded)}. Every identity field that moves is named, or the declaration covers only half the change.`;
1335
+ }
1336
+ }
1337
+ __name(faultSentence, "faultSentence");
1338
+ function contractsSentence(running) {
1339
+ if (!running.known) return running.why;
1340
+ const { total, contracts } = running.contracts;
1341
+ if (total === 0) return "No contract is running.";
1342
+ const rest = total - contracts.length;
1343
+ return [
1344
+ `${total} contract(s) are still running:`,
1345
+ ...contracts.map(describeContract),
1346
+ ...rest > 0 ? [
1347
+ ` \u2026 and ${rest} more.`
1348
+ ] : []
1349
+ ].join("\n");
1350
+ }
1351
+ __name(contractsSentence, "contractsSentence");
1352
+ function describeContract(contract) {
1353
+ const under = contract.issuerLegalName ? `concluded under ${quote(contract.issuerLegalName)}` : "names no issuer";
1354
+ const from2 = contract.effectiveFrom instanceof Date ? contract.effectiveFrom.toISOString().slice(0, 10) : "an unreadable date";
1355
+ return ` ${contract.id} (tenant ${contract.tenantId}, from ${from2}, ${under})`;
1356
+ }
1357
+ __name(describeContract, "describeContract");
1358
+ function declarationFor(recorded, moved) {
1359
+ const lines = moved.map((field) => ` ${field}: ${recorded[field] === null ? "null" : JSON.stringify(recorded[field])}`);
1360
+ return [
1361
+ "issuer:",
1362
+ " correctionOf:",
1363
+ ...lines,
1364
+ " reason: <why the same entity now reads differently>"
1365
+ ].join("\n");
1366
+ }
1367
+ __name(declarationFor, "declarationFor");
1368
+ function describe2(identity) {
1369
+ return `${quote(identity.legalName)} (VAT id ${quote(identity.vatId)}, tax number ${quote(identity.taxNumber)})`;
1370
+ }
1371
+ __name(describe2, "describe");
1372
+ function listFields(fields) {
1373
+ return fields.map((field) => `\`${field}\``).join(", ");
1374
+ }
1375
+ __name(listFields, "listFields");
1376
+ function quote(value) {
1377
+ return value === null ? "none" : `'${value}'`;
1378
+ }
1379
+ __name(quote, "quote");
1380
+ function messageOf(error) {
1381
+ return error instanceof Error ? error.message : String(error);
1382
+ }
1383
+ __name(messageOf, "messageOf");
1384
+
1385
+ // src/platform/module-options.ts
1386
+ var DB_CATALOG_MEMBERS = [
1387
+ "path",
1388
+ "env"
1389
+ ];
1390
+ function dbCatalogNamesAPath(dbCatalog) {
1391
+ return typeof dbCatalog?.path === "string" && dbCatalog.path.trim() !== "";
1392
+ }
1393
+ __name(dbCatalogNamesAPath, "dbCatalogNamesAPath");
1394
+ function dbCatalogLeftovers(dbCatalog) {
1395
+ const taken = new Set(DB_CATALOG_MEMBERS);
1396
+ return Object.entries(dbCatalog).filter(([key, value]) => !taken.has(key) && value !== void 0).map(([key]) => key);
1397
+ }
1398
+ __name(dbCatalogLeftovers, "dbCatalogLeftovers");
1399
+ function dbCatalogNamesAFile(dbCatalog) {
1400
+ return dbCatalogNamesAPath(dbCatalog) && dbCatalogLeftovers(dbCatalog).length === 0;
1401
+ }
1402
+ __name(dbCatalogNamesAFile, "dbCatalogNamesAFile");
1403
+
507
1404
  // src/platform/validation/rules.ts
508
1405
  import { assertPersistenceCapabilities } from "@saasicat/core";
1406
+
1407
+ // src/platform/compose/bundle-repository-source.ts
1408
+ function resolveBundleRepository(persistence) {
1409
+ return persistence?.catalog?.bundleRepository ?? persistence?.entitlement?.bundleRepository;
1410
+ }
1411
+ __name(resolveBundleRepository, "resolveBundleRepository");
1412
+
1413
+ // src/platform/validation/rules.ts
509
1414
  var DOCS_BASE = "https://github.com/uelker70/saasicat/blob/main/docs/reference/options.md";
510
1415
  var headingSlug = /* @__PURE__ */ __name((text) => text.trim().toLowerCase().replace(/[^a-z0-9 -]/g, "").replace(/ /g, "-"), "headingSlug");
511
1416
  var docsUrlFor = /* @__PURE__ */ __name((id) => `${DOCS_BASE}#${headingSlug(id)}`, "docsUrlFor");
@@ -521,6 +1426,7 @@ function absent(required) {
521
1426
  }
522
1427
  __name(absent, "absent");
523
1428
  var list = /* @__PURE__ */ __name((names) => names.join(", "), "list");
1429
+ var catalogOf = /* @__PURE__ */ __name((c) => c.catalog ?? c.options.planCatalog, "catalogOf");
524
1430
  var RULE_SPECS = [
525
1431
  {
526
1432
  id: "core.adapters-bound",
@@ -532,7 +1438,42 @@ var RULE_SPECS = [
532
1438
  id: "catalog.identity-or-sink",
533
1439
  when: /* @__PURE__ */ __name((c) => !c.options.planCatalog, "when"),
534
1440
  assert: /* @__PURE__ */ __name((c) => Boolean(c.adapters.planCatalogReadSink && c.options.dbCatalog), "assert"),
535
- message: "no plan catalogue is reachable. Either set `planCatalog` (the quickstart YAML path) or, for DB hydration, BOTH a `planCatalogReadSink` (via `adapters` or `persistence`) AND `dbCatalog` ({ projectKey, currency, vatRate }). Without the identity the sink loads with projectKey '' and the app boots with a silently empty catalogue."
1441
+ message: "no plan catalogue is reachable. Either set `planCatalog` (the quickstart YAML path) or, for DB hydration, BOTH a `planCatalogReadSink` (via `adapters` or `persistence`) AND `dbCatalog` ({ path: 'config/saas.yaml' }, the file the settings are read from). Without the file the app boots with a silently empty catalogue and no settings."
1442
+ },
1443
+ {
1444
+ id: "catalog.db-catalog-names-the-file",
1445
+ when: /* @__PURE__ */ __name((c) => c.options.dbCatalog !== void 0, "when"),
1446
+ assert: /* @__PURE__ */ __name((c) => dbCatalogNamesAFile(c.options.dbCatalog), "assert"),
1447
+ message: /* @__PURE__ */ __name((c) => {
1448
+ const given = c.options.dbCatalog;
1449
+ const leftovers = dbCatalogNamesAPath(given) ? dbCatalogLeftovers(given) : [];
1450
+ if (leftovers.length > 0) {
1451
+ return `\`dbCatalog\` names the file and nothing else, and this one still carries ${list(leftovers)}. Delete that here; the platform reads the settings from the file it names, and a value left beside the path would be ignored while looking like the one that runs. See docs/guides/upgrade-to-1.0.md.`;
1452
+ }
1453
+ return "`dbCatalog` names the file: `dbCatalog: { path: 'config/saas.yaml' }`. It used to take `app`, `currency`, `vatRate` and `tenantBilling` as values, and that was a second place a setting could live. Delete the values here; the platform reads them from the file it names \u2014 the same one they were forwarded from. See docs/guides/upgrade-to-1.0.md.";
1454
+ }, "message")
1455
+ },
1456
+ {
1457
+ id: "catalog.db-catalog-file-loads",
1458
+ // Only where a file was named at all. On the quickstart path the
1459
+ // consumer calls the loader themselves, and their own stack says where.
1460
+ when: /* @__PURE__ */ __name((c) => c.options.dbCatalog !== void 0, "when"),
1461
+ assert: /* @__PURE__ */ __name((c) => c.catalogFailure === void 0, "assert"),
1462
+ message: /* @__PURE__ */ __name((c) => {
1463
+ const given = c.options.dbCatalog;
1464
+ const named = dbCatalogNamesAPath(given) ? given.path : "config/saas.yaml";
1465
+ const why = c.catalogFailure ? `: ${c.catalogFailure.message}` : "";
1466
+ return `\`dbCatalog\` names '${named}' and the file did not load${why}. A relative path is resolved against the directory the process was started in, not against the file the option is written in. See docs/guides/wire-the-backend.md.`;
1467
+ }, "message")
1468
+ },
1469
+ {
1470
+ id: "catalog.app-is-named",
1471
+ // Only where a catalogue is reachable at all — otherwise
1472
+ // `catalog.identity-or-sink` is the finding, and reporting both would
1473
+ // name the same omission twice.
1474
+ when: /* @__PURE__ */ __name((c) => catalogOf(c) !== void 0, "when"),
1475
+ assert: /* @__PURE__ */ __name((c) => Boolean(catalogOf(c)?.app?.name?.trim()), "assert"),
1476
+ message: "the catalogue names no application. `app.name` is the one place an installation names itself \u2014 it is the manifest display name, the login-page brand and the discovery snapshot key \u2014 so an absent one is not a default to fill in: the app would boot identified by an empty string. Set `app: { name: \u2026 }` in config/saas.yaml."
536
1477
  },
537
1478
  {
538
1479
  id: "catalog.requires-persistence",
@@ -576,6 +1517,24 @@ var RULE_SPECS = [
576
1517
  assert: /* @__PURE__ */ __name((c) => Boolean(optionsOf(c.options.subscriptionContract)?.subscriptionContractRepository ?? bundle(c)?.entitlement?.subscriptionContractRepository), "assert"),
577
1518
  message: "subscriptionContract is enabled, but no repository is available. Set `subscriptionContract.subscriptionContractRepository` or use a compatible persistence bundle."
578
1519
  },
1520
+ {
1521
+ id: "subscription-contract.requires-subscriber-repository",
1522
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.subscriptionContract), "when"),
1523
+ assert: /* @__PURE__ */ __name((c) => Boolean(optionsOf(c.options.subscriptionContract)?.subscriberRepository ?? bundle(c)?.entitlement?.subscriberRepository), "assert"),
1524
+ message: "subscriptionContract is enabled, but no subscriber repository is available: every contract names the subscriber it is concluded with. Set `subscriptionContract.subscriberRepository` or use a persistence bundle providing `persistence.entitlement.subscriberRepository`."
1525
+ },
1526
+ {
1527
+ id: "payments.requires-persistence",
1528
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.payments), "when"),
1529
+ assert: /* @__PURE__ */ __name((c) => absent(paymentStores(c)).length === 0, "assert"),
1530
+ message: /* @__PURE__ */ __name((c) => `payments is enabled, but these adapters are missing: ${list(absent(paymentStores(c)))}. A gateway event is claimed and its effect written on one transaction, so all of them come from one persistence bundle.`, "message")
1531
+ },
1532
+ {
1533
+ id: "payments.requires-accounts-in-the-file",
1534
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.payments) && catalogOf(c) !== void 0, "when"),
1535
+ assert: /* @__PURE__ */ __name((c) => Boolean(catalogOf(c)?.payments), "assert"),
1536
+ message: "payments is enabled, and config/saas.yaml has no `payments` block naming the gateway accounts the bound gateways belong to."
1537
+ },
579
1538
  {
580
1539
  id: "promo-codes.public-preview-requires-first-time-check",
581
1540
  when: /* @__PURE__ */ __name((c) => {
@@ -650,9 +1609,15 @@ var tenantBillingPorts = /* @__PURE__ */ __name((c) => {
650
1609
  subscriptionWritePort: config?.subscriptionWritePort
651
1610
  };
652
1611
  }, "tenantBillingPorts");
1612
+ var paymentStores = /* @__PURE__ */ __name((c) => ({
1613
+ paymentEventLog: bundle(c)?.payments?.paymentEventLog,
1614
+ subscriberPaymentMethodRepository: bundle(c)?.payments?.subscriberPaymentMethodRepository,
1615
+ subscriberRepository: bundle(c)?.entitlement?.subscriberRepository,
1616
+ transactionRunner: c.adapters.transactionRunner
1617
+ }), "paymentStores");
653
1618
  var bundleRepositories = /* @__PURE__ */ __name((c) => ({
654
1619
  subscriptionBundleRepository: bundle(c)?.entitlement?.subscriptionBundleRepository,
655
- bundleRepository: bundle(c)?.catalog?.bundleRepository
1620
+ bundleRepository: resolveBundleRepository(bundle(c))
656
1621
  }), "bundleRepositories");
657
1622
  var entitlementAdapters = /* @__PURE__ */ __name((c) => ({
658
1623
  subscriptionRepository: c.adapters.subscriptionRepository,
@@ -727,6 +1692,13 @@ var QuotaProvidersUsageSnapshot = class {
727
1692
  };
728
1693
 
729
1694
  // src/platform/compose/context.ts
1695
+ function operatorGuards(options) {
1696
+ return [
1697
+ ...options.controller.guards,
1698
+ SuperAdminGuard
1699
+ ];
1700
+ }
1701
+ __name(operatorGuards, "operatorGuards");
730
1702
  function optionsOf2(slot) {
731
1703
  return typeof slot === "object" ? slot : {};
732
1704
  }
@@ -739,10 +1711,7 @@ function composeAdminResources({ options, persistence }) {
739
1711
  return [
740
1712
  AdminResourcesModule.forRoot({
741
1713
  resources: config.resources ?? persistence?.adminResources?.resources,
742
- guards: config.guards ?? [
743
- ...options.controller.guards,
744
- SuperAdminGuard
745
- ],
1714
+ guards: config.guards ?? operatorGuards(options),
746
1715
  imports: config.imports ?? options.imports
747
1716
  })
748
1717
  ];
@@ -750,7 +1719,7 @@ function composeAdminResources({ options, persistence }) {
750
1719
  __name(composeAdminResources, "composeAdminResources");
751
1720
 
752
1721
  // src/platform/compose/catalog.ts
753
- function composeCatalog({ options, persistence, appInfo }) {
1722
+ function composeCatalog({ options, persistence }) {
754
1723
  const config = options.catalog;
755
1724
  if (!config) return [];
756
1725
  const catalog = persistence?.catalog;
@@ -763,7 +1732,7 @@ function composeCatalog({ options, persistence, appInfo }) {
763
1732
  promotionRepository: catalog.promotionRepository,
764
1733
  marketingSettingsRepository: catalog.marketingSettingsRepository,
765
1734
  controller: config.adminControllers === false ? void 0 : {
766
- guards: options.controller.guards
1735
+ guards: operatorGuards(options)
767
1736
  },
768
1737
  imports: config.imports ?? options.imports,
769
1738
  extraProviders: config.extraProviders,
@@ -771,13 +1740,18 @@ function composeCatalog({ options, persistence, appInfo }) {
771
1740
  autoSyncDiscoveryAtBoot: config.autoSyncDiscoveryAtBoot,
772
1741
  marketedOnlyFeatures: config.marketedOnlyFeatures,
773
1742
  featureUiRegistry: config.featureUiRegistry,
774
- publicMarketingCatalog: config.publicMarketingCatalog
1743
+ // What the public catalogue says about payment methods comes from
1744
+ // the same option that wires the gateways, so the two cannot
1745
+ // disagree: no `payments`, nothing takes new payment methods.
1746
+ publicMarketingCatalog: config.publicMarketingCatalog && {
1747
+ ...config.publicMarketingCatalog,
1748
+ newPaymentMethodsFrom: config.publicMarketingCatalog.newPaymentMethodsFrom ?? (options.payments ? PaymentGatewayRegistry : null)
1749
+ }
775
1750
  })
776
1751
  ];
777
1752
  if (config.publicCatalog !== false) {
778
1753
  modules.push(PublicCatalogModule.forRoot({
779
1754
  featureUiRegistry: config.featureUiRegistry,
780
- projectKey: appInfo.key,
781
1755
  bundleRepository: catalog.bundleRepository,
782
1756
  marketingRepository: catalog.marketingProjectionRepository,
783
1757
  catalogEntryRepository: catalog.catalogEntryRepository,
@@ -826,25 +1800,34 @@ function composeAdminStats({ options, persistence }) {
826
1800
  return [
827
1801
  AdminStatsModule.forRoot({
828
1802
  ...config,
829
- guards: config.guards ?? [
830
- ...options.controller.guards,
831
- SuperAdminGuard
832
- ],
1803
+ guards: config.guards ?? operatorGuards(options),
833
1804
  auditStatsPort: config.auditStatsPort ?? persistence?.core.auditStats,
834
1805
  imports: config.imports ?? options.imports
835
1806
  })
836
1807
  ];
837
1808
  }
838
1809
  __name(composeAdminStats, "composeAdminStats");
839
- function composeCheckoutOffer({ options, persistence }) {
1810
+ function composeCheckoutOffer({ options, adapters, persistence }) {
840
1811
  const config = options.checkoutOffer;
841
1812
  if (!config) return [];
1813
+ const contractRepository = config.conclusion?.subscriptionContractRepository ?? persistence?.entitlement?.subscriptionContractRepository;
1814
+ const subscriberRepository = config.conclusion?.subscriberRepository ?? persistence?.entitlement?.subscriberRepository;
1815
+ const transactionRunner = config.conclusion?.transactionRunner ?? adapters.transactionRunner;
842
1816
  return [
843
1817
  CheckoutOfferModule.forRoot({
844
1818
  ...config,
845
- bundleRepository: config.bundleRepository ?? persistence?.catalog?.bundleRepository,
1819
+ bundleRepository: config.bundleRepository ?? resolveBundleRepository(persistence),
846
1820
  planRepository: config.planRepository ?? persistence?.catalog?.planRepository,
1821
+ promotionRepository: config.promotionRepository ?? persistence?.catalog?.promotionRepository,
847
1822
  catalogEntryRepository: config.catalogEntryRepository ?? persistence?.catalog?.catalogEntryRepository,
1823
+ // Named by hand, it is passed on even when part of it resolves to
1824
+ // nothing, so the module refuses to start the same way it does when
1825
+ // wired directly; derived from the bundle, it is wired when all exist.
1826
+ conclusion: config.conclusion || contractRepository && subscriberRepository && transactionRunner ? {
1827
+ subscriptionContractRepository: contractRepository,
1828
+ subscriberRepository,
1829
+ transactionRunner
1830
+ } : void 0,
848
1831
  imports: config.imports ?? options.imports
849
1832
  })
850
1833
  ];
@@ -857,12 +1840,39 @@ function composeSubscriptionContract({ options, persistence }) {
857
1840
  SubscriptionContractModule.forRoot({
858
1841
  ...config,
859
1842
  subscriptionContractRepository: config.subscriptionContractRepository ?? persistence?.entitlement?.subscriptionContractRepository,
1843
+ subscriberRepository: config.subscriberRepository ?? persistence?.entitlement?.subscriberRepository,
860
1844
  imports: config.imports ?? options.imports
861
1845
  })
862
1846
  ];
863
1847
  }
864
1848
  __name(composeSubscriptionContract, "composeSubscriptionContract");
865
1849
 
1850
+ // src/platform/compose/payments.ts
1851
+ function composePayments(ctx) {
1852
+ const config = ctx.options.payments;
1853
+ if (!config) return [];
1854
+ const tenantBilling = optionsOf2(ctx.options.tenantBilling);
1855
+ const authGuards = ctx.shared.authGuards;
1856
+ return [
1857
+ PaymentsModule.forRoot({
1858
+ gateways: config.gateways,
1859
+ paymentEventLog: ctx.persistence?.payments?.paymentEventLog,
1860
+ subscriberPaymentMethodRepository: ctx.persistence?.payments?.subscriberPaymentMethodRepository,
1861
+ subscriberRepository: ctx.persistence?.entitlement?.subscriberRepository,
1862
+ transactionRunner: ctx.adapters.transactionRunner,
1863
+ tenantRoutes: authGuards ? {
1864
+ authGuards,
1865
+ billingPermissionGuards: config.billingPermissionGuards,
1866
+ tenantIdResolver: tenantBilling.tenantIdResolver,
1867
+ userEmailResolver: tenantBilling.userEmailResolver
1868
+ } : void 0,
1869
+ imports: config.imports ?? ctx.options.imports,
1870
+ extraProviders: config.extraProviders
1871
+ })
1872
+ ];
1873
+ }
1874
+ __name(composePayments, "composePayments");
1875
+
866
1876
  // src/platform/compose/promo-codes.ts
867
1877
  function composePromoCodes({ options, adapters, persistence }) {
868
1878
  if (!options.promoCodes) return [];
@@ -877,18 +1887,19 @@ function composePromoCodes({ options, adapters, persistence }) {
877
1887
  validationLogRepository: promo.validationLogRepository,
878
1888
  subscriptionLookup: promo.subscriptionLookup,
879
1889
  revenueAggregator: promo.revenueAggregator,
1890
+ holdRepository: promo.holdRepository,
880
1891
  transactionRunner: adapters.transactionRunner,
881
1892
  firstTimeCustomerCheck: firstTimeCustomerCheck ?? {
882
1893
  hasExistingCustomerForEmail: /* @__PURE__ */ __name(async () => false, "hasExistingCustomerForEmail")
883
1894
  },
884
1895
  includePublicController: includePublicController ?? false,
885
1896
  adminController: adminController === false ? false : {
886
- guards: adminGuards ?? [
887
- ...options.controller.guards,
888
- SuperAdminGuard
889
- ]
1897
+ guards: adminGuards ?? operatorGuards(options)
890
1898
  },
891
- imports: promoImports ?? options.imports
1899
+ imports: promoImports ?? options.imports,
1900
+ // A checkout offer prices a promo code through `PromoCodesService`,
1901
+ // from a module that does not import this one.
1902
+ global: Boolean(options.checkoutOffer)
892
1903
  })
893
1904
  ];
894
1905
  }
@@ -927,6 +1938,12 @@ function composeTenantBilling(ctx) {
927
1938
  TenantBillingModule.forRoot({
928
1939
  ...tenantOptions,
929
1940
  authGuards: ctx.shared.authGuards,
1941
+ // The same adapter `SubscriptionBundleModule` gets. That module
1942
+ // exports the token, but it is a sibling import here rather than an
1943
+ // ancestor, so its exports never reach this module's providers —
1944
+ // and the plan-change rule that reads bookings would resolve to
1945
+ // "none" and allow the move it exists to refuse.
1946
+ subscriptionBundleRepository: ctx.persistence?.entitlement?.subscriptionBundleRepository,
930
1947
  subscriptionUsagePort: ctx.shared.subscriptionUsagePort,
931
1948
  usageSnapshotPort: usageSnapshotPort ?? tenantSlice?.usageSnapshotPort ?? quotaUsageSnapshotProvider(quotaProviders),
932
1949
  subscriptionWritePort: subscriptionWritePort ?? tenantSlice?.subscriptionWritePort,
@@ -951,7 +1968,7 @@ function composeSubscriptionBundles(ctx) {
951
1968
  SubscriptionBundleModule.forRoot({
952
1969
  ...bundleOptions,
953
1970
  subscriptionBundleRepository: ctx.persistence?.entitlement?.subscriptionBundleRepository,
954
- bundleRepository: ctx.persistence?.catalog?.bundleRepository,
1971
+ bundleRepository: resolveBundleRepository(ctx.persistence),
955
1972
  controller: controller === false ? void 0 : {
956
1973
  ...controller ?? {},
957
1974
  authGuards: ctx.shared.authGuards,
@@ -976,6 +1993,8 @@ var FEATURE_COMPOSERS = [
976
1993
  // Before the bundles — see the note above.
977
1994
  composeTenantBilling,
978
1995
  composeSubscriptionBundles,
1996
+ // After tenant billing too: its tenant routes share the guards.
1997
+ composePayments,
979
1998
  composeSetup,
980
1999
  composeAdminStats,
981
2000
  composeCheckoutOffer,
@@ -1069,32 +2088,32 @@ var StaticPlanResolver = class {
1069
2088
  };
1070
2089
 
1071
2090
  // src/platform/static-entitlement.service.ts
1072
- import { Inject as Inject3, Injectable as Injectable3 } from "@nestjs/common";
1073
- function _ts_decorate5(decorators, target, key, desc) {
2091
+ import { Inject as Inject7, Injectable as Injectable6 } from "@nestjs/common";
2092
+ function _ts_decorate10(decorators, target, key, desc) {
1074
2093
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1075
2094
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1076
2095
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1077
2096
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1078
2097
  }
1079
- __name(_ts_decorate5, "_ts_decorate");
1080
- function _ts_metadata4(k, v) {
2098
+ __name(_ts_decorate10, "_ts_decorate");
2099
+ function _ts_metadata8(k, v) {
1081
2100
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1082
2101
  }
1083
- __name(_ts_metadata4, "_ts_metadata");
1084
- function _ts_param4(paramIndex, decorator) {
2102
+ __name(_ts_metadata8, "_ts_metadata");
2103
+ function _ts_param8(paramIndex, decorator) {
1085
2104
  return function(target, key) {
1086
2105
  decorator(target, key, paramIndex);
1087
2106
  };
1088
2107
  }
1089
- __name(_ts_param4, "_ts_param");
2108
+ __name(_ts_param8, "_ts_param");
1090
2109
  var StaticEntitlementService = class {
1091
2110
  static {
1092
2111
  __name(this, "StaticEntitlementService");
1093
2112
  }
1094
- catalog;
2113
+ catalogs;
1095
2114
  resolver;
1096
- constructor(catalog, resolver) {
1097
- this.catalog = catalog;
2115
+ constructor(catalogs, resolver) {
2116
+ this.catalogs = catalogs;
1098
2117
  this.resolver = resolver;
1099
2118
  }
1100
2119
  /**
@@ -1111,7 +2130,7 @@ var StaticEntitlementService = class {
1111
2130
  quotas: {}
1112
2131
  };
1113
2132
  }
1114
- const plan = this.findPlan(planId);
2133
+ const plan = findPlan(await this.catalogs.current(), planId);
1115
2134
  if (!plan) {
1116
2135
  return {
1117
2136
  planId,
@@ -1135,34 +2154,31 @@ var StaticEntitlementService = class {
1135
2154
  const value = snap.quotas[quotaKey];
1136
2155
  return value === void 0 ? null : value;
1137
2156
  }
1138
- findPlan(planId) {
1139
- return (this.catalog.plans ?? []).find((p) => p.id === planId);
1140
- }
1141
2157
  };
1142
- StaticEntitlementService = _ts_decorate5([
1143
- Injectable3(),
1144
- _ts_param4(0, Inject3(PLAN_CATALOG_TOKEN)),
1145
- _ts_param4(1, Inject3(PLAN_RESOLVER_PORT_TOKEN)),
1146
- _ts_metadata4("design:type", Function),
1147
- _ts_metadata4("design:paramtypes", [
1148
- typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
2158
+ StaticEntitlementService = _ts_decorate10([
2159
+ Injectable6(),
2160
+ _ts_param8(0, Inject7(PLAN_CATALOG_SOURCE_TOKEN)),
2161
+ _ts_param8(1, Inject7(PLAN_RESOLVER_PORT_TOKEN)),
2162
+ _ts_metadata8("design:type", Function),
2163
+ _ts_metadata8("design:paramtypes", [
2164
+ typeof PlanCatalogSource === "undefined" ? Object : PlanCatalogSource,
1149
2165
  typeof PlanResolverPort === "undefined" ? Object : PlanResolverPort
1150
2166
  ])
1151
2167
  ], StaticEntitlementService);
1152
2168
 
1153
2169
  // src/platform/tenant-manifest.service.ts
1154
- import { Injectable as Injectable4 } from "@nestjs/common";
1155
- function _ts_decorate6(decorators, target, key, desc) {
2170
+ import { Injectable as Injectable7 } from "@nestjs/common";
2171
+ function _ts_decorate11(decorators, target, key, desc) {
1156
2172
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1157
2173
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1158
2174
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1159
2175
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1160
2176
  }
1161
- __name(_ts_decorate6, "_ts_decorate");
1162
- function _ts_metadata5(k, v) {
2177
+ __name(_ts_decorate11, "_ts_decorate");
2178
+ function _ts_metadata9(k, v) {
1163
2179
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1164
2180
  }
1165
- __name(_ts_metadata5, "_ts_metadata");
2181
+ __name(_ts_metadata9, "_ts_metadata");
1166
2182
  var TenantManifestService = class {
1167
2183
  static {
1168
2184
  __name(this, "TenantManifestService");
@@ -1208,10 +2224,10 @@ var TenantManifestService = class {
1208
2224
  };
1209
2225
  }
1210
2226
  };
1211
- TenantManifestService = _ts_decorate6([
1212
- Injectable4(),
1213
- _ts_metadata5("design:type", Function),
1214
- _ts_metadata5("design:paramtypes", [
2227
+ TenantManifestService = _ts_decorate11([
2228
+ Injectable7(),
2229
+ _ts_metadata9("design:type", Function),
2230
+ _ts_metadata9("design:paramtypes", [
1215
2231
  typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService
1216
2232
  ])
1217
2233
  ], TenantManifestService);
@@ -1225,24 +2241,24 @@ function isVisible(item, features) {
1225
2241
  __name(isVisible, "isVisible");
1226
2242
 
1227
2243
  // src/platform/tenant-manifest.controller.ts
1228
- import { Controller as Controller2, Get as Get2, Inject as Inject4, Req, UseGuards } from "@nestjs/common";
1229
- function _ts_decorate7(decorators, target, key, desc) {
2244
+ import { Controller as Controller3, Get as Get3, Inject as Inject8, Req as Req2, UseGuards as UseGuards2 } from "@nestjs/common";
2245
+ function _ts_decorate12(decorators, target, key, desc) {
1230
2246
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1231
2247
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1232
2248
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1233
2249
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1234
2250
  }
1235
- __name(_ts_decorate7, "_ts_decorate");
1236
- function _ts_metadata6(k, v) {
2251
+ __name(_ts_decorate12, "_ts_decorate");
2252
+ function _ts_metadata10(k, v) {
1237
2253
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1238
2254
  }
1239
- __name(_ts_metadata6, "_ts_metadata");
1240
- function _ts_param5(paramIndex, decorator) {
2255
+ __name(_ts_metadata10, "_ts_metadata");
2256
+ function _ts_param9(paramIndex, decorator) {
1241
2257
  return function(target, key) {
1242
2258
  decorator(target, key, paramIndex);
1243
2259
  };
1244
2260
  }
1245
- __name(_ts_param5, "_ts_param");
2261
+ __name(_ts_param9, "_ts_param");
1246
2262
  function buildTenantManifestController(options) {
1247
2263
  const path = options.path ?? "tenant/manifest";
1248
2264
  let GeneratedTenantManifestController = class GeneratedTenantManifestController {
@@ -1261,21 +2277,21 @@ function buildTenantManifestController(options) {
1261
2277
  return this.svc.getManifest(tenantId);
1262
2278
  }
1263
2279
  };
1264
- _ts_decorate7([
1265
- Get2(),
1266
- _ts_param5(0, Req()),
1267
- _ts_metadata6("design:type", Function),
1268
- _ts_metadata6("design:paramtypes", [
2280
+ _ts_decorate12([
2281
+ Get3(),
2282
+ _ts_param9(0, Req2()),
2283
+ _ts_metadata10("design:type", Function),
2284
+ _ts_metadata10("design:paramtypes", [
1269
2285
  typeof RequestWithUser === "undefined" ? Object : RequestWithUser
1270
2286
  ]),
1271
- _ts_metadata6("design:returntype", Promise)
2287
+ _ts_metadata10("design:returntype", Promise)
1272
2288
  ], GeneratedTenantManifestController.prototype, "get", null);
1273
- GeneratedTenantManifestController = _ts_decorate7([
1274
- Controller2(path),
1275
- UseGuards(...options.guards),
1276
- _ts_param5(0, Inject4(TenantManifestService)),
1277
- _ts_metadata6("design:type", Function),
1278
- _ts_metadata6("design:paramtypes", [
2289
+ GeneratedTenantManifestController = _ts_decorate12([
2290
+ Controller3(path),
2291
+ UseGuards2(...options.guards),
2292
+ _ts_param9(0, Inject8(TenantManifestService)),
2293
+ _ts_metadata10("design:type", Function),
2294
+ _ts_metadata10("design:paramtypes", [
1279
2295
  typeof TenantManifestService === "undefined" ? Object : TenantManifestService
1280
2296
  ])
1281
2297
  ], GeneratedTenantManifestController);
@@ -1284,27 +2300,27 @@ function buildTenantManifestController(options) {
1284
2300
  __name(buildTenantManifestController, "buildTenantManifestController");
1285
2301
 
1286
2302
  // src/platform/enforce-quota.interceptor.ts
1287
- import { Inject as Inject5, Injectable as Injectable5, Optional as Optional2 } from "@nestjs/common";
2303
+ import { Inject as Inject9, Injectable as Injectable8, Optional as Optional6 } from "@nestjs/common";
1288
2304
  import { Reflector } from "@nestjs/core";
1289
2305
  import { from } from "rxjs";
1290
2306
  import { switchMap } from "rxjs/operators";
1291
- function _ts_decorate8(decorators, target, key, desc) {
2307
+ function _ts_decorate13(decorators, target, key, desc) {
1292
2308
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1293
2309
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1294
2310
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1295
2311
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1296
2312
  }
1297
- __name(_ts_decorate8, "_ts_decorate");
1298
- function _ts_metadata7(k, v) {
2313
+ __name(_ts_decorate13, "_ts_decorate");
2314
+ function _ts_metadata11(k, v) {
1299
2315
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1300
2316
  }
1301
- __name(_ts_metadata7, "_ts_metadata");
1302
- function _ts_param6(paramIndex, decorator) {
2317
+ __name(_ts_metadata11, "_ts_metadata");
2318
+ function _ts_param10(paramIndex, decorator) {
1303
2319
  return function(target, key) {
1304
2320
  decorator(target, key, paramIndex);
1305
2321
  };
1306
2322
  }
1307
- __name(_ts_param6, "_ts_param");
2323
+ __name(_ts_param10, "_ts_param");
1308
2324
  var QUOTA_PROVIDERS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/QuotaProviders");
1309
2325
  var EnforceQuotaInterceptor = class {
1310
2326
  static {
@@ -1347,14 +2363,14 @@ var EnforceQuotaInterceptor = class {
1347
2363
  }
1348
2364
  }
1349
2365
  };
1350
- EnforceQuotaInterceptor = _ts_decorate8([
1351
- Injectable5(),
1352
- _ts_param6(0, Inject5(Reflector)),
1353
- _ts_param6(1, Inject5(StaticEntitlementService)),
1354
- _ts_param6(2, Optional2()),
1355
- _ts_param6(2, Inject5(QUOTA_PROVIDERS_TOKEN)),
1356
- _ts_metadata7("design:type", Function),
1357
- _ts_metadata7("design:paramtypes", [
2366
+ EnforceQuotaInterceptor = _ts_decorate13([
2367
+ Injectable8(),
2368
+ _ts_param10(0, Inject9(Reflector)),
2369
+ _ts_param10(1, Inject9(StaticEntitlementService)),
2370
+ _ts_param10(2, Optional6()),
2371
+ _ts_param10(2, Inject9(QUOTA_PROVIDERS_TOKEN)),
2372
+ _ts_metadata11("design:type", Function),
2373
+ _ts_metadata11("design:paramtypes", [
1358
2374
  typeof Reflector === "undefined" ? Object : Reflector,
1359
2375
  typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
1360
2376
  Object
@@ -1362,26 +2378,26 @@ EnforceQuotaInterceptor = _ts_decorate8([
1362
2378
  ], EnforceQuotaInterceptor);
1363
2379
 
1364
2380
  // src/platform/static-feature.guard.ts
1365
- import { ForbiddenException as ForbiddenException2, Inject as Inject6, Injectable as Injectable6, Optional as Optional3 } from "@nestjs/common";
2381
+ import { ForbiddenException as ForbiddenException2, Inject as Inject10, Injectable as Injectable9, Optional as Optional7 } from "@nestjs/common";
1366
2382
  import { Reflector as Reflector2 } from "@nestjs/core";
1367
2383
  import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/core";
1368
- function _ts_decorate9(decorators, target, key, desc) {
2384
+ function _ts_decorate14(decorators, target, key, desc) {
1369
2385
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1370
2386
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1371
2387
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1372
2388
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1373
2389
  }
1374
- __name(_ts_decorate9, "_ts_decorate");
1375
- function _ts_metadata8(k, v) {
2390
+ __name(_ts_decorate14, "_ts_decorate");
2391
+ function _ts_metadata12(k, v) {
1376
2392
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1377
2393
  }
1378
- __name(_ts_metadata8, "_ts_metadata");
1379
- function _ts_param7(paramIndex, decorator) {
2394
+ __name(_ts_metadata12, "_ts_metadata");
2395
+ function _ts_param11(paramIndex, decorator) {
1380
2396
  return function(target, key) {
1381
2397
  decorator(target, key, paramIndex);
1382
2398
  };
1383
2399
  }
1384
- __name(_ts_param7, "_ts_param");
2400
+ __name(_ts_param11, "_ts_param");
1385
2401
  var STATIC_FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/StaticFeatureGuardConfig");
1386
2402
  var StaticFeatureGuard = class {
1387
2403
  static {
@@ -1435,14 +2451,14 @@ var StaticFeatureGuard = class {
1435
2451
  return true;
1436
2452
  }
1437
2453
  };
1438
- StaticFeatureGuard = _ts_decorate9([
1439
- Injectable6(),
1440
- _ts_param7(0, Inject6(Reflector2)),
1441
- _ts_param7(1, Inject6(StaticEntitlementService)),
1442
- _ts_param7(2, Optional3()),
1443
- _ts_param7(2, Inject6(STATIC_FEATURE_GUARD_CONFIG_TOKEN)),
1444
- _ts_metadata8("design:type", Function),
1445
- _ts_metadata8("design:paramtypes", [
2454
+ StaticFeatureGuard = _ts_decorate14([
2455
+ Injectable9(),
2456
+ _ts_param11(0, Inject10(Reflector2)),
2457
+ _ts_param11(1, Inject10(StaticEntitlementService)),
2458
+ _ts_param11(2, Optional7()),
2459
+ _ts_param11(2, Inject10(STATIC_FEATURE_GUARD_CONFIG_TOKEN)),
2460
+ _ts_metadata12("design:type", Function),
2461
+ _ts_metadata12("design:paramtypes", [
1446
2462
  typeof Reflector2 === "undefined" ? Object : Reflector2,
1447
2463
  typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
1448
2464
  Object
@@ -1460,27 +2476,37 @@ var SubscriptionPlanResolver = class {
1460
2476
  }
1461
2477
  subscriptions;
1462
2478
  activeStatuses;
1463
- constructor(subscriptions, activeStatuses = DEFAULT_ACTIVE_STATUSES) {
2479
+ canceledEntitlementPlan;
2480
+ constructor(subscriptions, activeStatuses = DEFAULT_ACTIVE_STATUSES, canceledEntitlementPlan = null) {
1464
2481
  this.subscriptions = subscriptions;
1465
2482
  this.activeStatuses = activeStatuses;
2483
+ this.canceledEntitlementPlan = canceledEntitlementPlan;
1466
2484
  }
1467
2485
  async getPlanIdForTenant(tenantId) {
1468
2486
  const subscription = await this.subscriptions.findByTenantId(tenantId);
1469
- if (!subscription || !this.activeStatuses.has(subscription.status)) return null;
2487
+ if (!subscription) return null;
2488
+ if (cancellationHasLanded(subscription, /* @__PURE__ */ new Date())) {
2489
+ return this.canceledEntitlementPlan;
2490
+ }
2491
+ if (!this.activeStatuses.has(subscription.status)) return null;
1470
2492
  return subscription.plan;
1471
2493
  }
1472
2494
  };
1473
2495
 
1474
2496
  // src/platform/saasicat.module.ts
1475
- import { Logger, Module as Module2 } from "@nestjs/common";
2497
+ import { Logger as Logger4, Module as Module3 } from "@nestjs/common";
1476
2498
  import { DiscoveryModule as NestDiscoveryModule } from "@nestjs/core";
1477
2499
 
2500
+ // src/platform/compose/base.ts
2501
+ import { planCatalogSettingsOf } from "@saasicat/core";
2502
+
1478
2503
  // src/platform/compose/manifest.ts
1479
2504
  var PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/PlatformSubscriptionRepository");
1480
2505
  var STANDARD_MANIFEST_REGISTRATION_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/nest/StandardManifestRegistration");
1481
2506
  function buildStandardManifestContribution(catalog, adminResources, promoCodes) {
1482
2507
  const capabilities = {
1483
- "discovery.read": true
2508
+ "discovery.read": true,
2509
+ "settings.read": true
1484
2510
  };
1485
2511
  if (catalog && catalog.adminControllers !== false) {
1486
2512
  Object.assign(capabilities, {
@@ -1532,32 +2558,25 @@ function buildStandardManifestContribution(catalog, adminResources, promoCodes)
1532
2558
  __name(buildStandardManifestContribution, "buildStandardManifestContribution");
1533
2559
  function buildMinimalManifestConfig() {
1534
2560
  return {
1535
- useFactory: /* @__PURE__ */ __name((catalog) => ({
2561
+ useFactory: /* @__PURE__ */ __name((settings) => ({
1536
2562
  project: {
1537
- key: catalog.projectKey,
1538
- displayName: catalog.app?.name ?? catalog.projectKey,
1539
- label: catalog.app?.label,
1540
- icon: catalog.app?.icon,
1541
- logoUrl: catalog.app?.logoUrl,
2563
+ // One name, from the one place an installation carries it.
2564
+ key: settings.app.name,
2565
+ displayName: settings.app.name,
2566
+ label: settings.app.label,
2567
+ icon: settings.app.icon,
2568
+ logoUrl: settings.app.logoUrl,
1542
2569
  environment: process.env.NODE_ENV === "production" ? "production" : "development",
1543
- availableLocales: catalog.marketing?.availableLocales,
1544
- defaultLocale: catalog.marketing?.availableLocales?.[0]
2570
+ availableLocales: settings.marketing?.availableLocales,
2571
+ defaultLocale: settings.marketing?.availableLocales?.[0]
1545
2572
  },
1546
2573
  build: {
1547
2574
  platformPackageVersion: "0.0.0",
1548
2575
  appVersion: "0.0.0"
1549
- },
1550
- planCatalogSnapshot: {
1551
- source: "saasicat-module",
1552
- hash: "sha256-quickstart",
1553
- currency: catalog.currency,
1554
- vatRate: catalog.vatRate,
1555
- plans: catalog.plans ?? [],
1556
- features: catalog.features ?? []
1557
2576
  }
1558
2577
  }), "useFactory"),
1559
2578
  inject: [
1560
- PLAN_CATALOG_TOKEN
2579
+ PLAN_CATALOG_SETTINGS_TOKEN
1561
2580
  ]
1562
2581
  };
1563
2582
  }
@@ -1565,43 +2584,61 @@ __name(buildMinimalManifestConfig, "buildMinimalManifestConfig");
1565
2584
 
1566
2585
  // src/platform/compose/base.ts
1567
2586
  var DEFAULT_SNAPSHOT_PATH = "var/discovery-snapshot.json";
1568
- function resolveAppInfo(options) {
1569
- return options.app ?? {
1570
- key: options.planCatalog?.projectKey ?? options.dbCatalog?.projectKey ?? "app",
1571
- version: options.planCatalog?.app?.version ?? options.dbCatalog?.app?.version ?? "0.0.0"
2587
+ var SOURCE_IN_CODE = "the object passed to SaaSiCatModule.forRoot({ planCatalog })";
2588
+ function resolveCatalog(options) {
2589
+ if (options.planCatalog) return {
2590
+ catalog: options.planCatalog
2591
+ };
2592
+ if (!dbCatalogNamesAFile(options.dbCatalog)) return {};
2593
+ try {
2594
+ return {
2595
+ catalog: loadPlanCatalogFromFile(options.dbCatalog)
2596
+ };
2597
+ } catch (failure) {
2598
+ return {
2599
+ failure: failure instanceof Error ? failure : new Error(String(failure))
2600
+ };
2601
+ }
2602
+ }
2603
+ __name(resolveCatalog, "resolveCatalog");
2604
+ function resolveSettingsSource(catalog) {
2605
+ return catalogSource(catalog) ?? SOURCE_IN_CODE;
2606
+ }
2607
+ __name(resolveSettingsSource, "resolveSettingsSource");
2608
+ function resolveAppInfo(options, catalog) {
2609
+ if (options.app) return options.app;
2610
+ return {
2611
+ key: catalog.app.name,
2612
+ version: catalog.app.version ?? "0.0.0"
1572
2613
  };
1573
2614
  }
1574
2615
  __name(resolveAppInfo, "resolveAppInfo");
1575
- function composePlanCatalog(options, sink) {
2616
+ function composePlanCatalog(options, catalog, sink) {
1576
2617
  if (options.planCatalog) {
1577
2618
  return PlanCatalogModule.forRootWithCatalog(options.planCatalog, {
1578
2619
  global: true
1579
2620
  });
1580
2621
  }
1581
- const dbCatalog = options.dbCatalog;
1582
2622
  return PlanCatalogModule.forRoot({
1583
- projectKey: dbCatalog.projectKey,
1584
- app: dbCatalog.app,
1585
- currency: dbCatalog.currency,
1586
- vatRate: dbCatalog.vatRate,
1587
- marketing: dbCatalog.marketing,
2623
+ ...planCatalogSettingsOf(catalog),
1588
2624
  sink,
1589
2625
  imports: options.imports
1590
2626
  });
1591
2627
  }
1592
2628
  __name(composePlanCatalog, "composePlanCatalog");
1593
- function composeBaseModules(options, appInfo, ports) {
2629
+ function composeBaseModules(options, catalog, appInfo, ports) {
2630
+ const { appliedSettingsPort, emailPort, ...adminPorts } = ports;
1594
2631
  return [
1595
2632
  DiscoveryModule.forRoot({
1596
2633
  app: appInfo,
1597
2634
  controller: {
1598
- guards: options.controller.guards
2635
+ guards: operatorGuards(options)
1599
2636
  },
1600
2637
  imports: options.imports,
1601
2638
  snapshotPath: options.discoverySnapshotPath === void 0 ? DEFAULT_SNAPSHOT_PATH : options.discoverySnapshotPath
1602
2639
  }),
1603
2640
  AdminModule.forRoot({
1604
- ...ports,
2641
+ ...adminPorts,
1605
2642
  imports: options.imports,
1606
2643
  global: true
1607
2644
  }),
@@ -1609,14 +2646,25 @@ function composeBaseModules(options, appInfo, ports) {
1609
2646
  config: options.adminManifestConfig ?? buildMinimalManifestConfig(),
1610
2647
  extraProviders: options.adminManifestExtraProviders,
1611
2648
  includeManifestController: options.includeManifestController,
1612
- guards: options.controller.guards,
2649
+ guards: operatorGuards(options),
1613
2650
  reloadGuards: options.reloadGuards,
1614
2651
  imports: options.imports,
1615
2652
  // Global like AdminModule above: apps register their manifest
1616
2653
  // contribution by injecting AdminManifestService into one of their
1617
- // own modules (handbook §6.6). Re-exporting the module from here
2654
+ // own modules (docs/guides/wire-the-backend.md, "Manifest
2655
+ // Contributions"). Re-exporting the module from here
1618
2656
  // does not make that injection resolvable.
1619
2657
  global: true
2658
+ }),
2659
+ SettingsModule.forRoot({
2660
+ port: appliedSettingsPort,
2661
+ email: emailPort,
2662
+ source: resolveSettingsSource(catalog),
2663
+ controller: {
2664
+ guards: operatorGuards(options)
2665
+ },
2666
+ includeController: options.includeSettingsController,
2667
+ imports: options.imports
1620
2668
  })
1621
2669
  ];
1622
2670
  }
@@ -1716,7 +2764,15 @@ function planResolverProvider(ctx, resolution) {
1716
2764
  if (resolution.viaSubscriptions) {
1717
2765
  return {
1718
2766
  provide: PLAN_RESOLVER_PORT_TOKEN,
1719
- useFactory: /* @__PURE__ */ __name((subscriptions) => new SubscriptionPlanResolver(subscriptions), "useFactory"),
2767
+ useFactory: /* @__PURE__ */ __name((subscriptions) => new SubscriptionPlanResolver(
2768
+ subscriptions,
2769
+ void 0,
2770
+ // The same floor `EntitlementService` applies, read from the
2771
+ // same option: two enforcement paths that disagree about
2772
+ // what a cancelled subscription keeps would be worse than
2773
+ // either answer on its own.
2774
+ ctx.options.entitlement?.resolutionConfig?.canceledEntitlementPlan ?? null
2775
+ ), "useFactory"),
1720
2776
  inject: [
1721
2777
  PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN
1722
2778
  ]
@@ -1730,14 +2786,14 @@ function planResolverProvider(ctx, resolution) {
1730
2786
  __name(planResolverProvider, "planResolverProvider");
1731
2787
 
1732
2788
  // src/platform/saasicat.module.ts
1733
- function _ts_decorate10(decorators, target, key, desc) {
2789
+ function _ts_decorate15(decorators, target, key, desc) {
1734
2790
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1735
2791
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1736
2792
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1737
2793
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1738
2794
  }
1739
- __name(_ts_decorate10, "_ts_decorate");
1740
- var PLATFORM_LOGGER = new Logger("SaaSiCat");
2795
+ __name(_ts_decorate15, "_ts_decorate");
2796
+ var PLATFORM_LOGGER = new Logger4("SaaSiCat");
1741
2797
  function enforcementChainState(state) {
1742
2798
  return {
1743
2799
  provide: ENFORCEMENT_CHAIN_STATE_TOKEN,
@@ -1762,29 +2818,37 @@ var SaaSiCatModule = class _SaaSiCatModule {
1762
2818
  mfa: explicit.mfa ?? persistence?.core.mfa,
1763
2819
  audit: explicit.audit ?? persistence?.core.audit,
1764
2820
  rlsBypass: explicit.rlsBypass ?? persistence?.core.rlsBypass,
2821
+ appliedSettings: explicit.appliedSettings ?? persistence?.core.appliedSettings,
2822
+ email: explicit.email,
1765
2823
  planCatalogReadSink: explicit.planCatalogReadSink ?? persistence?.planCatalogReadSink,
1766
2824
  planResolver: explicit.planResolver,
1767
2825
  subscriptionRepository: explicit.subscriptionRepository ?? persistence?.entitlement?.subscriptionRepository,
1768
2826
  planVersionRepository: explicit.planVersionRepository ?? persistence?.entitlement?.planVersionRepository,
1769
2827
  transactionRunner: explicit.transactionRunner ?? persistence?.core.transactionRunner
1770
2828
  };
2829
+ const { catalog, failure: catalogFailure } = resolveCatalog(options);
1771
2830
  assertConfiguration({
1772
2831
  options,
1773
- adapters
2832
+ adapters,
2833
+ catalog,
2834
+ catalogFailure
1774
2835
  });
1775
2836
  const mfaPort = adapters.mfa;
1776
2837
  const auditPort = adapters.audit;
1777
2838
  const rlsBypassPort = adapters.rlsBypass;
1778
- const appInfo = resolveAppInfo(options);
2839
+ const running = catalog;
2840
+ const appInfo = resolveAppInfo(options, running);
1779
2841
  const imports = [
1780
2842
  // Make injected client/auth providers visible to the high-level
1781
2843
  // module's own quota providers and automatic plan resolver too.
1782
2844
  ...options.imports ?? [],
1783
- composePlanCatalog(options, adapters.planCatalogReadSink),
1784
- ...composeBaseModules(options, appInfo, {
2845
+ composePlanCatalog(options, running, adapters.planCatalogReadSink),
2846
+ ...composeBaseModules(options, running, appInfo, {
1785
2847
  mfaPort,
1786
2848
  auditPort,
1787
- rlsBypassPort
2849
+ rlsBypassPort,
2850
+ appliedSettingsPort: adapters.appliedSettings,
2851
+ emailPort: adapters.email
1788
2852
  })
1789
2853
  ];
1790
2854
  const composition = {
@@ -1833,6 +2897,8 @@ var SaaSiCatModule = class _SaaSiCatModule {
1833
2897
  }));
1834
2898
  }
1835
2899
  if (options.enforcementChainCheck !== false) imports.push(NestDiscoveryModule);
2900
+ lightweightProviders.push(IssuerIdentityInspector, IssuerIdentityCheck);
2901
+ lightweightExports.push(IssuerIdentityInspector, IssuerIdentityCheck);
1836
2902
  const tenantManifest = composeTenantManifest(composition);
1837
2903
  lightweightProviders.push(...tenantManifest.providers);
1838
2904
  lightweightExports.push(...tenantManifest.exports);
@@ -1846,18 +2912,30 @@ var SaaSiCatModule = class _SaaSiCatModule {
1846
2912
  };
1847
2913
  }
1848
2914
  };
1849
- SaaSiCatModule = _ts_decorate10([
1850
- Module2({})
2915
+ SaaSiCatModule = _ts_decorate15([
2916
+ Module3({})
1851
2917
  ], SaaSiCatModule);
1852
2918
 
1853
2919
  export {
2920
+ EMAIL_PORT_TOKEN,
1854
2921
  SETUP_PROVISIONING_PORT_TOKEN,
1855
2922
  SETUP_CONFIG_TOKEN,
1856
2923
  SetupService,
1857
2924
  SetupController,
1858
2925
  SetupModule,
2926
+ SettingsChangeNotifier,
2927
+ describeChange,
2928
+ fingerprintOf,
2929
+ APPLIED_SETTINGS_PORT_TOKEN,
2930
+ SETTINGS_SOURCE_TOKEN,
2931
+ AppliedSettingsRecorder,
2932
+ buildSettingsController,
2933
+ SettingsModule,
1859
2934
  ENFORCEMENT_CHAIN_STATE_TOKEN,
1860
2935
  EnforcementChainCheck,
2936
+ IssuerIdentityInspector,
2937
+ IssuerIdentityCheck,
2938
+ DB_CATALOG_MEMBERS,
1861
2939
  headingSlug,
1862
2940
  docsUrlFor,
1863
2941
  PLATFORM_RULES,