@saasicat/nest 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +90 -201
  2. package/dist/_entries.cjs +3265 -2846
  3. package/dist/billing/index.d.cts +4 -4
  4. package/dist/billing/index.d.ts +4 -4
  5. package/dist/billing/index.js +33 -31
  6. package/dist/catalog/index.js +5 -5
  7. package/dist/checkout-offer/index.js +2 -2
  8. package/dist/chunk-2FR6ZL7R.js +0 -0
  9. package/dist/chunk-2JTISFUZ.js +792 -0
  10. package/dist/chunk-2SLTRKXC.js +0 -0
  11. package/dist/chunk-3E6ONOMG.js +50 -0
  12. package/dist/{chunk-UOWK2HYG.js → chunk-7ZPFQXHM.js} +1 -1
  13. package/dist/{chunk-B7NRY2LV.js → chunk-CJFAOD7K.js} +1 -39
  14. package/dist/{chunk-J62YI25C.js → chunk-EHKMPP6J.js} +1194 -775
  15. package/dist/{chunk-RPO4IVUY.js → chunk-F4B2NJUZ.js} +26 -91
  16. package/dist/chunk-H4DOTO7S.js +85 -0
  17. package/dist/{chunk-V3MRZ7SH.js → chunk-UHD6FJBQ.js} +421 -1197
  18. package/dist/{define-saasicat-BK6RRWWG.d.ts → define-saasicat-3ois8t8s.d.ts} +26 -3
  19. package/dist/{define-saasicat-D30I-WET.d.cts → define-saasicat-CFxut1VR.d.cts} +26 -3
  20. package/dist/{enforce-quota.interceptor-B9QueAFf.d.ts → enforce-quota.interceptor-CYDNvAQz.d.ts} +3 -3
  21. package/dist/{enforce-quota.interceptor-DY6CLjoP.d.cts → enforce-quota.interceptor-N80KC34N.d.cts} +3 -3
  22. package/dist/entitlement/index.js +7 -4
  23. package/dist/index.d.cts +5 -5
  24. package/dist/index.d.ts +5 -5
  25. package/dist/index.js +87 -79
  26. package/dist/{saas-platform.module-D1mUZPrz.d.ts → module-options-ByJL7FZm.d.ts} +40 -27
  27. package/dist/{saas-platform.module-CgalxzcT.d.cts → module-options-klrPnWGF.d.cts} +40 -27
  28. package/dist/platform/index.d.cts +201 -19
  29. package/dist/platform/index.d.ts +201 -19
  30. package/dist/platform/index.js +58 -33
  31. package/dist/promo/index.js +2 -2
  32. package/dist/subscription-contract/index.js +5 -2
  33. package/dist/{tenant-billing.controller-DHSUrT_N.d.cts → tenant-billing.controller-CAMMdfn1.d.cts} +1 -1
  34. package/dist/{tenant-billing.controller-BSlpFqVA.d.ts → tenant-billing.controller-CYYK0CpT.d.ts} +1 -1
  35. package/dist/{subscription-bundles.module-BDbODWvA.d.ts → tenant-billing.module-B0stLoJn.d.ts} +51 -51
  36. package/dist/{subscription-bundles.module-Cfi-lFWF.d.cts → tenant-billing.module-BxJS6fX4.d.cts} +51 -51
  37. package/dist/testing/index.d.cts +8 -8
  38. package/dist/testing/index.d.ts +8 -8
  39. package/dist/testing/index.js +13 -11
  40. package/package.json +4 -4
  41. package/dist/{chunk-QB7MICKQ.js → chunk-7X5MSAIL.js} +9 -9
  42. package/dist/{chunk-FY4EJTV4.js → chunk-OYNHOY45.js} +0 -0
  43. package/dist/{chunk-I4XD43AN.js → chunk-XTVAFJ3P.js} +3 -3
@@ -1,6 +1,18 @@
1
1
  import {
2
2
  PromoCodesModule
3
3
  } from "./chunk-B6IWLV7R.js";
4
+ import {
5
+ SubscriptionContractModule
6
+ } from "./chunk-3E6ONOMG.js";
7
+ import {
8
+ AdminManifestModule,
9
+ AdminManifestService,
10
+ AdminModule,
11
+ AdminResourcesModule,
12
+ AdminStatsModule,
13
+ MfaService,
14
+ SuperAdminGuard
15
+ } from "./chunk-SN5UOC43.js";
4
16
  import {
5
17
  FEATURE_GUARD_MARKER,
6
18
  PublicCatalogModule,
@@ -8,48 +20,38 @@ import {
8
20
  SubscriptionBundleModule,
9
21
  TenantBillingModule,
10
22
  isPlatformFeatureGuard
11
- } from "./chunk-V3MRZ7SH.js";
12
- import {
13
- SubscriptionContractModule
14
- } from "./chunk-B7NRY2LV.js";
23
+ } from "./chunk-UHD6FJBQ.js";
15
24
  import {
16
25
  CatalogModule
17
- } from "./chunk-QB7MICKQ.js";
26
+ } from "./chunk-7X5MSAIL.js";
18
27
  import {
19
28
  CheckoutOfferModule
20
- } from "./chunk-I4XD43AN.js";
29
+ } from "./chunk-XTVAFJ3P.js";
30
+ import {
31
+ SaaSiCatPublicRoute
32
+ } from "./chunk-YSC46BGM.js";
21
33
  import {
22
34
  DiscoveryModule
23
35
  } from "./chunk-6WL6VQ7Z.js";
24
36
  import {
25
- EntitlementModule,
37
+ EntitlementModule
38
+ } from "./chunk-H4DOTO7S.js";
39
+ import {
26
40
  LimitExceededError
27
- } from "./chunk-RPO4IVUY.js";
41
+ } from "./chunk-F4B2NJUZ.js";
28
42
  import {
29
43
  PLAN_CATALOG_TOKEN,
30
44
  PlanCatalogModule
31
45
  } from "./chunk-DQKCK7DX.js";
32
- import {
33
- ENFORCE_QUOTA_KEY
34
- } from "./chunk-L4SUWH5B.js";
35
- import {
36
- AdminManifestModule,
37
- AdminManifestService,
38
- AdminModule,
39
- AdminResourcesModule,
40
- AdminStatsModule,
41
- MfaService,
42
- SuperAdminGuard
43
- } from "./chunk-SN5UOC43.js";
44
- import {
45
- SaaSiCatPublicRoute
46
- } from "./chunk-YSC46BGM.js";
47
46
  import {
48
47
  codedError
49
48
  } from "./chunk-EBJJNTW6.js";
50
49
  import {
51
50
  asProvider
52
51
  } from "./chunk-SABTXESR.js";
52
+ import {
53
+ ENFORCE_QUOTA_KEY
54
+ } from "./chunk-L4SUWH5B.js";
53
55
  import {
54
56
  __name
55
57
  } from "./chunk-SHUYVCID.js";
@@ -350,23 +352,9 @@ SetupModule = _ts_decorate3([
350
352
  Module({})
351
353
  ], SetupModule);
352
354
 
353
- // src/platform/plan-resolver.port.ts
354
- var PLAN_RESOLVER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlanResolverPort");
355
- var StaticPlanResolver = class {
356
- static {
357
- __name(this, "StaticPlanResolver");
358
- }
359
- planId;
360
- constructor(planId) {
361
- this.planId = planId;
362
- }
363
- async getPlanIdForTenant() {
364
- return this.planId;
365
- }
366
- };
367
-
368
- // src/platform/static-entitlement.service.ts
369
- import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
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";
370
358
  function _ts_decorate4(decorators, target, key, desc) {
371
359
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
372
360
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -384,121 +372,63 @@ function _ts_param3(paramIndex, decorator) {
384
372
  };
385
373
  }
386
374
  __name(_ts_param3, "_ts_param");
387
- var StaticEntitlementService = class {
388
- static {
389
- __name(this, "StaticEntitlementService");
390
- }
391
- catalog;
392
- resolver;
393
- constructor(catalog, resolver) {
394
- this.catalog = catalog;
395
- this.resolver = resolver;
396
- }
397
- /**
398
- * Returns the effective features + quotas of a tenant from the
399
- * plan catalog. When the plan is missing: an empty snapshot (no feature,
400
- * all quotas at 0).
401
- */
402
- async snapshot(tenantId) {
403
- const planId = await this.resolver.getPlanIdForTenant(tenantId);
404
- if (!planId) {
405
- return {
406
- planId: null,
407
- features: [],
408
- quotas: {}
409
- };
410
- }
411
- const plan = this.findPlan(planId);
412
- if (!plan) {
413
- return {
414
- planId,
415
- features: [],
416
- quotas: {}
417
- };
418
- }
419
- return {
420
- planId,
421
- features: plan.features ?? [],
422
- quotas: plan.quotas ?? {}
423
- };
424
- }
425
- /** Convenience helper for tests / synchronous checks in the UI manifest. */
426
- async hasFeature(tenantId, featureKey) {
427
- const snap = await this.snapshot(tenantId);
428
- return snap.features.includes(featureKey);
429
- }
430
- async quotaLimit(tenantId, quotaKey) {
431
- const snap = await this.snapshot(tenantId);
432
- const value = snap.quotas[quotaKey];
433
- return value === void 0 ? null : value;
434
- }
435
- findPlan(planId) {
436
- return (this.catalog.plans ?? []).find((p) => p.id === planId);
437
- }
438
- };
439
- StaticEntitlementService = _ts_decorate4([
440
- Injectable2(),
441
- _ts_param3(0, Inject2(PLAN_CATALOG_TOKEN)),
442
- _ts_param3(1, Inject2(PLAN_RESOLVER_PORT_TOKEN)),
443
- _ts_metadata3("design:type", Function),
444
- _ts_metadata3("design:paramtypes", [
445
- typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
446
- typeof PlanResolverPort === "undefined" ? Object : PlanResolverPort
447
- ])
448
- ], StaticEntitlementService);
449
-
450
- // src/platform/feature-guard-coverage.check.ts
451
- import { Inject as Inject3, Injectable as Injectable3, Logger } from "@nestjs/common";
452
- import { DiscoveryService, MetadataScanner } from "@nestjs/core";
453
- function _ts_decorate5(decorators, target, key, desc) {
454
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
455
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
456
- 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;
457
- return c > 3 && r && Object.defineProperty(target, key, r), r;
458
- }
459
- __name(_ts_decorate5, "_ts_decorate");
460
- function _ts_metadata4(k, v) {
461
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
462
- }
463
- __name(_ts_metadata4, "_ts_metadata");
464
- function _ts_param4(paramIndex, decorator) {
465
- return function(target, key) {
466
- decorator(target, key, paramIndex);
467
- };
468
- }
469
- __name(_ts_param4, "_ts_param");
470
375
  var GUARDS_METADATA = "__guards__";
471
376
  var METHOD_METADATA = "method";
