@saasicat/nest 0.26.1 → 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 (37) 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 +29 -27
  6. package/dist/chunk-2FR6ZL7R.js +0 -0
  7. package/dist/chunk-2JTISFUZ.js +792 -0
  8. package/dist/chunk-2SLTRKXC.js +0 -0
  9. package/dist/chunk-3E6ONOMG.js +50 -0
  10. package/dist/{chunk-B7NRY2LV.js → chunk-CJFAOD7K.js} +1 -39
  11. package/dist/{chunk-TCBYOFAC.js → chunk-EHKMPP6J.js} +1164 -745
  12. package/dist/{chunk-NNNXXUPT.js → chunk-F4B2NJUZ.js} +23 -88
  13. package/dist/chunk-H4DOTO7S.js +85 -0
  14. package/dist/{chunk-YWDLYOFN.js → chunk-UHD6FJBQ.js} +412 -1188
  15. package/dist/{define-saasicat-BK6RRWWG.d.ts → define-saasicat-3ois8t8s.d.ts} +26 -3
  16. package/dist/{define-saasicat-D30I-WET.d.cts → define-saasicat-CFxut1VR.d.cts} +26 -3
  17. package/dist/{enforce-quota.interceptor-B9QueAFf.d.ts → enforce-quota.interceptor-CYDNvAQz.d.ts} +3 -3
  18. package/dist/{enforce-quota.interceptor-DY6CLjoP.d.cts → enforce-quota.interceptor-N80KC34N.d.cts} +3 -3
  19. package/dist/entitlement/index.js +5 -2
  20. package/dist/index.d.cts +5 -5
  21. package/dist/index.d.ts +5 -5
  22. package/dist/index.js +39 -31
  23. package/dist/{saas-platform.module-D1mUZPrz.d.ts → module-options-ByJL7FZm.d.ts} +40 -27
  24. package/dist/{saas-platform.module-CgalxzcT.d.cts → module-options-klrPnWGF.d.cts} +40 -27
  25. package/dist/platform/index.d.cts +201 -19
  26. package/dist/platform/index.d.ts +201 -19
  27. package/dist/platform/index.js +34 -9
  28. package/dist/subscription-contract/index.js +5 -2
  29. package/dist/{tenant-billing.controller-DHSUrT_N.d.cts → tenant-billing.controller-CAMMdfn1.d.cts} +1 -1
  30. package/dist/{tenant-billing.controller-BSlpFqVA.d.ts → tenant-billing.controller-CYYK0CpT.d.ts} +1 -1
  31. package/dist/{subscription-bundles.module-BDbODWvA.d.ts → tenant-billing.module-B0stLoJn.d.ts} +51 -51
  32. package/dist/{subscription-bundles.module-Cfi-lFWF.d.cts → tenant-billing.module-BxJS6fX4.d.cts} +51 -51
  33. package/dist/testing/index.d.cts +8 -8
  34. package/dist/testing/index.d.ts +8 -8
  35. package/dist/testing/index.js +6 -4
  36. package/package.json +4 -4
  37. /package/dist/{chunk-FY4EJTV4.js → chunk-OYNHOY45.js} +0 -0
@@ -1,6 +1,9 @@
1
1
  import {
2
2
  PromoCodesModule
3
3
  } from "./chunk-B6IWLV7R.js";
4
+ import {
5
+ SubscriptionContractModule
6
+ } from "./chunk-3E6ONOMG.js";
4
7
  import {
5
8
  AdminManifestModule,
6
9
  AdminManifestService,
@@ -17,10 +20,7 @@ import {
17
20
  SubscriptionBundleModule,
18
21
  TenantBillingModule,
19
22
  isPlatformFeatureGuard
20
- } from "./chunk-YWDLYOFN.js";
21
- import {
22
- SubscriptionContractModule
23
- } from "./chunk-B7NRY2LV.js";
23
+ } from "./chunk-UHD6FJBQ.js";
24
24
  import {
25
25
  CatalogModule
26
26
  } from "./chunk-7X5MSAIL.js";
@@ -34,9 +34,11 @@ import {
34
34
  DiscoveryModule
35
35
  } from "./chunk-6WL6VQ7Z.js";
