@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.
- package/LICENSE +90 -201
- package/dist/_entries.cjs +3265 -2846
- package/dist/billing/index.d.cts +4 -4
- package/dist/billing/index.d.ts +4 -4
- package/dist/billing/index.js +29 -27
- package/dist/chunk-2FR6ZL7R.js +0 -0
- package/dist/chunk-2JTISFUZ.js +792 -0
- package/dist/chunk-2SLTRKXC.js +0 -0
- package/dist/chunk-3E6ONOMG.js +50 -0
- package/dist/{chunk-B7NRY2LV.js → chunk-CJFAOD7K.js} +1 -39
- package/dist/{chunk-TCBYOFAC.js → chunk-EHKMPP6J.js} +1164 -745
- package/dist/{chunk-NNNXXUPT.js → chunk-F4B2NJUZ.js} +23 -88
- package/dist/chunk-H4DOTO7S.js +85 -0
- package/dist/{chunk-YWDLYOFN.js → chunk-UHD6FJBQ.js} +412 -1188
- package/dist/{define-saasicat-BK6RRWWG.d.ts → define-saasicat-3ois8t8s.d.ts} +26 -3
- package/dist/{define-saasicat-D30I-WET.d.cts → define-saasicat-CFxut1VR.d.cts} +26 -3
- package/dist/{enforce-quota.interceptor-B9QueAFf.d.ts → enforce-quota.interceptor-CYDNvAQz.d.ts} +3 -3
- package/dist/{enforce-quota.interceptor-DY6CLjoP.d.cts → enforce-quota.interceptor-N80KC34N.d.cts} +3 -3
- package/dist/entitlement/index.js +5 -2
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +39 -31
- package/dist/{saas-platform.module-D1mUZPrz.d.ts → module-options-ByJL7FZm.d.ts} +40 -27
- package/dist/{saas-platform.module-CgalxzcT.d.cts → module-options-klrPnWGF.d.cts} +40 -27
- package/dist/platform/index.d.cts +201 -19
- package/dist/platform/index.d.ts +201 -19
- package/dist/platform/index.js +34 -9
- package/dist/subscription-contract/index.js +5 -2
- package/dist/{tenant-billing.controller-DHSUrT_N.d.cts → tenant-billing.controller-CAMMdfn1.d.cts} +1 -1
- package/dist/{tenant-billing.controller-BSlpFqVA.d.ts → tenant-billing.controller-CYYK0CpT.d.ts} +1 -1
- package/dist/{subscription-bundles.module-BDbODWvA.d.ts → tenant-billing.module-B0stLoJn.d.ts} +51 -51
- package/dist/{subscription-bundles.module-Cfi-lFWF.d.cts → tenant-billing.module-BxJS6fX4.d.cts} +51 -51
- package/dist/testing/index.d.cts +8 -8
- package/dist/testing/index.d.ts +8 -8
- package/dist/testing/index.js +6 -4
- package/package.json +4 -4
- /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-
|
|
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-
|
|
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
|
|
370
|
-
function
|
|
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(
|
|
377
|
-
function
|
|
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(
|
|
381
|
-
function
|
|
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(
|
|
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 =
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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/
|
|
451
|
-
import {
|
|
452
|
-
|
|
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(
|
|
460
|
-
function
|
|
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(
|
|
464
|
-
|
|
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, "
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
*
|
|
493
|
-
*
|
|
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
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
],
|
|
541
|
-
function
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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(
|
|
1225
|
+
__name(isVisible, "isVisible");
|
|
557
1226
|
|
|
558
|
-
// src/platform/
|
|
559
|
-
import {
|
|
560
|
-
|
|
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(
|
|
569
|
-
function
|
|
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(
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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
|
|
1288
|
+
import { Reflector } from "@nestjs/core";
|
|
649
1289
|
import { from } from "rxjs";
|
|
650
1290
|
import { switchMap } from "rxjs/operators";
|
|
651
|
-
function
|
|
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(
|
|
658
|
-
function
|
|
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(
|
|
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 =
|
|
1350
|
+
EnforceQuotaInterceptor = _ts_decorate8([
|
|
711
1351
|
Injectable5(),
|
|
712
|
-
_ts_param6(0, Inject5(
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
typeof
|
|
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/
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
|
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
|
-
|
|
903
|
-
|
|
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
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
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
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
if (
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
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:
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
StaticFeatureGuard,
|
|
1875
|
+
TenantManifestService,
|
|
1876
|
+
buildTenantManifestController,
|
|
1457
1877
|
QUOTA_PROVIDERS_TOKEN,
|
|
1458
1878
|
EnforceQuotaInterceptor,
|
|
1459
|
-
|
|
1879
|
+
STATIC_FEATURE_GUARD_CONFIG_TOKEN,
|
|
1880
|
+
StaticFeatureGuard,
|
|
1460
1881
|
SubscriptionPlanResolver,
|
|
1461
|
-
TenantManifestService,
|
|
1462
|
-
buildTenantManifestController,
|
|
1463
1882
|
SaasPlatformModule
|
|
1464
1883
|
};
|