472
- var FeatureGuardCoverageCheck = class {
377
+ var ENFORCEMENT_CHAIN_STATE_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/EnforcementChainState");
378
+ var EnforcementChainCheck = class {
473
379
  static {
474
- __name(this, "FeatureGuardCoverageCheck");
380
+ __name(this, "EnforcementChainCheck");
475
381
  }
476
382
  discoveryService;
477
383
  metadataScanner;
478
- logger = new Logger("SaaSiCat");
479
- constructor(discoveryService, metadataScanner) {
384
+ state;
385
+ constructor(discoveryService, metadataScanner, state = {
386
+ featureEnforcementActive: true,
387
+ quotaEnforcementActive: true,
388
+ globalGuardBound: true,
389
+ globalGuardOptedOut: false
390
+ }) {
480
391
  this.discoveryService = discoveryService;
481
392
  this.metadataScanner = metadataScanner;
393
+ this.state = state;
482
394
  }
483
395
  onApplicationBootstrap() {
484
- const uncovered = this.findUncoveredRoutes();
396
+ const annotated = this.findAnnotatedRoutes();
397
+ if (!this.state.featureEnforcementActive) {
398
+ if (annotated.length === 0) return;
399
+ throw new Error(`${annotated.length} route(s) are annotated for licence enforcement, but nothing can resolve a tenant to a plan, so @RequireFeature and @EnforceQuota do nothing at all: every annotated route serves unlicensed traffic and every quota reads as unlimited.
400
+ ${describe(annotated)}
401
+ Set one of: adapters.planResolver, defaultPlanId, or tenantBilling. If this application deliberately publishes a catalogue without enforcing it, remove the annotations \u2014 a decorator that cannot act is not documentation, it is a claim the code does not keep.`);
402
+ }
403
+ if (!this.state.quotaEnforcementActive) {
404
+ const unenforced = annotated.filter((r) => r.quota !== null);
405
+ if (unenforced.length > 0) {
406
+ throw new Error(`${unenforced.length} route(s) enforce a quota, but nothing registers EnforceQuotaInterceptor, so every @EnforceQuota reads as unlimited and the limit a customer paid for is not applied:
407
+ ${describe(unenforced)}
408
+ The V3 entitlement stack resolves features, not quotas: the interceptor is registered only where a plan can be resolved. Set one of adapters.planResolver, defaultPlanId, or tenantBilling with a subscription repository \u2014 or remove the annotations, because a decorator that cannot act is not documentation.
409
+ If this application enforces these quotas itself, set \`enforcementChainCheck: false\` \u2014 it turns this check off and nothing else.`);
410
+ }
411
+ }
412
+ if (this.state.globalGuardBound) return;
413
+ const uncovered = annotated.filter((r) => r.features.length > 0 && !r.guarded);
485
414
  if (uncovered.length === 0) return;
486
- const list = uncovered.map((r) => ` ${r.controller}.${r.handler} \u2014 requires ${r.features.join(" or ")}`).join("\n");
487
- this.logger.warn(`globalFeatureGuard is off and ${uncovered.length} annotated route(s) have no feature guard in front of them, so they serve unlicensed traffic:
488
- ${list}
489
- Bind one behind your auth guard \u2014 @UseGuards(JwtAuthGuard, StaticFeatureGuard), or FeatureGuard from @saasicat/nest/billing on the V3 entitlement path. A guard of your own that wraps either one is not recognised here; if that is what these routes use, they are covered.`);
415
+ const why = this.state.globalGuardOptedOut ? "globalFeatureGuard is off" : "the platform binds no global feature guard on this entitlement path";
416
+ throw new Error(`${why} and ${uncovered.length} annotated route(s) have no feature guard in front of them, so they serve unlicensed traffic:
417
+ ${describe(uncovered)}
418
+ Bind one behind your auth guard \u2014 @UseGuards(JwtAuthGuard, StaticFeatureGuard), or FeatureGuard from @saasicat/nest/billing on the V3 entitlement path.
419
+ Two things this cannot see, and the way out of each: a guard of your own that wraps ours (put FEATURE_GUARD_MARKER on the wrapper class \u2014 the symbol StaticFeatureGuard and FeatureGuard both carry), and a feature guard you bind globally as an APP_GUARD rather than per controller. If either is what these routes use, set \`enforcementChainCheck: false\` \u2014 it turns this check off and nothing else.`);
490
420
  }
491
421
  /**
492
- * Annotated handlers whose own guards and whose controller's guards contain
493
- * no recognisable feature guard.
422
+ * Every route carrying an enforcement annotation, with whether a
423
+ * recognisable feature guard covers it.
494
424
  *
495
425
  * Deliberately conservative in one direction only: an unrecognised wrapper
496
426
  * is reported (and the message says so) rather than assumed safe, because
497
427
  * the named route is cheap to check. The reverse — staying silent about a
498
428
  * route with no guard at all — is the failure this exists to prevent.
499
429
  */
500
- findUncoveredRoutes() {
501
- const uncovered = [];
430
+ findAnnotatedRoutes() {
431
+ const annotated = [];
502
432
  for (const wrapper of this.discoveryService.getControllers()) {
503
433
  const instance = wrapper.instance;
504
434
  if (!instance || typeof instance !== "object") continue;
@@ -510,39 +440,58 @@ Bind one behind your auth guard \u2014 @UseGuards(JwtAuthGuard, StaticFeatureGua
510
440
  const handler = instance[method];
511
441
  if (typeof handler !== "function") continue;
512
442
  if (Reflect.getMetadata(METHOD_METADATA, handler) === void 0) continue;
513
- const features = readFeatures(handler) ?? readFeatures(ctor) ?? /* not annotated */
514
- null;
515
- if (!features || features.length === 0) continue;
443
+ const features = readFeatures(handler) ?? readFeatures(ctor) ?? /* none */
444
+ [];
445
+ const quota = readQuota(handler) ?? readQuota(ctor);
446
+ if (features.length === 0 && quota === null) continue;
516
447
  const guards = [
517
448
  ...classGuards,
518
449
  ...readGuards(handler)
519
450
  ];
520
- if (guards.some(isPlatformFeatureGuard)) continue;
521
- uncovered.push({
451
+ annotated.push({
522
452
  controller: ctor.name,
523
453
  handler: method,
524
- features
454
+ features,
455
+ quota,
456
+ guarded: guards.some(isPlatformFeatureGuard)
525
457
  });
526
458
  }
527
459
  }
528
- return uncovered;
460
+ return annotated;
529
461
  }
530
462
  };
531
- FeatureGuardCoverageCheck = _ts_decorate5([
532
- Injectable3(),
533
- _ts_param4(0, Inject3(DiscoveryService)),
534
- _ts_param4(1, Inject3(MetadataScanner)),
535
- _ts_metadata4("design:type", Function),
536
- _ts_metadata4("design:paramtypes", [
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", [
537
471
  typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
538
- typeof MetadataScanner === "undefined" ? Object : MetadataScanner
472
+ typeof MetadataScanner === "undefined" ? Object : MetadataScanner,
473
+ typeof EnforcementChainState === "undefined" ? Object : EnforcementChainState
539
474
  ])
540
- ], FeatureGuardCoverageCheck);
475
+ ], EnforcementChainCheck);
476
+ function describe(routes) {
477
+ return routes.map((r) => {
478
+ const requirement = r.features.length > 0 ? `requires ${r.features.join(" or ")}` : `enforces quota ${r.quota}`;
479
+ return ` ${r.controller}.${r.handler} \u2014 ${requirement}`;
480
+ }).join("\n");
481
+ }
482
+ __name(describe, "describe");
541
483
  function readFeatures(target) {
542
484
  const value = Reflect.getMetadata(REQUIRE_FEATURE_KEY, target);
543
485
  return Array.isArray(value) ? value : null;
544
486
  }
545
487
  __name(readFeatures, "readFeatures");
488
+ function readQuota(target) {
489
+ const value = Reflect.getMetadata(ENFORCE_QUOTA_KEY, target);
490
+ if (typeof value !== "object" || value === null) return null;
491
+ const quotaKey = value.quotaKey;
492
+ return typeof quotaKey === "string" ? quotaKey : null;
493
+ }
494
+ __name(readQuota, "readQuota");
546
495
  function readGuards(target) {
547
496
  const value = Reflect.getMetadata(GUARDS_METADATA, target);
548
497
  if (!Array.isArray(value)) return [];
@@ -555,179 +504,217 @@ function readGuards(target) {
555
504
  }
556
505
  __name(readGuards, "readGuards");
557
506
 
558
- // src/platform/static-feature.guard.ts
559
- import { ForbiddenException as ForbiddenException2, Inject as Inject4, Injectable as Injectable4, Optional } from "@nestjs/common";
560
- import { Reflector } from "@nestjs/core";
561
- import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
562
- function _ts_decorate6(decorators, target, key, desc) {
563
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
564
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
565
- 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;
566
- return c > 3 && r && Object.defineProperty(target, key, r), r;
507
+ // src/platform/validation/rules.ts
508
+ import { assertPersistenceCapabilities } from "@saasicat/types";
509
+ var DOCS_BASE = "https://github.com/uelker70/saasicat/blob/main/docs/reference/options.md";
510
+ var headingSlug = /* @__PURE__ */ __name((text) => text.trim().toLowerCase().replace(/[^a-z0-9 -]/g, "").replace(/ /g, "-"), "headingSlug");
511
+ var docsUrlFor = /* @__PURE__ */ __name((id) => `${DOCS_BASE}#${headingSlug(id)}`, "docsUrlFor");
512
+ function optionsOf(slot) {
513
+ return typeof slot === "object" ? slot : void 0;
567
514
  }
568
- __name(_ts_decorate6, "_ts_decorate");
569
- function _ts_metadata5(k, v) {
570
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
515
+ __name(optionsOf, "optionsOf");
516
+ var bundle = /* @__PURE__ */ __name((c) => c.options.persistence, "bundle");
517
+ var requiresFullEntitlement = /* @__PURE__ */ __name((c) => Boolean(c.options.entitlement || c.options.tenantBilling || c.options.subscriptionBundles), "requiresFullEntitlement");
518
+ var resolvesPlans = /* @__PURE__ */ __name((c) => Boolean(c.adapters.planResolver || c.options.defaultPlanId || c.options.tenantBilling && c.adapters.subscriptionRepository), "resolvesPlans");
519
+ function absent(required) {
520
+ return Object.keys(required).filter((key) => !required[key]);
571
521
  }
572
- __name(_ts_metadata5, "_ts_metadata");
573
- function _ts_param5(paramIndex, decorator) {
574
- return function(target, key) {
575
- decorator(target, key, paramIndex);
522
+ __name(absent, "absent");
523
+ var list = /* @__PURE__ */ __name((names) => names.join(", "), "list");
524
+ var RULE_SPECS = [
525
+ {
526
+ id: "core.adapters-bound",
527
+ when: /* @__PURE__ */ __name(() => true, "when"),
528
+ assert: /* @__PURE__ */ __name((c) => absent(coreAdapters(c)).length === 0, "assert"),
529
+ message: /* @__PURE__ */ __name((c) => `the core adapters are missing: ${list(absent(coreAdapters(c)))}. Bind them through \`persistence: prismaPersistence({ client })\` or one by one through \`adapters\`.`, "message")
530
+ },
531
+ {
532
+ id: "catalog.identity-or-sink",
533
+ when: /* @__PURE__ */ __name((c) => !c.options.planCatalog, "when"),
534
+ 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."
536
+ },
537
+ {
538
+ id: "catalog.requires-persistence",
539
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.catalog), "when"),
540
+ assert: /* @__PURE__ */ __name((c) => Boolean(bundle(c)?.catalog), "assert"),
541
+ message: "catalog is enabled, but `persistence.catalog` is missing. Use a complete persistence bundle, or wire `CatalogModule` directly."
542
+ },
543
+ {
544
+ id: "admin-resources.requires-persistence",
545
+ when: /* @__PURE__ */ __name((c) => {
546
+ if (!c.options.adminResources) return false;
547
+ return !optionsOf(c.options.adminResources)?.resources;
548
+ }, "when"),
549
+ assert: /* @__PURE__ */ __name((c) => Boolean(bundle(c)?.adminResources), "assert"),
550
+ message: "adminResources is enabled, but `persistence.adminResources` is missing. Use a complete persistence bundle, or pass your own `adminResources.resources`."
551
+ },
552
+ {
553
+ id: "promo-codes.requires-persistence-and-transactions",
554
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.promoCodes), "when"),
555
+ assert: /* @__PURE__ */ __name((c) => Boolean(bundle(c)?.promo && c.adapters.transactionRunner), "assert"),
556
+ message: /* @__PURE__ */ __name((c) => `promoCodes requires \`persistence.promo\` and a transactionRunner; missing: ${list(absent({
557
+ "persistence.promo": bundle(c)?.promo,
558
+ transactionRunner: c.adapters.transactionRunner
559
+ }))}. Redeeming a code writes two rows and must not half-apply.`, "message")
560
+ },
561
+ {
562
+ id: "setup.requires-provisioning-port",
563
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.setup), "when"),
564
+ assert: /* @__PURE__ */ __name((c) => Boolean(optionsOf(c.options.setup)?.provisioningPort ?? bundle(c)?.core.superAdminProvisioning), "assert"),
565
+ message: "setup is enabled, but no provisioningPort is available. Set `setup.provisioningPort` or use a bundle providing `persistence.core.superAdminProvisioning`."
566
+ },
567
+ {
568
+ id: "admin-stats.requires-audit-stats-port",
569
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.adminStats), "when"),
570
+ assert: /* @__PURE__ */ __name((c) => Boolean(optionsOf(c.options.adminStats)?.auditStatsPort ?? bundle(c)?.core.auditStats), "assert"),
571
+ message: "adminStats is enabled, but no auditStatsPort is available. Set `adminStats.auditStatsPort` or use a bundle providing `persistence.core.auditStats`."
572
+ },
573
+ {
574
+ id: "subscription-contract.requires-repository",
575
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.subscriptionContract), "when"),
576
+ assert: /* @__PURE__ */ __name((c) => Boolean(optionsOf(c.options.subscriptionContract)?.subscriptionContractRepository ?? bundle(c)?.entitlement?.subscriptionContractRepository), "assert"),
577
+ message: "subscriptionContract is enabled, but no repository is available. Set `subscriptionContract.subscriptionContractRepository` or use a compatible persistence bundle."
578
+ },
579
+ {
580
+ id: "promo-codes.public-preview-requires-first-time-check",
581
+ when: /* @__PURE__ */ __name((c) => {
582
+ const config = optionsOf(c.options.promoCodes);
583
+ return config !== void 0 && config.includePublicController !== false;
584
+ }, "when"),
585
+ assert: /* @__PURE__ */ __name((c) => Boolean(optionsOf(c.options.promoCodes)?.firstTimeCustomerCheck), "assert"),
586
+ message: "the public promo preview is mounted but has no `firstTimeCustomerCheck`, so every first-time-only code would preview as unavailable. Pass one, or set `promoCodes.includePublicController: false`."
587
+ },
588
+ {
589
+ id: "tenant-billing.requires-ports",
590
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.tenantBilling) && !bundle(c)?.tenantBilling, "when"),
591
+ assert: /* @__PURE__ */ __name((c) => absent(tenantBillingPorts(c)).length === 0, "assert"),
592
+ message: /* @__PURE__ */ __name((c) => `tenantBilling is enabled without a \`persistence.tenantBilling\` slice, and these adapters are missing: ${list(absent(tenantBillingPorts(c)))}.`, "message")
593
+ },
594
+ {
595
+ id: "subscription-bundles.requires-tenant-billing",
596
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.subscriptionBundles), "when"),
597
+ assert: /* @__PURE__ */ __name((c) => Boolean(c.options.tenantBilling), "assert"),
598
+ message: "subscriptionBundles requires tenantBilling, so the two share authentication and subscription usage rather than resolving them twice."
599
+ },
600
+ {
601
+ id: "subscription-bundles.requires-repositories",
602
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.subscriptionBundles), "when"),
603
+ assert: /* @__PURE__ */ __name((c) => absent(bundleRepositories(c)).length === 0, "assert"),
604
+ message: /* @__PURE__ */ __name((c) => `subscriptionBundles is enabled, but these persistence adapters are missing: ${list(absent(bundleRepositories(c)))}.`, "message")
605
+ },
606
+ {
607
+ id: "entitlement.requires-adapters",
608
+ when: requiresFullEntitlement,
609
+ assert: /* @__PURE__ */ __name((c) => absent(entitlementAdapters(c)).length === 0, "assert"),
610
+ message: /* @__PURE__ */ __name((c) => `entitlement active or required by the standard stack, but adapters are missing: ${list(absent(entitlementAdapters(c)))}.`, "message")
611
+ },
612
+ {
613
+ id: "entitlement.requires-transactional-persistence",
614
+ when: /* @__PURE__ */ __name((c) => requiresFullEntitlement(c) && Boolean(bundle(c)), "when"),
615
+ assert: /* @__PURE__ */ __name((c) => absent(entitlementCapabilities(c)).length === 0, "assert"),
616
+ message: /* @__PURE__ */ __name((c) => `the entitlement stack needs a store that can do ${list(absent(entitlementCapabilities(c)))}, and the persistence bundle declares it cannot. \`enforceLimit()\` reads a counter and writes it back; without a transaction and a row lock two concurrent requests both pass the limit.`, "message"),
617
+ // `PersistenceCapabilityError` carries `code:
618
+ // 'PERSISTENCE_CAPABILITY_MISSING'`, which is a machine-readable part
619
+ // of the published contract rather than prose. Raising it through the
620
+ // shared helper keeps the class, the code and the wording identical to
621
+ // what a consumer catches today.
622
+ error: /* @__PURE__ */ __name((c) => {
623
+ assertPersistenceCapabilities(bundle(c).capabilities, {
624
+ transactions: true,
625
+ pessimisticLocking: true
626
+ }, "SaasPlatformModule entitlement (transactional enforceLimit)");
627
+ throw new Error("persistence capabilities check did not fail as expected");
628
+ }, "error")
629
+ },
630
+ {
631
+ id: "tenant-manifest.requires-plan-resolution",
632
+ when: /* @__PURE__ */ __name((c) => Boolean(c.options.tenantManifest), "when"),
633
+ assert: resolvesPlans,
634
+ message: "tenantManifest is enabled but nothing can resolve a tenant to a plan, so it could not filter features. Set `defaultPlanId`, `adapters.planResolver`, or enable `tenantBilling`."
635
+ }
636
+ ];
637
+ var PLATFORM_RULES = RULE_SPECS.map((spec) => ({
638
+ ...spec,
639
+ docs: docsUrlFor(spec.id)
640
+ }));
641
+ var coreAdapters = /* @__PURE__ */ __name((c) => ({
642
+ mfa: c.adapters.mfa,
643
+ audit: c.adapters.audit,
644
+ rlsBypass: c.adapters.rlsBypass
645
+ }), "coreAdapters");
646
+ var tenantBillingPorts = /* @__PURE__ */ __name((c) => {
647
+ const config = optionsOf(c.options.tenantBilling);
648
+ return {
649
+ subscriptionUsagePort: config?.subscriptionUsagePort,
650
+ subscriptionWritePort: config?.subscriptionWritePort
576
651
  };
652
+ }, "tenantBillingPorts");
653
+ var bundleRepositories = /* @__PURE__ */ __name((c) => ({
654
+ subscriptionBundleRepository: bundle(c)?.entitlement?.subscriptionBundleRepository,
655
+ bundleRepository: bundle(c)?.catalog?.bundleRepository
656
+ }), "bundleRepositories");
657
+ var entitlementAdapters = /* @__PURE__ */ __name((c) => ({
658
+ subscriptionRepository: c.adapters.subscriptionRepository,
659
+ planVersionRepository: c.adapters.planVersionRepository,
660
+ transactionRunner: c.adapters.transactionRunner
661
+ }), "entitlementAdapters");
662
+ var entitlementCapabilities = /* @__PURE__ */ __name((c) => ({
663
+ transactions: bundle(c)?.capabilities.transactions,
664
+ "pessimistic locking": bundle(c)?.capabilities.pessimisticLocking
665
+ }), "entitlementCapabilities");
666
+
667
+ // src/platform/validation/validate.ts
668
+ function findViolations(configuration, rules = PLATFORM_RULES) {
669
+ const violations = [];
670
+ for (const rule of rules) {
671
+ if (!rule.when(configuration)) continue;
672
+ if (rule.assert(configuration)) continue;
673
+ violations.push({
674
+ id: rule.id,
675
+ message: typeof rule.message === "string" ? rule.message : rule.message(configuration),
676
+ docs: rule.docs
677
+ });
678
+ }
679
+ return violations;
577
680
  }