36
36
  import {
37
- EntitlementModule,
37
+ EntitlementModule
38
+ } from "./chunk-H4DOTO7S.js";
39
+ import {
38
40
  LimitExceededError
39
- } from "./chunk-NNNXXUPT.js";
41
+ } from "./chunk-F4B2NJUZ.js";
40
42
  import {
41
43
  PLAN_CATALOG_TOKEN,
42
44
  PlanCatalogModule
@@ -350,6 +352,707 @@ SetupModule = _ts_decorate3([
350
352
  Module({})
351
353
  ], SetupModule);
352
354
 
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";
358
+ function _ts_decorate4(decorators, target, key, desc) {
359
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
360
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
361
+ 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;
362
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
363
+ }
364
+ __name(_ts_decorate4, "_ts_decorate");
365
+ function _ts_metadata3(k, v) {
366
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
367
+ }
368
+ __name(_ts_metadata3, "_ts_metadata");
369
+ function _ts_param3(paramIndex, decorator) {
370
+ return function(target, key) {
371
+ decorator(target, key, paramIndex);
372
+ };
373
+ }
374
+ __name(_ts_param3, "_ts_param");
375
+ var GUARDS_METADATA = "__guards__";
376
+ var METHOD_METADATA = "method";
377
+ var ENFORCEMENT_CHAIN_STATE_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform/EnforcementChainState");
378
+ var EnforcementChainCheck = class {
379
+ static {
380
+ __name(this, "EnforcementChainCheck");
381
+ }
382
+ discoveryService;
383
+ metadataScanner;
384
+ state;
385
+ constructor(discoveryService, metadataScanner, state = {
386
+ featureEnforcementActive: true,
387
+ quotaEnforcementActive: true,
388
+ globalGuardBound: true,
389
+ globalGuardOptedOut: false
390
+ }) {
391
+ this.discoveryService = discoveryService;
392
+ this.metadataScanner = metadataScanner;
393
+ this.state = state;
394
+ }
395
+ onApplicationBootstrap() {
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);
414
+ if (uncovered.length === 0) return;
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.`);
420
+ }
421
+ /**
422
+ * Every route carrying an enforcement annotation, with whether a
423
+ * recognisable feature guard covers it.
424
+ *
425
+ * Deliberately conservative in one direction only: an unrecognised wrapper
426
+ * is reported (and the message says so) rather than assumed safe, because
427
+ * the named route is cheap to check. The reverse — staying silent about a
428
+ * route with no guard at all — is the failure this exists to prevent.
429
+ */
430
+ findAnnotatedRoutes() {
431
+ const annotated = [];
432
+ for (const wrapper of this.discoveryService.getControllers()) {
433
+ const instance = wrapper.instance;
434
+ if (!instance || typeof instance !== "object") continue;
435
+ const ctor = instance.constructor;
436
+ if (!ctor) continue;
437
+ const classGuards = readGuards(ctor);
438
+ const prototype = Object.getPrototypeOf(instance);
439
+ for (const method of this.metadataScanner.getAllMethodNames(prototype)) {
440
+ const handler = instance[method];
441
+ if (typeof handler !== "function") continue;
442
+ if (Reflect.getMetadata(METHOD_METADATA, handler) === void 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;
447
+ const guards = [
448
+ ...classGuards,
449
+ ...readGuards(handler)
450
+ ];
451
+ annotated.push({
452
+ controller: ctor.name,
453
+ handler: method,
454
+ features,
455
+ quota,
456
+ guarded: guards.some(isPlatformFeatureGuard)
457
+ });
458
+ }
459
+ }
460
+ return annotated;
461
+ }
462
+ };
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", [
471
+ typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
472
+ typeof MetadataScanner === "undefined" ? Object : MetadataScanner,
473
+ typeof EnforcementChainState === "undefined" ? Object : EnforcementChainState
474
+ ])
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");
483
+ function readFeatures(target) {
484
+ const value = Reflect.getMetadata(REQUIRE_FEATURE_KEY, target);
485
+ return Array.isArray(value) ? value : null;
486
+ }
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");
495
+ function readGuards(target) {
496
+ const value = Reflect.getMetadata(GUARDS_METADATA, target);
497
+ if (!Array.isArray(value)) return [];
498
+ return value.flatMap((guard) => typeof guard === "function" ? [
499
+ guard
500
+ ] : [
501
+ guard,
502
+ guard?.constructor
503
+ ]);
504
+ }
505
+ __name(readGuards, "readGuards");
506
+
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;
514
+ }
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]);
521
+ }
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
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;
680
+ }
681
+ __name(findViolations, "findViolations");
682
+ var SaaSiCatConfigurationError = class extends Error {
683
+ static {
684
+ __name(this, "SaaSiCatConfigurationError");
685
+ }
686
+ violations;
687
+ constructor(violations) {
688
+ super(formatViolations(violations)), this.violations = violations;
689
+ this.name = "SaaSiCatConfigurationError";
690
+ }
691
+ };
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}`;
698
+ }
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);
708
+ }
709
+ __name(assertConfiguration, "assertConfiguration");
710
+
711
+ // src/platform/quota-providers-usage-snapshot.ts
712
+ var QuotaProvidersUsageSnapshot = class {
713
+ static {
714
+ __name(this, "QuotaProvidersUsageSnapshot");
715
+ }
716
+ providers;
717
+ constructor(providers) {
718
+ this.providers = providers;
719
+ }
720
+ async snapshot(tenantId) {
721
+ const entries = await Promise.all(this.providers.map(async (provider) => [
722
+ provider.key,
723
+ await provider.count(tenantId)
724
+ ]));
725
+ return Object.fromEntries(entries);
726
+ }
727
+ };
728
+
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
+
353
1056
  // src/platform/plan-resolver.port.ts