578
- __name(_ts_param5, "_ts_param");
579
- var STATIC_FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StaticFeatureGuardConfig");
580
- var StaticFeatureGuard = class {
681
+ __name(findViolations, "findViolations");
682
+ var SaaSiCatConfigurationError = class extends Error {
581
683
  static {
582
- __name(this, "StaticFeatureGuard");
583
- }
584
- reflector;
585
- entitlements;
586
- config;
587
- /** Lets `FeatureGuardCoverageCheck` recognise this guard across CJS entries. */
588
- static [FEATURE_GUARD_MARKER] = true;
589
- constructor(reflector, entitlements, config = null) {
590
- this.reflector = reflector;
591
- this.entitlements = entitlements;
592
- this.config = config;
684
+ __name(this, "SaaSiCatConfigurationError");
593
685
  }
594
- async canActivate(context) {
595
- const required = this.reflector.getAllAndOverride(REQUIRE_FEATURE_KEY, [
596
- context.getHandler(),
597
- context.getClass()
598
- ]);
599
- if (!required || required.length === 0) return true;
600
- const request = context.switchToHttp().getRequest();
601
- const user = request.user;
602
- if (!user) {
603
- throw new ForbiddenException2({
604
- code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
605
- message: "Not authenticated"
606
- });
607
- }
608
- const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
609
- if (role === "SUPER_ADMIN") return true;
610
- const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
611
- if (!tenantId) {
612
- throw new ForbiddenException2({
613
- code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
614
- message: "No tenant assigned"
615
- });
616
- }
617
- const snap = await this.entitlements.snapshot(tenantId);
618
- const allowed = required.some((f) => snap.features.includes(f));
619
- if (!allowed) {
620
- const body = {
621
- code: FEATURE_NOT_LICENSED,
622
- featureKey: required[0],
623
- featureKeys: required,
624
- offers: [],
625
- message: `Feature ${required.join(" / ")} is not included in the current plan.`
626
- };
627
- throw new ForbiddenException2(body);
628
- }
629
- return true;
686
+ violations;
687
+ constructor(violations) {
688
+ super(formatViolations(violations)), this.violations = violations;
689
+ this.name = "SaaSiCatConfigurationError";
630
690
  }
631
691
  };
632
- StaticFeatureGuard = _ts_decorate6([
633
- Injectable4(),
634
- _ts_param5(0, Inject4(Reflector)),
635
- _ts_param5(1, Inject4(StaticEntitlementService)),
636
- _ts_param5(2, Optional()),
637
- _ts_param5(2, Inject4(STATIC_FEATURE_GUARD_CONFIG_TOKEN)),
638
- _ts_metadata5("design:type", Function),
639
- _ts_metadata5("design:paramtypes", [
640
- typeof Reflector === "undefined" ? Object : Reflector,
641
- typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
642
- Object
643
- ])
644
- ], StaticFeatureGuard);
645
-
646
- // src/platform/enforce-quota.interceptor.ts
647
- import { Inject as Inject5, Injectable as Injectable5, Optional as Optional2 } from "@nestjs/common";
648
- import { Reflector as Reflector2 } from "@nestjs/core";
649
- import { from } from "rxjs";
650
- import { switchMap } from "rxjs/operators";
651
- function _ts_decorate7(decorators, target, key, desc) {
652
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
653
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
654
- 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;
655
- return c > 3 && r && Object.defineProperty(target, key, r), r;
656
- }
657
- __name(_ts_decorate7, "_ts_decorate");
658
- function _ts_metadata6(k, v) {
659
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
692
+ function formatViolations(violations) {
693
+ const heading = violations.length === 1 ? "SaaSiCatModule.forRoot: 1 configuration problem:" : `SaaSiCatModule.forRoot: ${violations.length} configuration problems:`;
694
+ const body = violations.map((v, index) => ` ${index + 1}. [${v.id}] ${v.message}
695
+ \u2192 ${v.docs}`).join("\n");
696
+ return `${heading}
697
+ ${body}`;
660
698
  }
661
- __name(_ts_metadata6, "_ts_metadata");
662
- function _ts_param6(paramIndex, decorator) {
663
- return function(target, key) {
664
- decorator(target, key, paramIndex);
665
- };
699
+ __name(formatViolations, "formatViolations");
700
+ function assertConfiguration(configuration, rules = PLATFORM_RULES) {
701
+ const violations = findViolations(configuration, rules);
702
+ if (violations.length === 0) return;
703
+ if (violations.length === 1) {
704
+ const rule = rules.find((r) => r.id === violations[0].id);
705
+ if (rule?.error) throw rule.error(configuration);
706
+ }
707
+ throw new SaaSiCatConfigurationError(violations);
666
708
  }
667
- __name(_ts_param6, "_ts_param");
668
- var QUOTA_PROVIDERS_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/QuotaProviders");
669
- var EnforceQuotaInterceptor = class {
709
+ __name(assertConfiguration, "assertConfiguration");
710
+
711
+ // src/platform/quota-providers-usage-snapshot.ts
712
+ var QuotaProvidersUsageSnapshot = class {
670
713
  static {
671
- __name(this, "EnforceQuotaInterceptor");
714
+ __name(this, "QuotaProvidersUsageSnapshot");
672
715
  }
673
- reflector;
674
- entitlements;
675
716
  providers;
676
- constructor(reflector, entitlements, providers = null) {
677
- this.reflector = reflector;
678
- this.entitlements = entitlements;
679
- this.providers = providers;
680
- }
681
- intercept(context, next) {
682
- const meta = this.reflector.getAllAndOverride(ENFORCE_QUOTA_KEY, [
683
- context.getHandler(),
684
- context.getClass()
685
- ]);
686
- if (!meta?.quotaKey) return next.handle();
687
- return from(this.check(context, meta)).pipe(switchMap(() => next.handle()));
688
- }
689
- async check(context, meta) {
690
- const request = context.switchToHttp().getRequest();
691
- const user = request.user;
692
- if (!user) return;
693
- const role = user.role ?? user.platformRole;
694
- if (role === "SUPER_ADMIN") return;
695
- const tenantId = request.tenantId ?? user.tenantId;
696
- if (!tenantId) return;
697
- const provider = (this.providers ?? []).find((p) => p.key === meta.quotaKey);
698
- if (!provider) {
699
- return;
700
- }
701
- const limit = await this.entitlements.quotaLimit(tenantId, meta.quotaKey);
702
- if (limit === null || limit === -1) return;
703
- const current = await provider.count(tenantId);
704
- const delta = meta.incrementBy ?? 1;
705
- if (current + delta > limit) {
706
- throw new LimitExceededError(meta.quotaKey, limit, current);
707
- }
708
- }
709
- };
710
- EnforceQuotaInterceptor = _ts_decorate7([
711
- Injectable5(),
712
- _ts_param6(0, Inject5(Reflector2)),
713
- _ts_param6(1, Inject5(StaticEntitlementService)),
714
- _ts_param6(2, Optional2()),
715
- _ts_param6(2, Inject5(QUOTA_PROVIDERS_TOKEN)),
716
- _ts_metadata6("design:type", Function),
717
- _ts_metadata6("design:paramtypes", [
718
- typeof Reflector2 === "undefined" ? Object : Reflector2,
719
- typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
720
- Object
721
- ])
722
- ], EnforceQuotaInterceptor);
723
-
724
- // src/platform/quota-providers-usage-snapshot.ts
725
- var QuotaProvidersUsageSnapshot = class {
726
- static {
727
- __name(this, "QuotaProvidersUsageSnapshot");
728
- }
729
- providers;
730
- constructor(providers) {
717
+ constructor(providers) {
731
718
  this.providers = providers;
732
719
  }
733
720
  async snapshot(tenantId) {
@@ -739,41 +726,443 @@ var QuotaProvidersUsageSnapshot = class {
739
726
  }
740
727
  };
741
728
 
742
- // src/platform/subscription-plan-resolver.ts
743
- var DEFAULT_ACTIVE_STATUSES = /* @__PURE__ */ new Set([
744
- "ACTIVE",
745
- "TRIAL"
746
- ]);
747
- var SubscriptionPlanResolver = class {
729
+ // src/platform/compose/context.ts
730
+ function optionsOf2(slot) {
731
+ return typeof slot === "object" ? slot : {};
732
+ }
733
+ __name(optionsOf2, "optionsOf");
734
+
735
+ // src/platform/compose/admin-resources.ts
736
+ function composeAdminResources({ options, persistence }) {
737
+ if (!options.adminResources) return [];
738
+ const config = optionsOf2(options.adminResources);
739
+ return [
740
+ AdminResourcesModule.forRoot({
741
+ resources: config.resources ?? persistence?.adminResources?.resources,
742
+ guards: config.guards ?? [
743
+ ...options.controller.guards,
744
+ SuperAdminGuard
745
+ ],
746
+ imports: config.imports ?? options.imports
747
+ })
748
+ ];
749
+ }
750
+ __name(composeAdminResources, "composeAdminResources");
751
+
752
+ // src/platform/compose/catalog.ts
753
+ function composeCatalog({ options, persistence, appInfo }) {
754
+ const config = options.catalog;
755
+ if (!config) return [];
756
+ const catalog = persistence?.catalog;
757
+ const modules = [
758
+ CatalogModule.forRoot({
759
+ planRepository: catalog.planRepository,
760
+ bundleRepository: catalog.bundleRepository,
761
+ catalogEntryRepository: catalog.catalogEntryRepository,
762
+ marketingProjectionRepository: catalog.marketingProjectionRepository,
763
+ promotionRepository: catalog.promotionRepository,
764
+ marketingSettingsRepository: catalog.marketingSettingsRepository,
765
+ controller: config.adminControllers === false ? void 0 : {
766
+ guards: options.controller.guards
767
+ },
768
+ imports: config.imports ?? options.imports,
769
+ extraProviders: config.extraProviders,
770
+ strictModeCheckMode: config.strictModeCheckMode,
771
+ autoSyncDiscoveryAtBoot: config.autoSyncDiscoveryAtBoot,
772
+ marketedOnlyFeatures: config.marketedOnlyFeatures,
773
+ featureUiRegistry: config.featureUiRegistry,
774
+ publicMarketingCatalog: config.publicMarketingCatalog
775
+ })
776
+ ];
777
+ if (config.publicCatalog !== false) {
778
+ modules.push(PublicCatalogModule.forRoot({
779
+ featureUiRegistry: config.featureUiRegistry,
780
+ projectKey: appInfo.key,
781
+ bundleRepository: catalog.bundleRepository,
782
+ marketingRepository: catalog.marketingProjectionRepository,
783
+ catalogEntryRepository: catalog.catalogEntryRepository,
784
+ imports: config.imports ?? options.imports
785
+ }));
786
+ }
787
+ return modules;
788
+ }
789
+ __name(composeCatalog, "composeCatalog");
790
+
791
+ // src/platform/compose/entitlement.ts
792
+ function composeEntitlement({ options, adapters, persistence, requiresFullEntitlement: requiresFullEntitlement2 }) {
793
+ if (!requiresFullEntitlement2) return [];
794
+ return [
795
+ EntitlementModule.forRoot({
796
+ subscriptionRepository: adapters.subscriptionRepository,
797
+ planVersionRepository: adapters.planVersionRepository,
798
+ transactionRunner: adapters.transactionRunner,
799
+ resolutionConfig: options.entitlement ? options.entitlement.resolutionConfig : void 0,
800
+ subscriptionContractRepository: persistence?.entitlement?.subscriptionContractRepository,
801
+ subscriptionBundleRepository: persistence?.entitlement?.subscriptionBundleRepository,
802
+ bundleRepository: persistence?.entitlement?.bundleRepository,
803
+ imports: options.imports,
804
+ global: Boolean(options.tenantBilling || options.subscriptionBundles)
805
+ })
806
+ ];
807
+ }
808
+ __name(composeEntitlement, "composeEntitlement");
809
+
810
+ // src/platform/compose/optional-modules.ts
811
+ function composeSetup({ options, persistence }) {
812
+ if (!options.setup) return [];
813
+ const config = optionsOf2(options.setup);
814
+ return [
815
+ SetupModule.forRoot({
816
+ ...config,
817
+ provisioningPort: config.provisioningPort ?? persistence?.core.superAdminProvisioning,
818
+ imports: config.imports ?? options.imports
819
+ })
820
+ ];
821
+ }
822
+ __name(composeSetup, "composeSetup");
823
+ function composeAdminStats({ options, persistence }) {
824
+ const config = options.adminStats;
825
+ if (!config) return [];
826
+ return [
827
+ AdminStatsModule.forRoot({
828
+ ...config,
829
+ guards: config.guards ?? [
830
+ ...options.controller.guards,
831
+ SuperAdminGuard
832
+ ],
833
+ auditStatsPort: config.auditStatsPort ?? persistence?.core.auditStats,
834
+ imports: config.imports ?? options.imports
835
+ })
836
+ ];
837
+ }
838
+ __name(composeAdminStats, "composeAdminStats");
839
+ function composeCheckoutOffer({ options, persistence }) {
840
+ const config = options.checkoutOffer;
841
+ if (!config) return [];
842
+ return [
843
+ CheckoutOfferModule.forRoot({
844
+ ...config,
845
+ bundleRepository: config.bundleRepository ?? persistence?.catalog?.bundleRepository,
846
+ planRepository: config.planRepository ?? persistence?.catalog?.planRepository,
847
+ catalogEntryRepository: config.catalogEntryRepository ?? persistence?.catalog?.catalogEntryRepository,
848
+ imports: config.imports ?? options.imports
849
+ })
850
+ ];
851
+ }
852
+ __name(composeCheckoutOffer, "composeCheckoutOffer");
853
+ function composeSubscriptionContract({ options, persistence }) {
854
+ if (!options.subscriptionContract) return [];
855
+ const config = optionsOf2(options.subscriptionContract);
856
+ return [
857
+ SubscriptionContractModule.forRoot({
858
+ ...config,
859
+ subscriptionContractRepository: config.subscriptionContractRepository ?? persistence?.entitlement?.subscriptionContractRepository,
860
+ imports: config.imports ?? options.imports
861
+ })
862
+ ];
863
+ }
864
+ __name(composeSubscriptionContract, "composeSubscriptionContract");
865
+
866
+ // src/platform/compose/promo-codes.ts
867
+ function composePromoCodes({ options, adapters, persistence }) {
868
+ if (!options.promoCodes) return [];
869
+ const config = optionsOf2(options.promoCodes);
870
+ const promo = persistence?.promo;
871
+ const { firstTimeCustomerCheck, adminGuards, imports: promoImports, includePublicController, adminController, ...promoOptions } = config;
872
+ return [
873
+ PromoCodesModule.forRoot({
874
+ ...promoOptions,
875
+ promoCodeRepository: promo.promoCodeRepository,
876
+ redemptionRepository: promo.redemptionRepository,
877
+ validationLogRepository: promo.validationLogRepository,
878
+ subscriptionLookup: promo.subscriptionLookup,
879
+ revenueAggregator: promo.revenueAggregator,
880
+ transactionRunner: adapters.transactionRunner,
881
+ firstTimeCustomerCheck: firstTimeCustomerCheck ?? {
882
+ hasExistingCustomerForEmail: /* @__PURE__ */ __name(async () => false, "hasExistingCustomerForEmail")
883
+ },
884
+ includePublicController: includePublicController ?? false,
885
+ adminController: adminController === false ? false : {
886
+ guards: adminGuards ?? [
887
+ ...options.controller.guards,
888
+ SuperAdminGuard
889
+ ]
890
+ },
891
+ imports: promoImports ?? options.imports
892
+ })
893
+ ];
894
+ }
895
+ __name(composePromoCodes, "composePromoCodes");
896
+
897
+ // src/platform/compose/tenant-bindings.ts
898
+ function normalizeTenantAuthGuards(guards) {
899
+ if (!Array.isArray(guards)) return guards;
900
+ return {
901
+ useFactory: /* @__PURE__ */ __name((...instances) => instances, "useFactory"),
902
+ inject: guards
903
+ };
904
+ }
905
+ __name(normalizeTenantAuthGuards, "normalizeTenantAuthGuards");
906
+ function quotaUsageSnapshotProvider(quotaProviders) {
907
+ return {
908
+ useFactory: /* @__PURE__ */ __name((...providers) => new QuotaProvidersUsageSnapshot(providers), "useFactory"),
909
+ inject: [
910
+ ...quotaProviders
911
+ ]
912
+ };
913
+ }
914
+ __name(quotaUsageSnapshotProvider, "quotaUsageSnapshotProvider");
915
+
916
+ // src/platform/compose/tenant-billing.ts
917
+ function composeTenantBilling(ctx) {
918
+ const config = ctx.options.tenantBilling;
919
+ if (!config) return [];
920
+ const tenantSlice = ctx.persistence?.tenantBilling;
921
+ const { authGuards, subscriptionUsagePort, usageSnapshotPort, subscriptionWritePort, imports: tenantImports, extraProviders, extraExports, ...tenantOptions } = config;
922
+ const quotaProviders = ctx.options.quotaProviders ?? [];
923
+ ctx.shared.quotaProvidersHostedByTenantBilling = quotaProviders.length > 0;
924
+ ctx.shared.authGuards = normalizeTenantAuthGuards(authGuards);
925
+ ctx.shared.subscriptionUsagePort = subscriptionUsagePort ?? tenantSlice?.subscriptionUsagePort;
926
+ return [
927
+ TenantBillingModule.forRoot({
928
+ ...tenantOptions,
929
+ authGuards: ctx.shared.authGuards,
930
+ subscriptionUsagePort: ctx.shared.subscriptionUsagePort,
931
+ usageSnapshotPort: usageSnapshotPort ?? tenantSlice?.usageSnapshotPort ?? quotaUsageSnapshotProvider(quotaProviders),
932
+ subscriptionWritePort: subscriptionWritePort ?? tenantSlice?.subscriptionWritePort,
933
+ imports: tenantImports ?? ctx.options.imports,
934
+ extraProviders: [
935
+ ...quotaProviders,
936
+ ...extraProviders ?? []
937
+ ],
938
+ extraExports: [
939
+ ...quotaProviders,
940
+ ...extraExports ?? []
941
+ ]
942
+ })
943
+ ];
944
+ }
945
+ __name(composeTenantBilling, "composeTenantBilling");
946
+ function composeSubscriptionBundles(ctx) {
947
+ if (!ctx.options.subscriptionBundles) return [];
948
+ const config = optionsOf2(ctx.options.subscriptionBundles);
949
+ const { controller, imports: bundleImports, extraProviders, ...bundleOptions } = config;
950
+ return [
951
+ SubscriptionBundleModule.forRoot({
952
+ ...bundleOptions,
953
+ subscriptionBundleRepository: ctx.persistence?.entitlement?.subscriptionBundleRepository,
954
+ bundleRepository: ctx.persistence?.catalog?.bundleRepository,
955
+ controller: controller === false ? void 0 : {
956
+ ...controller ?? {},
957
+ authGuards: ctx.shared.authGuards,
958
+ subscriptionUsagePort: ctx.shared.subscriptionUsagePort
959
+ },
960
+ // Falls back to the tenant-billing imports before the shared ones,
961
+ // because the two controllers usually resolve the same app
962
+ // providers and an app that scoped them for one meant both.
963
+ imports: bundleImports ?? optionsOf2(ctx.options.tenantBilling).imports ?? ctx.options.imports,
964
+ extraProviders
965
+ })
966
+ ];
967
+ }
968
+ __name(composeSubscriptionBundles, "composeSubscriptionBundles");
969
+
970
+ // src/platform/compose/index.ts
971
+ var FEATURE_COMPOSERS = [
972
+ composeEntitlement,
973
+ composeCatalog,
974
+ composeAdminResources,
975
+ composePromoCodes,
976
+ // Before the bundles — see the note above.
977
+ composeTenantBilling,
978
+ composeSubscriptionBundles,
979
+ composeSetup,
980
+ composeAdminStats,
981
+ composeCheckoutOffer,
982
+ composeSubscriptionContract
983
+ ];
984
+ function composeFeatures(ctx) {
985
+ return FEATURE_COMPOSERS.flatMap((compose) => compose(ctx));
986
+ }
987
+ __name(composeFeatures, "composeFeatures");
988
+
989
+ // src/platform/compose/module-exports.ts
990
+ var ALWAYS = [
991
+ PlanCatalogModule,
992
+ DiscoveryModule,
993
+ AdminModule,
994
+ // ADMIN_MANIFEST_CONFIG travels transitively: re-exporting an imported
995
+ // module's token directly is an UnknownExportException at boot — the module
996
+ // export here already carries it.
997
+ AdminManifestModule
998
+ ];
999
+ function optionalExports({ options, requiresFullEntitlement: requiresFullEntitlement2 }) {
1000
+ return [
1001
+ [
1002
+ requiresFullEntitlement2,
1003
+ EntitlementModule
1004
+ ],
1005
+ [
1006
+ options.catalog,
1007
+ CatalogModule
1008
+ ],
1009
+ [
1010
+ options.tenantBilling,
1011
+ TenantBillingModule
1012
+ ],
1013
+ [
1014
+ options.subscriptionBundles,
1015
+ SubscriptionBundleModule
1016
+ ],
1017
+ [
1018
+ options.adminResources,
1019
+ AdminResourcesModule
1020
+ ],
1021
+ [
1022
+ options.promoCodes,
1023
+ PromoCodesModule
1024
+ ],
1025
+ [
1026
+ options.setup,
1027
+ SetupModule
1028
+ ],
1029
+ [
1030
+ options.adminStats,
1031
+ AdminStatsModule
1032
+ ],
1033
+ [
1034
+ options.checkoutOffer,
1035
+ CheckoutOfferModule
1036
+ ],
1037
+ [
1038
+ options.subscriptionContract,
1039
+ SubscriptionContractModule
1040
+ ]
1041
+ ];
1042
+ }
1043
+ __name(optionalExports, "optionalExports");
1044
+ var MOUNTED_BUT_NOT_EXPORTED = [
1045
+ "PublicCatalogModule"
1046
+ ];
1047
+ function composeModuleExports(ctx, ownExports) {
1048
+ return [
1049
+ ...ALWAYS,
1050
+ ...optionalExports(ctx).filter(([enabled]) => Boolean(enabled)).map(([, module]) => module),
1051
+ ...ownExports
1052
+ ];
1053
+ }
1054
+ __name(composeModuleExports, "composeModuleExports");
1055
+
1056
+ // src/platform/plan-resolver.port.ts
1057
+ var PLAN_RESOLVER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlanResolverPort");
1058
+ var StaticPlanResolver = class {
748
1059
  static {
749
- __name(this, "SubscriptionPlanResolver");
1060
+ __name(this, "StaticPlanResolver");
750
1061
  }
751
- subscriptions;
752
- activeStatuses;
753
- constructor(subscriptions, activeStatuses = DEFAULT_ACTIVE_STATUSES) {
754
- this.subscriptions = subscriptions;
755
- this.activeStatuses = activeStatuses;
1062
+ planId;
1063
+ constructor(planId) {
1064
+ this.planId = planId;
756
1065
  }
757
- async getPlanIdForTenant(tenantId) {
758
- const subscription = await this.subscriptions.findByTenantId(tenantId);
759
- if (!subscription || !this.activeStatuses.has(subscription.status)) return null;
760
- return subscription.plan;
1066
+ async getPlanIdForTenant() {
1067
+ return this.planId;
1068
+ }
1069
+ };
1070
+
1071
+ // 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) {
1074
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1075
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1076
+ 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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1078
+ }
1079
+ __name(_ts_decorate5, "_ts_decorate");
1080
+ function _ts_metadata4(k, v) {
1081
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1082
+ }
1083
+ __name(_ts_metadata4, "_ts_metadata");
1084
+ function _ts_param4(paramIndex, decorator) {
1085
+ return function(target, key) {
1086
+ decorator(target, key, paramIndex);
1087
+ };
1088
+ }
1089
+ __name(_ts_param4, "_ts_param");
1090
+ var StaticEntitlementService = class {
1091
+ static {
1092
+ __name(this, "StaticEntitlementService");
1093
+ }
1094
+ catalog;
1095
+ resolver;
1096
+ constructor(catalog, resolver) {
1097
+ this.catalog = catalog;
1098
+ this.resolver = resolver;
1099
+ }
1100
+ /**
1101
+ * Returns the effective features + quotas of a tenant from the
1102
+ * plan catalog. When the plan is missing: an empty snapshot (no feature,
1103
+ * all quotas at 0).
1104
+ */
1105
+ async snapshot(tenantId) {
1106
+ const planId = await this.resolver.getPlanIdForTenant(tenantId);
1107
+ if (!planId) {
1108
+ return {
1109
+ planId: null,
1110
+ features: [],
1111
+ quotas: {}
1112
+ };
1113
+ }
1114
+ const plan = this.findPlan(planId);
1115
+ if (!plan) {
1116
+ return {
1117
+ planId,
1118
+ features: [],
1119
+ quotas: {}
1120
+ };
1121
+ }
1122
+ return {
1123
+ planId,
1124
+ features: plan.features ?? [],
1125
+ quotas: plan.quotas ?? {}
1126
+ };
1127
+ }
1128
+ /** Convenience helper for tests / synchronous checks in the UI manifest. */
1129
+ async hasFeature(tenantId, featureKey) {
1130
+ const snap = await this.snapshot(tenantId);
1131
+ return snap.features.includes(featureKey);
1132
+ }
1133
+ async quotaLimit(tenantId, quotaKey) {
1134
+ const snap = await this.snapshot(tenantId);
1135
+ const value = snap.quotas[quotaKey];
1136
+ return value === void 0 ? null : value;
1137
+ }
1138
+ findPlan(planId) {
1139
+ return (this.catalog.plans ?? []).find((p) => p.id === planId);
761
1140
  }
762
1141
  };
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,
1149
+ typeof PlanResolverPort === "undefined" ? Object : PlanResolverPort
1150
+ ])
1151
+ ], StaticEntitlementService);
763
1152
 
764
1153
  // src/platform/tenant-manifest.service.ts
765
- import { Injectable as Injectable6 } from "@nestjs/common";
766
- function _ts_decorate8(decorators, target, key, desc) {
1154
+ import { Injectable as Injectable4 } from "@nestjs/common";
1155
+ function _ts_decorate6(decorators, target, key, desc) {
767
1156
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
768
1157
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
769
1158
  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;
770
1159
  return c > 3 && r && Object.defineProperty(target, key, r), r;
771
1160
  }
772
- __name(_ts_decorate8, "_ts_decorate");
773
- function _ts_metadata7(k, v) {
1161
+ __name(_ts_decorate6, "_ts_decorate");
1162
+ function _ts_metadata5(k, v) {
774
1163
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
775
1164
  }
776
- __name(_ts_metadata7, "_ts_metadata");
1165
+ __name(_ts_metadata5, "_ts_metadata");
777
1166
  var TenantManifestService = class {
778
1167
  static {
779
1168
  __name(this, "TenantManifestService");
@@ -819,10 +1208,10 @@ var TenantManifestService = class {
819
1208
  };
820
1209
  }
821
1210
  };
822
- TenantManifestService = _ts_decorate8([
823
- Injectable6(),
824
- _ts_metadata7("design:type", Function),
825
- _ts_metadata7("design:paramtypes", [
1211
+ TenantManifestService = _ts_decorate6([
1212
+ Injectable4(),
1213
+ _ts_metadata5("design:type", Function),
1214
+ _ts_metadata5("design:paramtypes", [
826
1215
  typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService
827
1216
  ])
828
1217
  ], TenantManifestService);
@@ -836,7 +1225,146 @@ function isVisible(item, features) {
836
1225
  __name(isVisible, "isVisible");
837
1226
 
838
1227
  // src/platform/tenant-manifest.controller.ts
839
- import { Controller as Controller2, Get as Get2, Inject as Inject6, Req, UseGuards } from "@nestjs/common";
1228
+ import { Controller as Controller2, Get as Get2, Inject as Inject4, Req, UseGuards } from "@nestjs/common";
1229
+ function _ts_decorate7(decorators, target, key, desc) {
1230
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1231
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1232
+ 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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1234
+ }
1235
+ __name(_ts_decorate7, "_ts_decorate");
1236
+ function _ts_metadata6(k, v) {
1237
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1238
+ }
1239
+ __name(_ts_metadata6, "_ts_metadata");
1240
+ function _ts_param5(paramIndex, decorator) {
1241
+ return function(target, key) {
1242
+ decorator(target, key, paramIndex);
1243
+ };
1244
+ }
1245
+ __name(_ts_param5, "_ts_param");
1246
+ function buildTenantManifestController(options) {
1247
+ const path = options.path ?? "tenant/manifest";
1248
+ let GeneratedTenantManifestController = class GeneratedTenantManifestController {
1249
+ static {
1250
+ __name(this, "GeneratedTenantManifestController");
1251
+ }
1252
+ svc;
1253
+ constructor(svc) {
1254
+ this.svc = svc;
1255
+ }
1256
+ async get(req) {
1257
+ const tenantId = req.user?.tenantId ?? req.tenantId;
1258
+ if (!tenantId) {
1259
+ throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
1260
+ }
1261
+ return this.svc.getManifest(tenantId);
1262
+ }
1263
+ };
1264
+ _ts_decorate7([
1265
+ Get2(),
1266
+ _ts_param5(0, Req()),
1267
+ _ts_metadata6("design:type", Function),
1268
+ _ts_metadata6("design:paramtypes", [
1269
+ typeof RequestWithUser === "undefined" ? Object : RequestWithUser
1270
+ ]),
1271
+ _ts_metadata6("design:returntype", Promise)
1272
+ ], 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", [
1279
+ typeof TenantManifestService === "undefined" ? Object : TenantManifestService
1280
+ ])
1281
+ ], GeneratedTenantManifestController);
1282
+ return GeneratedTenantManifestController;
1283
+ }
1284
+ __name(buildTenantManifestController, "buildTenantManifestController");
1285
+
1286
+ // src/platform/enforce-quota.interceptor.ts
1287
+ import { Inject as Inject5, Injectable as Injectable5, Optional as Optional2 } from "@nestjs/common";
1288
+ import { Reflector } from "@nestjs/core";
1289
+ import { from } from "rxjs";
1290
+ import { switchMap } from "rxjs/operators";
1291
+ function _ts_decorate8(decorators, target, key, desc) {
1292
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1293
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1294
+ 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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1296
+ }
1297
+ __name(_ts_decorate8, "_ts_decorate");
1298
+ function _ts_metadata7(k, v) {
1299
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
1300
+ }
1301
+ __name(_ts_metadata7, "_ts_metadata");
1302
+ function _ts_param6(paramIndex, decorator) {
1303
+ return function(target, key) {
1304
+ decorator(target, key, paramIndex);
1305
+ };
1306
+ }
1307
+ __name(_ts_param6, "_ts_param");
1308
+ var QUOTA_PROVIDERS_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/QuotaProviders");
1309
+ var EnforceQuotaInterceptor = class {
1310
+ static {
1311
+ __name(this, "EnforceQuotaInterceptor");
1312
+ }
1313
+ reflector;
1314
+ entitlements;
1315
+ providers;
1316
+ constructor(reflector, entitlements, providers = null) {
1317
+ this.reflector = reflector;
1318
+ this.entitlements = entitlements;
1319
+ this.providers = providers;
1320
+ }
1321
+ intercept(context, next) {
1322
+ const meta = this.reflector.getAllAndOverride(ENFORCE_QUOTA_KEY, [
1323
+ context.getHandler(),
1324
+ context.getClass()
1325
+ ]);
1326
+ if (!meta?.quotaKey) return next.handle();
1327
+ return from(this.check(context, meta)).pipe(switchMap(() => next.handle()));
1328
+ }
1329
+ async check(context, meta) {
1330
+ const request = context.switchToHttp().getRequest();
1331
+ const user = request.user;
1332
+ if (!user) return;
1333
+ const role = user.role ?? user.platformRole;
1334
+ if (role === "SUPER_ADMIN") return;
1335
+ const tenantId = request.tenantId ?? user.tenantId;
1336
+ if (!tenantId) return;
1337
+ const provider = (this.providers ?? []).find((p) => p.key === meta.quotaKey);
1338
+ if (!provider) {
1339
+ return;
1340
+ }
1341
+ const limit = await this.entitlements.quotaLimit(tenantId, meta.quotaKey);
1342
+ if (limit === null || limit === -1) return;
1343
+ const current = await provider.count(tenantId);
1344
+ const delta = meta.incrementBy ?? 1;
1345
+ if (current + delta > limit) {
1346
+ throw new LimitExceededError(meta.quotaKey, limit, current);
1347
+ }
1348
+ }
1349
+ };
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", [
1358
+ typeof Reflector === "undefined" ? Object : Reflector,
1359
+ typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
1360
+ Object
1361
+ ])
1362
+ ], EnforceQuotaInterceptor);
1363
+
1364
+ // src/platform/static-feature.guard.ts
1365
+ import { ForbiddenException as ForbiddenException2, Inject as Inject6, Injectable as Injectable6, Optional as Optional3 } from "@nestjs/common";
1366
+ import { Reflector as Reflector2 } from "@nestjs/core";
1367
+ import { AUTH_ERROR_CODES, FEATURE_NOT_LICENSED } from "@saasicat/types";
840
1368
  function _ts_decorate9(decorators, target, key, desc) {
841
1369
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
842
1370
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -854,59 +1382,100 @@ function _ts_param7(paramIndex, decorator) {
854
1382
  };
855
1383
  }
856
1384
  __name(_ts_param7, "_ts_param");
857
- function buildTenantManifestController(options) {
858
- const path = options.path ?? "tenant/manifest";
859
- let GeneratedTenantManifestController = class GeneratedTenantManifestController {
860
- static {
861
- __name(this, "GeneratedTenantManifestController");
1385
+ var STATIC_FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StaticFeatureGuardConfig");
1386
+ var StaticFeatureGuard = class {
1387
+ static {
1388
+ __name(this, "StaticFeatureGuard");
1389
+ }
1390
+ reflector;
1391
+ entitlements;
1392
+ config;
1393
+ /** Lets `EnforcementChainCheck` recognise this guard across CJS entries. */
1394
+ static [FEATURE_GUARD_MARKER] = true;
1395
+ constructor(reflector, entitlements, config = null) {
1396
+ this.reflector = reflector;
1397
+ this.entitlements = entitlements;
1398
+ this.config = config;
1399
+ }
1400
+ async canActivate(context) {
1401
+ const required = this.reflector.getAllAndOverride(REQUIRE_FEATURE_KEY, [
1402
+ context.getHandler(),
1403
+ context.getClass()
1404
+ ]);
1405
+ if (!required || required.length === 0) return true;
1406
+ const request = context.switchToHttp().getRequest();
1407
+ const user = request.user;
1408
+ if (!user) {
1409
+ throw new ForbiddenException2({
1410
+ code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
1411
+ message: "Not authenticated"
1412
+ });
862
1413
  }
863
- svc;
864
- constructor(svc) {
865
- this.svc = svc;
1414
+ const role = this.config?.userRoleResolver ? this.config.userRoleResolver(user) : user.role ?? user.platformRole;
1415
+ if (role === "SUPER_ADMIN") return true;
1416
+ const tenantId = this.config?.tenantIdResolver ? this.config.tenantIdResolver(request) : request.tenantId ?? user.tenantId;
1417
+ if (!tenantId) {
1418
+ throw new ForbiddenException2({
1419
+ code: AUTH_ERROR_CODES.NO_TENANT_ASSIGNED,
1420
+ message: "No tenant assigned"
1421
+ });
866
1422
  }
867
- async get(req) {
868
- const tenantId = req.user?.tenantId ?? req.tenantId;
869
- if (!tenantId) {
870
- throw new Error("No tenant on the request \u2014 TenantManifest requires user.tenantId.");
871
- }
872
- return this.svc.getManifest(tenantId);
1423
+ const snap = await this.entitlements.snapshot(tenantId);
1424
+ const allowed = required.some((f) => snap.features.includes(f));
1425
+ if (!allowed) {
1426
+ const body = {
1427
+ code: FEATURE_NOT_LICENSED,
1428
+ featureKey: required[0],
1429
+ featureKeys: required,
1430
+ offers: [],
1431
+ message: `Feature ${required.join(" / ")} is not included in the current plan.`
1432
+ };
1433
+ throw new ForbiddenException2(body);
873
1434
  }
874
- };
875
- _ts_decorate9([
876
- Get2(),
877
- _ts_param7(0, Req()),
878
- _ts_metadata8("design:type", Function),
879
- _ts_metadata8("design:paramtypes", [
880
- typeof RequestWithUser === "undefined" ? Object : RequestWithUser
881
- ]),
882
- _ts_metadata8("design:returntype", Promise)
883
- ], GeneratedTenantManifestController.prototype, "get", null);
884
- GeneratedTenantManifestController = _ts_decorate9([
885
- Controller2(path),
886
- UseGuards(...options.guards),
887
- _ts_param7(0, Inject6(TenantManifestService)),
888
- _ts_metadata8("design:type", Function),
889
- _ts_metadata8("design:paramtypes", [
890
- typeof TenantManifestService === "undefined" ? Object : TenantManifestService
891
- ])
892
- ], GeneratedTenantManifestController);
893
- return GeneratedTenantManifestController;
894
- }
895
- __name(buildTenantManifestController, "buildTenantManifestController");
1435
+ return true;
1436
+ }
1437
+ };
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", [
1446
+ typeof Reflector2 === "undefined" ? Object : Reflector2,
1447
+ typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
1448
+ Object
1449
+ ])
1450
+ ], StaticFeatureGuard);
1451
+
1452
+ // src/platform/subscription-plan-resolver.ts
1453
+ var DEFAULT_ACTIVE_STATUSES = /* @__PURE__ */ new Set([
1454
+ "ACTIVE",
1455
+ "TRIAL"
1456
+ ]);
1457
+ var SubscriptionPlanResolver = class {
1458
+ static {
1459
+ __name(this, "SubscriptionPlanResolver");
1460
+ }
1461
+ subscriptions;
1462
+ activeStatuses;
1463
+ constructor(subscriptions, activeStatuses = DEFAULT_ACTIVE_STATUSES) {
1464
+ this.subscriptions = subscriptions;
1465
+ this.activeStatuses = activeStatuses;
1466
+ }
1467
+ async getPlanIdForTenant(tenantId) {
1468
+ const subscription = await this.subscriptions.findByTenantId(tenantId);
1469
+ if (!subscription || !this.activeStatuses.has(subscription.status)) return null;
1470
+ return subscription.plan;
1471
+ }
1472
+ };
896
1473
 
897
1474
  // src/platform/saas-platform.module.ts
898
- import { Logger as Logger2, Module as Module2 } from "@nestjs/common";
899
- import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
900
- import { assertPersistenceCapabilities } from "@saasicat/types";
1475
+ import { Logger, Module as Module2 } from "@nestjs/common";
901
1476
  import { DiscoveryModule as NestDiscoveryModule } from "@nestjs/core";
902
- function _ts_decorate10(decorators, target, key, desc) {
903
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
904
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
905
- 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;
906
- return c > 3 && r && Object.defineProperty(target, key, r), r;
907
- }
908
- __name(_ts_decorate10, "_ts_decorate");
909
- var PLATFORM_LOGGER = new Logger2("SaaSiCat");
1477
+
1478
+ // src/platform/compose/manifest.ts
910
1479
  var PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlatformSubscriptionRepository");
911
1480
  var STANDARD_MANIFEST_REGISTRATION_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StandardManifestRegistration");
912
1481
  function buildStandardManifestContribution(catalog, adminResources, promoCodes) {
@@ -961,21 +1530,6 @@ function buildStandardManifestContribution(catalog, adminResources, promoCodes)
961
1530
  };
962
1531
  }
963
1532
  __name(buildStandardManifestContribution, "buildStandardManifestContribution");
964
- function normalizeTenantAuthGuards(guards) {
965
- if (!Array.isArray(guards)) return guards;
966
- return {
967
- useFactory: /* @__PURE__ */ __name((...instances) => instances, "useFactory"),
968
- inject: guards
969
- };
970
- }
971
- __name(normalizeTenantAuthGuards, "normalizeTenantAuthGuards");
972
- function quotaUsageSnapshotProvider(quotaProviders) {
973
- return {
974
- useFactory: /* @__PURE__ */ __name((...providers) => new QuotaProvidersUsageSnapshot(providers), "useFactory"),
975
- inject: quotaProviders
976
- };
977
- }
978
- __name(quotaUsageSnapshotProvider, "quotaUsageSnapshotProvider");
979
1533
  function buildMinimalManifestConfig() {
980
1534
  return {
981
1535
  useFactory: /* @__PURE__ */ __name((catalog) => ({
@@ -1008,6 +1562,189 @@ function buildMinimalManifestConfig() {
1008
1562
  };
1009
1563
  }
1010
1564
  __name(buildMinimalManifestConfig, "buildMinimalManifestConfig");
1565
+
1566
+ // src/platform/compose/base.ts
1567
+ 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"
1572
+ };
1573
+ }
1574
+ __name(resolveAppInfo, "resolveAppInfo");
1575
+ function composePlanCatalog(options, sink) {
1576
+ if (options.planCatalog) {
1577
+ return PlanCatalogModule.forRootWithCatalog(options.planCatalog, {
1578
+ global: true
1579
+ });
1580
+ }
1581
+ const dbCatalog = options.dbCatalog;
1582
+ return PlanCatalogModule.forRoot({
1583
+ projectKey: dbCatalog.projectKey,
1584
+ app: dbCatalog.app,
1585
+ currency: dbCatalog.currency,
1586
+ vatRate: dbCatalog.vatRate,
1587
+ marketing: dbCatalog.marketing,
1588
+ sink,
1589
+ imports: options.imports
1590
+ });
1591
+ }
1592
+ __name(composePlanCatalog, "composePlanCatalog");
1593
+ function composeBaseModules(options, appInfo, ports) {
1594
+ return [
1595
+ DiscoveryModule.forRoot({
1596
+ app: appInfo,
1597
+ controller: {
1598
+ guards: options.controller.guards
1599
+ },
1600
+ imports: options.imports,
1601
+ snapshotPath: options.discoverySnapshotPath === void 0 ? DEFAULT_SNAPSHOT_PATH : options.discoverySnapshotPath
1602
+ }),
1603
+ AdminModule.forRoot({
1604
+ ...ports,
1605
+ imports: options.imports,
1606
+ global: true
1607
+ }),
1608
+ AdminManifestModule.forRoot({
1609
+ config: options.adminManifestConfig ?? buildMinimalManifestConfig(),
1610
+ extraProviders: options.adminManifestExtraProviders,
1611
+ includeManifestController: options.includeManifestController,
1612
+ guards: options.controller.guards,
1613
+ reloadGuards: options.reloadGuards,
1614
+ imports: options.imports,
1615
+ // Global like AdminModule above: apps register their manifest
1616
+ // contribution by injecting AdminManifestService into one of their
1617
+ // own modules (handbook §6.6). Re-exporting the module from here
1618
+ // does not make that injection resolvable.
1619
+ global: true
1620
+ })
1621
+ ];
1622
+ }
1623
+ __name(composeBaseModules, "composeBaseModules");
1624
+
1625
+ // src/platform/compose/tenant-manifest.ts
1626
+ var NONE = {
1627
+ providers: [],
1628
+ exports: [],
1629
+ controllers: []
1630
+ };
1631
+ function composeTenantManifest(ctx) {
1632
+ const { options } = ctx;
1633
+ if (!options.tenantManifest) return NONE;
1634
+ const tenantAuthGuards = optionsOf2(options.tenantBilling).authGuards;
1635
+ return {
1636
+ providers: [
1637
+ TenantManifestService
1638
+ ],
1639
+ exports: [
1640
+ TenantManifestService
1641
+ ],
1642
+ controllers: [
1643
+ buildTenantManifestController(options.tenantManifest === true ? {
1644
+ guards: Array.isArray(tenantAuthGuards) ? tenantAuthGuards : options.controller.guards
1645
+ } : options.tenantManifest)
1646
+ ]
1647
+ };
1648
+ }
1649
+ __name(composeTenantManifest, "composeTenantManifest");
1650
+
1651
+ // src/platform/compose/enforcement-runtime.ts
1652
+ import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
1653
+ function resolvePlanResolution(ctx) {
1654
+ const explicit = !!ctx.adapters.planResolver;
1655
+ const viaSubscriptions = Boolean(ctx.options.tenantBilling && ctx.adapters.subscriptionRepository);
1656
+ const fallback = !!ctx.options.defaultPlanId;
1657
+ return {
1658
+ explicit,
1659
+ viaSubscriptions,
1660
+ fallback,
1661
+ available: explicit || viaSubscriptions || fallback
1662
+ };
1663
+ }
1664
+ __name(resolvePlanResolution, "resolvePlanResolution");
1665
+ function composeEnforcementRuntime(ctx, resolution) {
1666
+ if (!resolution.available) return {
1667
+ providers: [],
1668
+ exports: []
1669
+ };
1670
+ const { options, adapters, shared } = ctx;
1671
+ const providers = [];
1672
+ if (resolution.viaSubscriptions && !resolution.explicit) {
1673
+ providers.push(asProvider(PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN, adapters.subscriptionRepository));
1674
+ }
1675
+ providers.push(
1676
+ planResolverProvider(ctx, resolution),
1677
+ StaticEntitlementService,
1678
+ StaticFeatureGuard,
1679
+ EnforceQuotaInterceptor,
1680
+ ...shared.quotaProvidersHostedByTenantBilling ? [] : options.quotaProviders ?? [],
1681
+ {
1682
+ provide: QUOTA_PROVIDERS_TOKEN,
1683
+ useFactory: /* @__PURE__ */ __name((...quotas) => quotas, "useFactory"),
1684
+ inject: options.quotaProviders ?? []
1685
+ },
1686
+ ...options.globalFeatureGuard === false ? [] : [
1687
+ {
1688
+ provide: APP_GUARD,
1689
+ useExisting: StaticFeatureGuard
1690
+ }
1691
+ ],
1692
+ // The interceptor stays global either way: interceptors run after all
1693
+ // guards, so it sees the authenticated request even when the app binds
1694
+ // its own guard chain.
1695
+ {
1696
+ provide: APP_INTERCEPTOR,
1697
+ useExisting: EnforceQuotaInterceptor
1698
+ }
1699
+ );
1700
+ return {
1701
+ providers,
1702
+ exports: [
1703
+ PLAN_RESOLVER_PORT_TOKEN,
1704
+ StaticEntitlementService,
1705
+ StaticFeatureGuard,
1706
+ EnforceQuotaInterceptor,
1707
+ QUOTA_PROVIDERS_TOKEN
1708
+ ]
1709
+ };
1710
+ }
1711
+ __name(composeEnforcementRuntime, "composeEnforcementRuntime");
1712
+ function planResolverProvider(ctx, resolution) {
1713
+ if (resolution.explicit) {
1714
+ return asProvider(PLAN_RESOLVER_PORT_TOKEN, ctx.adapters.planResolver);
1715
+ }
1716
+ if (resolution.viaSubscriptions) {
1717
+ return {
1718
+ provide: PLAN_RESOLVER_PORT_TOKEN,
1719
+ useFactory: /* @__PURE__ */ __name((subscriptions) => new SubscriptionPlanResolver(subscriptions), "useFactory"),
1720
+ inject: [
1721
+ PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN
1722
+ ]
1723
+ };
1724
+ }
1725
+ return {
1726
+ provide: PLAN_RESOLVER_PORT_TOKEN,
1727
+ useValue: new StaticPlanResolver(ctx.options.defaultPlanId)
1728
+ };
1729
+ }
1730
+ __name(planResolverProvider, "planResolverProvider");
1731
+
1732
+ // src/platform/saas-platform.module.ts
1733
+ function _ts_decorate10(decorators, target, key, desc) {
1734
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1735
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
1736
+ 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
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1738
+ }
1739
+ __name(_ts_decorate10, "_ts_decorate");
1740
+ var PLATFORM_LOGGER = new Logger("SaaSiCat");
1741
+ function enforcementChainState(state) {
1742
+ return {
1743
+ provide: ENFORCEMENT_CHAIN_STATE_TOKEN,
1744
+ useValue: state
1745
+ };
1746
+ }
1747
+ __name(enforcementChainState, "enforcementChainState");
1011
1748
  var SaasPlatformModule = class _SaasPlatformModule {
1012
1749
  static {
1013
1750
  __name(this, "SaasPlatformModule");
@@ -1020,11 +1757,7 @@ var SaasPlatformModule = class _SaasPlatformModule {
1020
1757
  const subscriptionBundlesConfig = options.subscriptionBundles || null;
1021
1758
  const adminResourcesConfig = options.adminResources || null;
1022
1759
  const promoCodesConfig = options.promoCodes || null;
1023
- const setupConfig = options.setup || null;
1024
- const adminStatsConfig = options.adminStats || null;
1025
- const checkoutOfferConfig = options.checkoutOffer || null;
1026
- const subscriptionContractConfig = options.subscriptionContract || null;
1027
- const requiresFullEntitlement = Boolean(options.entitlement || tenantBillingConfig || subscriptionBundlesConfig);
1760
+ const requiresFullEntitlement2 = Boolean(options.entitlement || tenantBillingConfig || subscriptionBundlesConfig);
1028
1761
  const adapters = {
1029
1762
  mfa: explicit.mfa ?? persistence?.core.mfa,
1030
1763
  audit: explicit.audit ?? persistence?.core.audit,
@@ -1035,296 +1768,36 @@ var SaasPlatformModule = class _SaasPlatformModule {
1035
1768
  planVersionRepository: explicit.planVersionRepository ?? persistence?.entitlement?.planVersionRepository,
1036
1769
  transactionRunner: explicit.transactionRunner ?? persistence?.core.transactionRunner
1037
1770
  };
1038
- const missingCore = [
1039
- "mfa",
1040
- "audit",
1041
- "rlsBypass"
1042
- ].filter((key) => adapters[key] === void 0);
1043
- if (missingCore.length) {
1044
- throw new Error(`SaasPlatformModule.forRoot: adapters missing (provide them via \`adapters\` or a \`persistence\` bundle): ${missingCore.join(", ")}`);
1045
- }
1771
+ assertConfiguration({
1772
+ options,
1773
+ adapters
1774
+ });
1046
1775
  const mfaPort = adapters.mfa;
1047
1776
  const auditPort = adapters.audit;
1048
1777
  const rlsBypassPort = adapters.rlsBypass;
1049
- if (!options.planCatalog && (!adapters.planCatalogReadSink || !options.dbCatalog)) {
1050
- throw new Error("SaasPlatformModule.forRoot: either set `planCatalog` (quickstart YAML path) or, for DB hydration, BOTH a planCatalogReadSink (`adapters`/`persistence`) AND `dbCatalog` ({ projectKey, currency, vatRate }).");
1051
- }
1052
- if (catalogConfig && !persistence?.catalog) {
1053
- throw new Error("SaasPlatformModule.forRoot: catalog is enabled, but `persistence.catalog` is missing. Use a complete persistence bundle or wire CatalogModule directly.");
1054
- }
1055
- if (adminResourcesConfig && adminResourcesConfig !== true && !adminResourcesConfig.resources && !persistence?.adminResources) {
1056
- throw new Error("SaasPlatformModule.forRoot: adminResources is enabled, but `persistence.adminResources` is missing.");
1057
- }
1058
- if (promoCodesConfig && (!persistence?.promo || !adapters.transactionRunner)) {
1059
- throw new Error("SaasPlatformModule.forRoot: promoCodes requires `persistence.promo` and a transactionRunner.");
1060
- }
1061
- if (setupConfig && !((setupConfig === true ? void 0 : setupConfig.provisioningPort) ?? persistence?.core.superAdminProvisioning)) {
1062
- throw new Error("SaasPlatformModule.forRoot: setup is enabled, but no provisioningPort is available (set `setup.provisioningPort` or `persistence.core.superAdminProvisioning`).");
1063
- }
1064
- if (adminStatsConfig && !adminStatsConfig.auditStatsPort && !persistence?.core.auditStats) {
1065
- throw new Error("SaasPlatformModule.forRoot: adminStats is enabled, but no auditStatsPort is available (set `adminStats.auditStatsPort` or `persistence.core.auditStats`).");
1066
- }
1067
- if (subscriptionContractConfig) {
1068
- const explicitRepository = subscriptionContractConfig === true ? void 0 : subscriptionContractConfig.subscriptionContractRepository;
1069
- if (!explicitRepository && !persistence?.entitlement?.subscriptionContractRepository) {
1070
- throw new Error("SaasPlatformModule.forRoot: subscriptionContract is enabled, but no repository is available (set `subscriptionContract.subscriptionContractRepository` or use a compatible persistence bundle).");
1071
- }
1072
- }
1073
- if (promoCodesConfig && promoCodesConfig !== true && promoCodesConfig.includePublicController !== false && !promoCodesConfig.firstTimeCustomerCheck) {
1074
- throw new Error("SaasPlatformModule.forRoot: public promo preview requires `firstTimeCustomerCheck`.");
1075
- }
1076
- if (adminResourcesConfig === true && !persistence?.adminResources) {
1077
- throw new Error("SaasPlatformModule.forRoot: adminResources is enabled, but `persistence.adminResources` is missing.");
1078
- }
1079
- if (tenantBillingConfig && !persistence?.tenantBilling) {
1080
- const missing = [
1081
- !tenantBillingConfig.subscriptionUsagePort && "subscriptionUsagePort",
1082
- !tenantBillingConfig.subscriptionWritePort && "subscriptionWritePort"
1083
- ].filter(Boolean);
1084
- if (missing.length) {
1085
- throw new Error(`SaasPlatformModule.forRoot: tenantBilling is enabled, but adapters are missing: ${missing.join(", ")}`);
1086
- }
1087
- }
1088
- if (subscriptionBundlesConfig && !tenantBillingConfig) {
1089
- throw new Error("SaasPlatformModule.forRoot: subscriptionBundles requires tenantBilling so auth and subscription usage can be shared.");
1090
- }
1091
- if (subscriptionBundlesConfig) {
1092
- const missing = [];
1093
- if (!persistence?.entitlement?.subscriptionBundleRepository) {
1094
- missing.push("subscriptionBundleRepository");
1095
- }
1096
- if (!persistence?.catalog?.bundleRepository) {
1097
- missing.push("bundleRepository");
1098
- }
1099
- if (missing.length) {
1100
- throw new Error(`SaasPlatformModule.forRoot: subscriptionBundles is enabled, but persistence adapters are missing: ${missing.join(", ")}`);
1101
- }
1102
- }
1103
- if (requiresFullEntitlement) {
1104
- const missing = [];
1105
- if (!adapters.subscriptionRepository) missing.push("subscriptionRepository");
1106
- if (!adapters.planVersionRepository) missing.push("planVersionRepository");
1107
- if (!adapters.transactionRunner) missing.push("transactionRunner");
1108
- if (missing.length) {
1109
- throw new Error(`SaasPlatformModule.forRoot: entitlement active or required by the standard stack, but adapters are missing: ${missing.join(", ")}`);
1110
- }
1111
- if (persistence) {
1112
- assertPersistenceCapabilities(persistence.capabilities, {
1113
- transactions: true,
1114
- pessimisticLocking: true
1115
- }, "SaasPlatformModule entitlement (transactional enforceLimit)");
1116
- }
1117
- }
1118
- const dbCatalog = options.dbCatalog;
1119
- const planCatalogModule = options.planCatalog ? PlanCatalogModule.forRootWithCatalog(options.planCatalog, {
1120
- global: true
1121
- }) : PlanCatalogModule.forRoot({
1122
- projectKey: dbCatalog.projectKey,
1123
- app: dbCatalog.app,
1124
- currency: dbCatalog.currency,
1125
- vatRate: dbCatalog.vatRate,
1126
- marketing: dbCatalog.marketing,
1127
- sink: adapters.planCatalogReadSink,
1128
- imports: options.imports
1129
- });
1130
- const appInfo = options.app ?? {
1131
- key: options.planCatalog?.projectKey ?? options.dbCatalog?.projectKey ?? "app",
1132
- version: options.planCatalog?.app?.version ?? options.dbCatalog?.app?.version ?? "0.0.0"
1133
- };
1778
+ const appInfo = resolveAppInfo(options);
1134
1779
  const imports = [
1135
1780
  // Make injected client/auth providers visible to the high-level
1136
1781
  // module's own quota providers and automatic plan resolver too.
1137
1782
  ...options.imports ?? [],
1138
- planCatalogModule,
1139
- DiscoveryModule.forRoot({
1140
- app: appInfo,
1141
- controller: {
1142
- guards: options.controller.guards
1143
- },
1144
- imports: options.imports,
1145
- snapshotPath: options.discoverySnapshotPath === void 0 ? "var/discovery-snapshot.json" : options.discoverySnapshotPath
1146
- }),
1147
- AdminModule.forRoot({
1783
+ composePlanCatalog(options, adapters.planCatalogReadSink),
1784
+ ...composeBaseModules(options, appInfo, {
1148
1785
  mfaPort,
1149
1786
  auditPort,
1150
- rlsBypassPort,
1151
- imports: options.imports,
1152
- global: true
1153
- }),
1154
- AdminManifestModule.forRoot({
1155
- config: options.adminManifestConfig ?? buildMinimalManifestConfig(),
1156
- extraProviders: options.adminManifestExtraProviders,
1157
- includeManifestController: options.includeManifestController,
1158
- guards: options.controller.guards,
1159
- reloadGuards: options.reloadGuards,
1160
- imports: options.imports,
1161
- // Global like AdminModule above: apps register their manifest
1162
- // contribution by injecting AdminManifestService into one of
1163
- // their own modules (handbook §6.6). Re-exporting the module
1164
- // from here does not make that injection resolvable.
1165
- global: true
1787
+ rlsBypassPort
1166
1788
  })
1167
1789
  ];
1168
- if (requiresFullEntitlement) {
1169
- imports.push(EntitlementModule.forRoot({
1170
- subscriptionRepository: adapters.subscriptionRepository,
1171
- planVersionRepository: adapters.planVersionRepository,
1172
- transactionRunner: adapters.transactionRunner,
1173
- resolutionConfig: options.entitlement ? options.entitlement.resolutionConfig : void 0,
1174
- subscriptionContractRepository: persistence?.entitlement?.subscriptionContractRepository,
1175
- subscriptionBundleRepository: persistence?.entitlement?.subscriptionBundleRepository,
1176
- bundleRepository: persistence?.entitlement?.bundleRepository,
1177
- imports: options.imports,
1178
- global: Boolean(tenantBillingConfig || subscriptionBundlesConfig)
1179
- }));
1180
- }
1181
- if (catalogConfig) {
1182
- const catalog = persistence?.catalog;
1183
- imports.push(CatalogModule.forRoot({
1184
- planRepository: catalog.planRepository,
1185
- bundleRepository: catalog.bundleRepository,
1186
- catalogEntryRepository: catalog.catalogEntryRepository,
1187
- marketingProjectionRepository: catalog.marketingProjectionRepository,
1188
- promotionRepository: catalog.promotionRepository,
1189
- marketingSettingsRepository: catalog.marketingSettingsRepository,
1190
- controller: catalogConfig.adminControllers === false ? void 0 : {
1191
- guards: options.controller.guards
1192
- },
1193
- imports: catalogConfig.imports ?? options.imports,
1194
- extraProviders: catalogConfig.extraProviders,
1195
- strictModeCheckMode: catalogConfig.strictModeCheckMode,
1196
- autoSyncDiscoveryAtBoot: catalogConfig.autoSyncDiscoveryAtBoot,
1197
- marketedOnlyFeatures: catalogConfig.marketedOnlyFeatures,
1198
- featureUiRegistry: catalogConfig.featureUiRegistry,
1199
- publicMarketingCatalog: catalogConfig.publicMarketingCatalog
1200
- }));
1201
- if (catalogConfig.publicCatalog !== false) {
1202
- imports.push(PublicCatalogModule.forRoot({
1203
- featureUiRegistry: catalogConfig.featureUiRegistry,
1204
- projectKey: appInfo.key,
1205
- bundleRepository: catalog.bundleRepository,
1206
- marketingRepository: catalog.marketingProjectionRepository,
1207
- catalogEntryRepository: catalog.catalogEntryRepository,
1208
- imports: catalogConfig.imports ?? options.imports
1209
- }));
1790
+ const composition = {
1791
+ options,
1792
+ adapters,
1793
+ persistence,
1794
+ appInfo,
1795
+ requiresFullEntitlement: requiresFullEntitlement2,
1796
+ shared: {
1797
+ quotaProvidersHostedByTenantBilling: false
1210
1798
  }
1211
- }
1212
- if (adminResourcesConfig) {
1213
- const config = adminResourcesConfig === true ? {} : adminResourcesConfig;
1214
- imports.push(AdminResourcesModule.forRoot({
1215
- resources: config.resources ?? persistence?.adminResources?.resources,
1216
- guards: config.guards ?? [
1217
- ...options.controller.guards,
1218
- SuperAdminGuard
1219
- ],
1220
- imports: config.imports ?? options.imports
1221
- }));
1222
- }
1223
- if (promoCodesConfig) {
1224
- const config = promoCodesConfig === true ? {} : promoCodesConfig;
1225
- const promo = persistence?.promo;
1226
- const { firstTimeCustomerCheck, adminGuards, imports: promoImports, includePublicController, adminController, ...promoOptions } = config;
1227
- imports.push(PromoCodesModule.forRoot({
1228
- ...promoOptions,
1229
- promoCodeRepository: promo.promoCodeRepository,
1230
- redemptionRepository: promo.redemptionRepository,
1231
- validationLogRepository: promo.validationLogRepository,
1232
- subscriptionLookup: promo.subscriptionLookup,
1233
- revenueAggregator: promo.revenueAggregator,
1234
- transactionRunner: adapters.transactionRunner,
1235
- firstTimeCustomerCheck: firstTimeCustomerCheck ?? {
1236
- hasExistingCustomerForEmail: /* @__PURE__ */ __name(async () => false, "hasExistingCustomerForEmail")
1237
- },
1238
- includePublicController: includePublicController ?? false,
1239
- adminController: adminController === false ? false : {
1240
- guards: adminGuards ?? [
1241
- ...options.controller.guards,
1242
- SuperAdminGuard
1243
- ]
1244
- },
1245
- imports: promoImports ?? options.imports
1246
- }));
1247
- }
1248
- let sharedTenantAuthGuards;
1249
- let sharedSubscriptionUsagePort;
1250
- let quotaProvidersHostedByTenantBilling = false;
1251
- if (tenantBillingConfig) {
1252
- const tenantSlice = persistence?.tenantBilling;
1253
- const { authGuards, subscriptionUsagePort, usageSnapshotPort, subscriptionWritePort, imports: tenantImports, extraProviders, extraExports, ...tenantOptions } = tenantBillingConfig;
1254
- const quotaProviders = options.quotaProviders ?? [];
1255
- quotaProvidersHostedByTenantBilling = quotaProviders.length > 0;
1256
- sharedTenantAuthGuards = normalizeTenantAuthGuards(authGuards);
1257
- sharedSubscriptionUsagePort = subscriptionUsagePort ?? tenantSlice?.subscriptionUsagePort;
1258
- const resolvedUsageSnapshotPort = usageSnapshotPort ?? tenantSlice?.usageSnapshotPort ?? quotaUsageSnapshotProvider(options.quotaProviders ?? []);
1259
- imports.push(TenantBillingModule.forRoot({
1260
- ...tenantOptions,
1261
- authGuards: sharedTenantAuthGuards,
1262
- subscriptionUsagePort: sharedSubscriptionUsagePort,
1263
- usageSnapshotPort: resolvedUsageSnapshotPort,
1264
- subscriptionWritePort: subscriptionWritePort ?? tenantSlice?.subscriptionWritePort,
1265
- imports: tenantImports ?? options.imports,
1266
- extraProviders: [
1267
- ...quotaProviders,
1268
- ...extraProviders ?? []
1269
- ],
1270
- extraExports: [
1271
- ...quotaProviders,
1272
- ...extraExports ?? []
1273
- ]
1274
- }));
1275
- }
1276
- if (subscriptionBundlesConfig) {
1277
- const config = subscriptionBundlesConfig === true ? {} : subscriptionBundlesConfig;
1278
- const { controller, imports: bundleImports, extraProviders, ...bundleOptions } = config;
1279
- imports.push(SubscriptionBundleModule.forRoot({
1280
- ...bundleOptions,
1281
- subscriptionBundleRepository: persistence?.entitlement?.subscriptionBundleRepository,
1282
- bundleRepository: persistence?.catalog?.bundleRepository,
1283
- controller: controller === false ? void 0 : {
1284
- ...controller ?? {},
1285
- authGuards: sharedTenantAuthGuards,
1286
- subscriptionUsagePort: sharedSubscriptionUsagePort
1287
- },
1288
- imports: bundleImports ?? tenantBillingConfig?.imports ?? options.imports,
1289
- extraProviders
1290
- }));
1291
- }
1292
- if (setupConfig) {
1293
- const config = setupConfig === true ? {} : setupConfig;
1294
- imports.push(SetupModule.forRoot({
1295
- ...config,
1296
- provisioningPort: config.provisioningPort ?? persistence?.core.superAdminProvisioning,
1297
- imports: config.imports ?? options.imports
1298
- }));
1299
- }
1300
- if (adminStatsConfig) {
1301
- imports.push(AdminStatsModule.forRoot({
1302
- ...adminStatsConfig,
1303
- guards: adminStatsConfig.guards ?? [
1304
- ...options.controller.guards,
1305
- SuperAdminGuard
1306
- ],
1307
- auditStatsPort: adminStatsConfig.auditStatsPort ?? persistence?.core.auditStats,
1308
- imports: adminStatsConfig.imports ?? options.imports
1309
- }));
1310
- }
1311
- if (checkoutOfferConfig) {
1312
- imports.push(CheckoutOfferModule.forRoot({
1313
- ...checkoutOfferConfig,
1314
- bundleRepository: checkoutOfferConfig.bundleRepository ?? persistence?.catalog?.bundleRepository,
1315
- planRepository: checkoutOfferConfig.planRepository ?? persistence?.catalog?.planRepository,
1316
- catalogEntryRepository: checkoutOfferConfig.catalogEntryRepository ?? persistence?.catalog?.catalogEntryRepository,
1317
- imports: checkoutOfferConfig.imports ?? options.imports
1318
- }));
1319
- }
1320
- if (subscriptionContractConfig) {
1321
- const config = subscriptionContractConfig === true ? {} : subscriptionContractConfig;
1322
- imports.push(SubscriptionContractModule.forRoot({
1323
- ...config,
1324
- subscriptionContractRepository: config.subscriptionContractRepository ?? persistence?.entitlement?.subscriptionContractRepository,
1325
- imports: config.imports ?? options.imports
1326
- }));
1327
- }
1799
+ };
1800
+ imports.push(...composeFeatures(composition));
1328
1801
  const lightweightProviders = [];
1329
1802
  const lightweightExports = [];
1330
1803
  if (options.autoManifest !== false) {
@@ -1340,100 +1813,35 @@ var SaasPlatformModule = class _SaasPlatformModule {
1340
1813
  ]
1341
1814
  });
1342
1815
  }
1343
- const hasExplicitResolver = !!adapters.planResolver;
1344
- const hasSubscriptionResolver = Boolean(tenantBillingConfig && adapters.subscriptionRepository);
1345
- const hasResolver = hasExplicitResolver || hasSubscriptionResolver;
1346
- const hasFallback = !!options.defaultPlanId;
1347
- if (hasResolver || hasFallback) {
1348
- if (hasSubscriptionResolver && !hasExplicitResolver) {
1349
- lightweightProviders.push(asProvider(PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN, adapters.subscriptionRepository));
1350
- }
1351
- lightweightProviders.push(hasExplicitResolver ? asProvider(PLAN_RESOLVER_PORT_TOKEN, adapters.planResolver) : hasSubscriptionResolver ? {
1352
- provide: PLAN_RESOLVER_PORT_TOKEN,
1353
- useFactory: /* @__PURE__ */ __name((subscriptions) => new SubscriptionPlanResolver(subscriptions), "useFactory"),
1354
- inject: [
1355
- PLATFORM_SUBSCRIPTION_REPOSITORY_TOKEN
1356
- ]
1357
- } : {
1358
- provide: PLAN_RESOLVER_PORT_TOKEN,
1359
- useValue: new StaticPlanResolver(options.defaultPlanId)
1360
- }, StaticEntitlementService, StaticFeatureGuard, EnforceQuotaInterceptor, ...quotaProvidersHostedByTenantBilling ? [] : options.quotaProviders ?? [], {
1361
- provide: QUOTA_PROVIDERS_TOKEN,
1362
- useFactory: /* @__PURE__ */ __name((...providers) => providers, "useFactory"),
1363
- inject: options.quotaProviders ?? []
1364
- }, ...options.globalFeatureGuard === false ? [] : [
1365
- {
1366
- provide: APP_GUARD,
1367
- useExisting: StaticFeatureGuard
1368
- }
1369
- ], {
1370
- provide: APP_INTERCEPTOR,
1371
- useExisting: EnforceQuotaInterceptor
1372
- });
1373
- lightweightExports.push(PLAN_RESOLVER_PORT_TOKEN, StaticEntitlementService, StaticFeatureGuard, EnforceQuotaInterceptor, QUOTA_PROVIDERS_TOKEN);
1374
- if (options.globalFeatureGuard === false) {
1375
- lightweightProviders.push(FeatureGuardCoverageCheck);
1376
- imports.push(NestDiscoveryModule);
1377
- }
1378
- } else {
1816
+ const resolution = resolvePlanResolution(composition);
1817
+ const runtime = composeEnforcementRuntime(composition, resolution);
1818
+ lightweightProviders.push(...runtime.providers);
1819
+ lightweightExports.push(...runtime.exports);
1820
+ if (!resolution.available && !requiresFullEntitlement2) {
1379
1821
  PLATFORM_LOGGER.warn("No plan resolver configured \u2014 @RequireFeature and @EnforceQuota are INERT. The platform registered neither StaticFeatureGuard nor EnforceQuotaInterceptor, so every annotated route serves unlicensed traffic and every quota is unlimited. Set one of: adapters.planResolver, defaultPlanId, or tenantBilling.");
1380
1822
  }
1381
- const tenantControllers = [];
1382
- if (options.tenantManifest) {
1383
- if (!hasResolver && !hasFallback) {
1384
- throw new Error("SaasPlatformModule.forRoot: tenantManifest requires defaultPlanId, adapters.planResolver or tenantBilling so it can resolve features.");
1385
- }
1386
- lightweightProviders.push(TenantManifestService);
1387
- lightweightExports.push(TenantManifestService);
1388
- tenantControllers.push(buildTenantManifestController(options.tenantManifest === true ? {
1389
- guards: Array.isArray(tenantBillingConfig?.authGuards) ? tenantBillingConfig.authGuards : options.controller.guards
1390
- } : options.tenantManifest));
1823
+ if (options.enforcementChainCheck !== false) {
1824
+ lightweightProviders.push(EnforcementChainCheck, enforcementChainState({
1825
+ featureEnforcementActive: resolution.available || requiresFullEntitlement2,
1826
+ quotaEnforcementActive: resolution.available,
1827
+ globalGuardBound: resolution.available && options.globalFeatureGuard !== false,
1828
+ // Whether the OPTION unbound it, as opposed to this simply
1829
+ // being a path where the platform binds none. The message
1830
+ // names the true cause, and an application that never set
1831
+ // the option is not told to look for it.
1832
+ globalGuardOptedOut: options.globalFeatureGuard === false
1833
+ }));
1391
1834
  }
1835
+ if (options.enforcementChainCheck !== false) imports.push(NestDiscoveryModule);
1836
+ const tenantManifest = composeTenantManifest(composition);
1837
+ lightweightProviders.push(...tenantManifest.providers);
1838
+ lightweightExports.push(...tenantManifest.exports);
1392
1839
  return {
1393
1840
  module: _SaasPlatformModule,
1394
1841
  imports,
1395
1842
  providers: lightweightProviders,
1396
- controllers: tenantControllers,
1397
- exports: [
1398
- PlanCatalogModule,
1399
- DiscoveryModule,
1400
- AdminModule,
1401
- // ADMIN_MANIFEST_CONFIG travels transitively: re-exporting an
1402
- // imported module's token directly is an UnknownExportException
1403
- // at boot — the module export below already carries it.
1404
- AdminManifestModule,
1405
- ...requiresFullEntitlement ? [
1406
- EntitlementModule
1407
- ] : [],
1408
- ...catalogConfig ? [
1409
- CatalogModule
1410
- ] : [],
1411
- ...tenantBillingConfig ? [
1412
- TenantBillingModule
1413
- ] : [],
1414
- ...subscriptionBundlesConfig ? [
1415
- SubscriptionBundleModule
1416
- ] : [],
1417
- ...adminResourcesConfig ? [
1418
- AdminResourcesModule
1419
- ] : [],
1420
- ...promoCodesConfig ? [
1421
- PromoCodesModule
1422
- ] : [],
1423
- ...setupConfig ? [
1424
- SetupModule
1425
- ] : [],
1426
- ...adminStatsConfig ? [
1427
- AdminStatsModule
1428
- ] : [],
1429
- ...checkoutOfferConfig ? [
1430
- CheckoutOfferModule
1431
- ] : [],
1432
- ...subscriptionContractConfig ? [
1433
- SubscriptionContractModule
1434
- ] : [],
1435
- ...lightweightExports
1436
- ],
1843
+ controllers: tenantManifest.controllers,
1844
+ exports: composeModuleExports(composition, lightweightExports),
1437
1845
  global: true
1438
1846
  };
1439
1847
  }
@@ -1448,17 +1856,28 @@ export {
1448
1856
  SetupService,
1449
1857
  SetupController,
1450
1858
  SetupModule,
1859
+ ENFORCEMENT_CHAIN_STATE_TOKEN,
1860
+ EnforcementChainCheck,
1861
+ headingSlug,
1862
+ docsUrlFor,
1863
+ PLATFORM_RULES,
1864
+ findViolations,
1865
+ SaaSiCatConfigurationError,
1866
+ assertConfiguration,
1867
+ QuotaProvidersUsageSnapshot,
1868
+ FEATURE_COMPOSERS,
1869
+ composeFeatures,
1870
+ MOUNTED_BUT_NOT_EXPORTED,
1871
+ composeModuleExports,
1451
1872
  PLAN_RESOLVER_PORT_TOKEN,
1452
1873
  StaticPlanResolver,
1453
1874
  StaticEntitlementService,
1454
- FeatureGuardCoverageCheck,
1455
- STATIC_FEATURE_GUARD_CONFIG_TOKEN,
1456
- StaticFeatureGuard,
1875
+ TenantManifestService,
1876
+ buildTenantManifestController,
1457
1877
  QUOTA_PROVIDERS_TOKEN,
1458
1878
  EnforceQuotaInterceptor,
1459
- QuotaProvidersUsageSnapshot,
1879
+ STATIC_FEATURE_GUARD_CONFIG_TOKEN,
1880
+ StaticFeatureGuard,
1460
1881
  SubscriptionPlanResolver,
1461
- TenantManifestService,
1462
- buildTenantManifestController,
1463
1882
  SaasPlatformModule
1464
1883
  };