354
1057
  var PLAN_RESOLVER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/PlanResolverPort");
355
1058
  var StaticPlanResolver = class {
@@ -366,24 +1069,24 @@ var StaticPlanResolver = class {
366
1069
  };
367
1070
 
368
1071
  // src/platform/static-entitlement.service.ts
369
- import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
370
- function _ts_decorate4(decorators, target, key, desc) {
1072
+ import { Inject as Inject3, Injectable as Injectable3 } from "@nestjs/common";
1073
+ function _ts_decorate5(decorators, target, key, desc) {
371
1074
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
372
1075
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
373
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;
374
1077
  return c > 3 && r && Object.defineProperty(target, key, r), r;
375
1078
  }
376
- __name(_ts_decorate4, "_ts_decorate");
377
- function _ts_metadata3(k, v) {
1079
+ __name(_ts_decorate5, "_ts_decorate");
1080
+ function _ts_metadata4(k, v) {
378
1081
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
379
1082
  }
380
- __name(_ts_metadata3, "_ts_metadata");
381
- function _ts_param3(paramIndex, decorator) {
1083
+ __name(_ts_metadata4, "_ts_metadata");
1084
+ function _ts_param4(paramIndex, decorator) {
382
1085
  return function(target, key) {
383
1086
  decorator(target, key, paramIndex);
384
1087
  };
385
1088
  }
386
- __name(_ts_param3, "_ts_param");
1089
+ __name(_ts_param4, "_ts_param");
387
1090
  var StaticEntitlementService = class {
388
1091
  static {
389
1092
  __name(this, "StaticEntitlementService");
@@ -436,229 +1139,166 @@ var StaticEntitlementService = class {
436
1139
  return (this.catalog.plans ?? []).find((p) => p.id === planId);
437
1140
  }
438
1141
  };
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", [
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", [
445
1148
  typeof PlanCatalog === "undefined" ? Object : PlanCatalog,
446
1149
  typeof PlanResolverPort === "undefined" ? Object : PlanResolverPort
447
1150
  ])
448
1151
  ], StaticEntitlementService);
449
1152
 
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) {
1153
+ // src/platform/tenant-manifest.service.ts
1154
+ import { Injectable as Injectable4 } from "@nestjs/common";
1155
+ function _ts_decorate6(decorators, target, key, desc) {
454
1156
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
455
1157
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
456
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;
457
1159
  return c > 3 && r && Object.defineProperty(target, key, r), r;
458
1160
  }
459
- __name(_ts_decorate5, "_ts_decorate");
460
- function _ts_metadata4(k, v) {
1161
+ __name(_ts_decorate6, "_ts_decorate");
1162
+ function _ts_metadata5(k, v) {
461
1163
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
462
1164
  }
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
- var GUARDS_METADATA = "__guards__";
471
- var METHOD_METADATA = "method";
472
- var FeatureGuardCoverageCheck = class {
1165
+ __name(_ts_metadata5, "_ts_metadata");
1166
+ var TenantManifestService = class {
473
1167
  static {
474
- __name(this, "FeatureGuardCoverageCheck");
475
- }
476
- discoveryService;
477
- metadataScanner;
478
- logger = new Logger("SaaSiCat");
479
- constructor(discoveryService, metadataScanner) {
480
- this.discoveryService = discoveryService;
481
- this.metadataScanner = metadataScanner;
1168
+ __name(this, "TenantManifestService");
482
1169
  }
483
- onApplicationBootstrap() {
484
- const uncovered = this.findUncoveredRoutes();
485
- 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.`);
1170
+ entitlements;
1171
+ navItems = [];
1172
+ constructor(entitlements) {
1173
+ this.entitlements = entitlements;
490
1174
  }
491
1175
  /**
492
- * Annotated handlers whose own guards and whose controller's guards contain
493
- * no recognisable feature guard.
494
- *
495
- * Deliberately conservative in one direction only: an unrecognised wrapper
496
- * is reported (and the message says so) rather than assumed safe, because
497
- * the named route is cheap to check. The reverse — staying silent about a
498
- * route with no guard at all — is the failure this exists to prevent.
1176
+ * Call in the app's `OnModuleInit`: registers app navigation for the
1177
+ * Tenant UI. Idempotent: the same `id` overwrites.
499
1178
  */
500
- findUncoveredRoutes() {
501
- const uncovered = [];
502
- for (const wrapper of this.discoveryService.getControllers()) {
503
- const instance = wrapper.instance;
504
- if (!instance || typeof instance !== "object") continue;
505
- const ctor = instance.constructor;
506
- if (!ctor) continue;
507
- const classGuards = readGuards(ctor);
508
- const prototype = Object.getPrototypeOf(instance);
509
- for (const method of this.metadataScanner.getAllMethodNames(prototype)) {
510
- const handler = instance[method];
511
- if (typeof handler !== "function") continue;
512
- 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;
516
- const guards = [
517
- ...classGuards,
518
- ...readGuards(handler)
519
- ];
520
- if (guards.some(isPlatformFeatureGuard)) continue;
521
- uncovered.push({
522
- controller: ctor.name,
523
- handler: method,
524
- features
525
- });
526
- }
527
- }
528
- return uncovered;
1179
+ registerNavItem(item) {
1180
+ const existing = this.navItems.findIndex((i) => i.id === item.id);
1181
+ if (existing >= 0) this.navItems[existing] = item;
1182
+ else this.navItems.push(item);
1183
+ }
1184
+ /** Bulk variant. */
1185
+ registerNavItems(items) {
1186
+ for (const item of items) this.registerNavItem(item);
1187
+ }
1188
+ /**
1189
+ * Returns the filtered manifest for a Tenant. NavItems with
1190
+ * `requiresFeature` are filtered out server-side as soon as none of the
1191
+ * required features are present in the entitlement snapshot.
1192
+ */
1193
+ async getManifest(tenantId) {
1194
+ const snap = await this.entitlements.snapshot(tenantId);
1195
+ const featureSet = new Set(snap.features);
1196
+ const visible = this.navItems.filter((item) => isVisible(item, featureSet)).map((item) => ({
1197
+ ...item
1198
+ })).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
1199
+ return {
1200
+ schemaVersion: 1,
1201
+ tenant: {
1202
+ id: tenantId
1203
+ },
1204
+ planId: snap.planId,
1205
+ features: snap.features,
1206
+ quotas: snap.quotas,
1207
+ navigation: visible
1208
+ };
529
1209
  }
530
1210
  };
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", [
537
- typeof DiscoveryService === "undefined" ? Object : DiscoveryService,
538
- typeof MetadataScanner === "undefined" ? Object : MetadataScanner
1211
+ TenantManifestService = _ts_decorate6([
1212
+ Injectable4(),
1213
+ _ts_metadata5("design:type", Function),
1214
+ _ts_metadata5("design:paramtypes", [
1215
+ typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService
539
1216
  ])
540
- ], FeatureGuardCoverageCheck);
541
- function readFeatures(target) {
542
- const value = Reflect.getMetadata(REQUIRE_FEATURE_KEY, target);
543
- return Array.isArray(value) ? value : null;
544
- }
545
- __name(readFeatures, "readFeatures");
546
- function readGuards(target) {
547
- const value = Reflect.getMetadata(GUARDS_METADATA, target);
548
- if (!Array.isArray(value)) return [];
549
- return value.flatMap((guard) => typeof guard === "function" ? [
550
- guard
551
- ] : [
552
- guard,
553
- guard?.constructor
554
- ]);
1217
+ ], TenantManifestService);
1218
+ function isVisible(item, features) {
1219
+ if (!item.requiresFeature) return true;
1220
+ const required = Array.isArray(item.requiresFeature) ? item.requiresFeature : [
1221
+ item.requiresFeature
1222
+ ];
1223
+ return required.some((f) => features.has(f));
555
1224
  }
556
- __name(readGuards, "readGuards");
1225
+ __name(isVisible, "isVisible");
557
1226
 
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) {
1227
+ // src/platform/tenant-manifest.controller.ts
1228
+ import { Controller as Controller2, Get as Get2, Inject as Inject4, Req, UseGuards } from "@nestjs/common";
1229
+ function _ts_decorate7(decorators, target, key, desc) {
563
1230
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
564
1231
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
565
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;
566
1233
  return c > 3 && r && Object.defineProperty(target, key, r), r;
567
1234
  }
568
- __name(_ts_decorate6, "_ts_decorate");
569
- function _ts_metadata5(k, v) {
1235
+ __name(_ts_decorate7, "_ts_decorate");
1236
+ function _ts_metadata6(k, v) {
570
1237
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
571
1238
  }
572
- __name(_ts_metadata5, "_ts_metadata");
1239
+ __name(_ts_metadata6, "_ts_metadata");
573
1240
  function _ts_param5(paramIndex, decorator) {
574
1241
  return function(target, key) {
575
1242
  decorator(target, key, paramIndex);
576
1243
  };
577
1244
  }
578
1245
  __name(_ts_param5, "_ts_param");
579
- var STATIC_FEATURE_GUARD_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-nest/StaticFeatureGuardConfig");
580
- var StaticFeatureGuard = class {
581
- 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;
593
- }
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
- });
1246
+ function buildTenantManifestController(options) {
1247
+ const path = options.path ?? "tenant/manifest";
1248
+ let GeneratedTenantManifestController = class GeneratedTenantManifestController {
1249
+ static {
1250
+ __name(this, "GeneratedTenantManifestController");
607
1251
  }
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
- });
1252
+ svc;
1253
+ constructor(svc) {
1254
+ this.svc = svc;
616
1255
  }
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);
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);
628
1262
  }
629
- return true;
630
- }
631
- };
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);
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");
645
1285
 
646
1286
  // src/platform/enforce-quota.interceptor.ts
647
1287
  import { Inject as Inject5, Injectable as Injectable5, Optional as Optional2 } from "@nestjs/common";
648
- import { Reflector as Reflector2 } from "@nestjs/core";
1288
+ import { Reflector } from "@nestjs/core";
649
1289
  import { from } from "rxjs";
650
1290
  import { switchMap } from "rxjs/operators";
651
- function _ts_decorate7(decorators, target, key, desc) {
1291
+ function _ts_decorate8(decorators, target, key, desc) {
652
1292
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
653
1293
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
654
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;
655
1295
  return c > 3 && r && Object.defineProperty(target, key, r), r;
656
1296
  }
657
- __name(_ts_decorate7, "_ts_decorate");
658
- function _ts_metadata6(k, v) {
1297
+ __name(_ts_decorate8, "_ts_decorate");
1298
+ function _ts_metadata7(k, v) {
659
1299
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
660
1300
  }
661
- __name(_ts_metadata6, "_ts_metadata");
1301
+ __name(_ts_metadata7, "_ts_metadata");
662
1302
  function _ts_param6(paramIndex, decorator) {
663
1303
  return function(target, key) {
664
1304
  decorator(target, key, paramIndex);
@@ -707,136 +1347,24 @@ var EnforceQuotaInterceptor = class {
707
1347
  }
708
1348
  }
709
1349
  };
710
- EnforceQuotaInterceptor = _ts_decorate7([
1350
+ EnforceQuotaInterceptor = _ts_decorate8([
711
1351
  Injectable5(),
712
- _ts_param6(0, Inject5(Reflector2)),
1352
+ _ts_param6(0, Inject5(Reflector)),
713
1353
  _ts_param6(1, Inject5(StaticEntitlementService)),
714
1354
  _ts_param6(2, Optional2()),
715
1355
  _ts_param6(2, Inject5(QUOTA_PROVIDERS_TOKEN)),
716
- _ts_metadata6("design:type", Function),
717
- _ts_metadata6("design:paramtypes", [
718
- typeof Reflector2 === "undefined" ? Object : Reflector2,
1356
+ _ts_metadata7("design:type", Function),
1357
+ _ts_metadata7("design:paramtypes", [
1358
+ typeof Reflector === "undefined" ? Object : Reflector,
719
1359
  typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService,
720
1360
  Object
721
1361
  ])
722
1362
  ], EnforceQuotaInterceptor);
723
1363
 
724
- // src/platform/quota-providers-usage-snapshot.ts
725
- var QuotaProvidersUsageSnapshot = class {
726
- static {
727
- __name(this, "QuotaProvidersUsageSnapshot");
728
- }
729
- providers;
730
- constructor(providers) {
731
- this.providers = providers;
732
- }
733
- async snapshot(tenantId) {
734
- const entries = await Promise.all(this.providers.map(async (provider) => [
735
- provider.key,
736
- await provider.count(tenantId)
737
- ]));
738
- return Object.fromEntries(entries);
739
- }
740
- };
741
-
742
- // src/platform/subscription-plan-resolver.ts
743
- var DEFAULT_ACTIVE_STATUSES = /* @__PURE__ */ new Set([
744
- "ACTIVE",
745
- "TRIAL"
746
- ]);
747
- var SubscriptionPlanResolver = class {
748
- static {
749
- __name(this, "SubscriptionPlanResolver");
750
- }
751
- subscriptions;
752
- activeStatuses;
753
- constructor(subscriptions, activeStatuses = DEFAULT_ACTIVE_STATUSES) {
754
- this.subscriptions = subscriptions;
755
- this.activeStatuses = activeStatuses;
756
- }
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;
761
- }
762
- };
763
-
764
- // src/platform/tenant-manifest.service.ts
765
- import { Injectable as Injectable6 } from "@nestjs/common";
766
- function _ts_decorate8(decorators, target, key, desc) {
767
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
768
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
769
- 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
- return c > 3 && r && Object.defineProperty(target, key, r), r;
771
- }
772
- __name(_ts_decorate8, "_ts_decorate");
773
- function _ts_metadata7(k, v) {
774
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
775
- }
776
- __name(_ts_metadata7, "_ts_metadata");
777
- var TenantManifestService = class {
778
- static {
779
- __name(this, "TenantManifestService");
780
- }
781
- entitlements;
782
- navItems = [];
783
- constructor(entitlements) {
784
- this.entitlements = entitlements;
785
- }
786
- /**
787
- * Call in the app's `OnModuleInit`: registers app navigation for the
788
- * Tenant UI. Idempotent: the same `id` overwrites.
789
- */
790
- registerNavItem(item) {
791
- const existing = this.navItems.findIndex((i) => i.id === item.id);
792
- if (existing >= 0) this.navItems[existing] = item;
793
- else this.navItems.push(item);
794
- }
795
- /** Bulk variant. */
796
- registerNavItems(items) {
797
- for (const item of items) this.registerNavItem(item);
798
- }
799
- /**
800
- * Returns the filtered manifest for a Tenant. NavItems with
801
- * `requiresFeature` are filtered out server-side as soon as none of the
802
- * required features are present in the entitlement snapshot.
803
- */
804
- async getManifest(tenantId) {
805
- const snap = await this.entitlements.snapshot(tenantId);
806
- const featureSet = new Set(snap.features);
807
- const visible = this.navItems.filter((item) => isVisible(item, featureSet)).map((item) => ({
808
- ...item
809
- })).sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
810
- return {
811
- schemaVersion: 1,
812
- tenant: {
813
- id: tenantId
814
- },
815
- planId: snap.planId,
816
- features: snap.features,
817
- quotas: snap.quotas,
818
- navigation: visible
819
- };
820
- }
821
- };
822
- TenantManifestService = _ts_decorate8([
823
- Injectable6(),
824
- _ts_metadata7("design:type", Function),
825
- _ts_metadata7("design:paramtypes", [
826
- typeof StaticEntitlementService === "undefined" ? Object : StaticEntitlementService
827
- ])
828
- ], TenantManifestService);
829
- function isVisible(item, features) {
830
- if (!item.requiresFeature) return true;
831
- const required = Array.isArray(item.requiresFeature) ? item.requiresFeature : [
832
- item.requiresFeature
833
- ];
834
- return required.some((f) => features.has(f));
835
- }
836
- __name(isVisible, "isVisible");
837
-
838
- // src/platform/tenant-manifest.controller.ts
839
- import { Controller as Controller2, Get as Get2, Inject as Inject6, Req, UseGuards } from "@nestjs/common";
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
  };