@saasicat/nest 0.18.0 → 0.19.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/dist/_entries.cjs +1309 -519
- package/dist/admin/index.js +1 -1
- package/dist/billing/index.d.cts +7 -4
- package/dist/billing/index.d.ts +7 -4
- package/dist/billing/index.js +6 -6
- package/dist/catalog/index.js +4 -4
- package/dist/checkout-offer/index.js +2 -2
- package/dist/{chunk-Z3FWN3HJ.js → chunk-5ZXBXYXU.js} +3 -3
- package/dist/{chunk-RZVSKKXX.js → chunk-6WA4X5U3.js} +46 -45
- package/dist/{chunk-V34AA7C7.js → chunk-6WL6VQ7Z.js} +1 -1
- package/dist/{chunk-ABB2CRAS.js → chunk-74SCJ4HF.js} +1 -1
- package/dist/{chunk-PLJEAUFU.js → chunk-7JIHG53V.js} +76 -39
- package/dist/{chunk-7RRWI5LH.js → chunk-AFK2IE2L.js} +10 -10
- package/dist/{chunk-32EMXPU2.js → chunk-B7NRY2LV.js} +48 -17
- package/dist/{chunk-MDIZUVIK.js → chunk-BSK6YBLI.js} +1 -1
- package/dist/{chunk-R2QOSVAA.js → chunk-DQKCK7DX.js} +1 -1
- package/dist/{chunk-GLLCB4BP.js → chunk-EUU3B6NB.js} +17 -13
- package/dist/{chunk-EXOLOJFZ.js → chunk-F6DCLVBB.js} +29 -8
- package/dist/{chunk-5KH2O4SF.js → chunk-I77VS3YH.js} +287 -105
- package/dist/{chunk-YITTE646.js → chunk-KUDKD7ZG.js} +34 -14
- package/dist/{chunk-P3DP6GJE.js → chunk-LXUBCLRK.js} +486 -160
- package/dist/{chunk-GJBXOCRZ.js → chunk-ORWIGSNN.js} +45 -17
- package/dist/{chunk-E7AJ42AB.js → chunk-QLJHIIOY.js} +168 -28
- package/dist/{chunk-BOADBIGB.js → chunk-YYUXKLXK.js} +2 -2
- package/dist/{define-saasicat-Rc0rXrYG.d.ts → define-saasicat-CqkhnHn-.d.ts} +1 -1
- package/dist/{define-saasicat-CeDBX9ji.d.cts → define-saasicat-D8c6Bs4T.d.cts} +1 -1
- package/dist/discovery/index.d.cts +2 -2
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +4 -4
- package/dist/{enforce-quota.interceptor-DMw-bPyk.d.cts → enforce-quota.interceptor-Dk0eEtzV.d.cts} +1 -1
- package/dist/{enforce-quota.interceptor-CmbWX39L.d.ts → enforce-quota.interceptor-Dp8wTYro.d.ts} +1 -1
- package/dist/entitlement/index.js +2 -2
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +17 -17
- package/dist/{module-DgYJur6f.d.ts → module-BwD-wAor.d.ts} +1 -1
- package/dist/{module-CSN8j8Tp.d.cts → module-DsDkbawm.d.cts} +1 -1
- package/dist/platform/index.d.cts +8 -8
- package/dist/platform/index.d.ts +8 -8
- package/dist/platform/index.js +13 -13
- package/dist/promo/index.d.cts +3 -3
- package/dist/promo/index.d.ts +3 -3
- package/dist/promo/index.js +3 -3
- package/dist/registration/index.js +1 -1
- package/dist/{saas-platform.module-DLhoqKNA.d.ts → saas-platform.module-BQmrDGVU.d.ts} +1 -1
- package/dist/{saas-platform.module-D3rUTiho.d.cts → saas-platform.module-DducnQ9s.d.cts} +1 -1
- package/dist/{service-DX8KbGXl.d.cts → service-DPZ2W1E-.d.cts} +2 -2
- package/dist/{service-DX8KbGXl.d.ts → service-DPZ2W1E-.d.ts} +2 -2
- package/dist/subscription-contract/index.js +2 -2
- package/dist/{tenant-billing.controller-BwZRnJ3q.d.ts → tenant-billing.controller-b1G1yB09.d.ts} +2 -2
- package/dist/{tenant-billing.controller-CG4CaFUP.d.cts → tenant-billing.controller-m42YQ_Ei.d.cts} +2 -2
- package/dist/testing/index.d.cts +5 -5
- package/dist/testing/index.d.ts +5 -5
- package/dist/testing/index.js +33 -33
- package/package.json +3 -3
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
|
|
17
17
|
// src/admin/super-admin.guard.ts
|
|
18
18
|
import { ForbiddenException, Injectable } from "@nestjs/common";
|
|
19
|
+
import { AUTH_ERROR_CODES } from "@saasicat/types";
|
|
19
20
|
function _ts_decorate(decorators, target, key, desc) {
|
|
20
21
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
21
22
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -29,10 +30,18 @@ var SuperAdminGuard = class {
|
|
|
29
30
|
}
|
|
30
31
|
canActivate(context) {
|
|
31
32
|
const request = context.switchToHttp().getRequest();
|
|
32
|
-
if (!request.user)
|
|
33
|
+
if (!request.user) {
|
|
34
|
+
throw new ForbiddenException({
|
|
35
|
+
code: AUTH_ERROR_CODES.NOT_AUTHENTICATED,
|
|
36
|
+
message: "Not authenticated"
|
|
37
|
+
});
|
|
38
|
+
}
|
|
33
39
|
const role = request.user.platformRole ?? request.user.role;
|
|
34
40
|
if (role !== "SUPER_ADMIN") {
|
|
35
|
-
throw new ForbiddenException(
|
|
41
|
+
throw new ForbiddenException({
|
|
42
|
+
code: AUTH_ERROR_CODES.SUPER_ADMIN_REQUIRED,
|
|
43
|
+
message: "Only the SUPER_ADMIN role is allowed"
|
|
44
|
+
});
|
|
36
45
|
}
|
|
37
46
|
return true;
|
|
38
47
|
}
|
|
@@ -119,7 +128,7 @@ var MfaService = class _MfaService {
|
|
|
119
128
|
return false;
|
|
120
129
|
}
|
|
121
130
|
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
-
this.logger.warn(`TOTP
|
|
131
|
+
this.logger.warn(`TOTP verification for user ${input.userId} failed unexpectedly: ${message}`);
|
|
123
132
|
return false;
|
|
124
133
|
}
|
|
125
134
|
}
|
|
@@ -146,6 +155,7 @@ MfaService = _ts_decorate2([
|
|
|
146
155
|
// src/admin/mfa.guard.ts
|
|
147
156
|
import { Inject as Inject2, Injectable as Injectable3, Logger as Logger2, SetMetadata, UnauthorizedException } from "@nestjs/common";
|
|
148
157
|
import { Reflector } from "@nestjs/core";
|
|
158
|
+
import { AUTH_ERROR_CODES as AUTH_ERROR_CODES2 } from "@saasicat/types";
|
|
149
159
|
function _ts_decorate3(decorators, target, key, desc) {
|
|
150
160
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
151
161
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -187,22 +197,30 @@ var MfaGuard = class _MfaGuard {
|
|
|
187
197
|
}
|
|
188
198
|
const request = context.switchToHttp().getRequest();
|
|
189
199
|
const user = request.user;
|
|
190
|
-
if (!user)
|
|
191
|
-
|
|
192
|
-
|
|
200
|
+
if (!user) {
|
|
201
|
+
throw new UnauthorizedException({
|
|
202
|
+
code: AUTH_ERROR_CODES2.NOT_AUTHENTICATED,
|
|
203
|
+
// `reason` is superseded by `code` and will be removed.
|
|
204
|
+
reason: "NOT_AUTHENTICATED"
|
|
205
|
+
});
|
|
206
|
+
}
|
|
193
207
|
const enabled = await this.mfaService.isEnabled(user.id);
|
|
194
208
|
if (!enabled) {
|
|
195
209
|
throw new UnauthorizedException({
|
|
210
|
+
code: AUTH_ERROR_CODES2.MFA_NOT_SET_UP,
|
|
211
|
+
// `reason` is superseded by `code` and will be removed.
|
|
196
212
|
reason: "MFA_NOT_SET_UP",
|
|
197
|
-
message: "
|
|
213
|
+
message: "Run the MFA setup via the CLI first."
|
|
198
214
|
});
|
|
199
215
|
}
|
|
200
216
|
const headerVal = request.headers?.["x-mfa-code"];
|
|
201
217
|
const code = Array.isArray(headerVal) ? headerVal[0] : headerVal;
|
|
202
218
|
if (!code) {
|
|
203
219
|
throw new UnauthorizedException({
|
|
220
|
+
code: AUTH_ERROR_CODES2.MFA_REQUIRED,
|
|
221
|
+
// `reason` is superseded by `code` and will be removed.
|
|
204
222
|
reason: "MFA_REQUIRED",
|
|
205
|
-
message: "TOTP
|
|
223
|
+
message: "TOTP code required in the X-Mfa-Code header."
|
|
206
224
|
});
|
|
207
225
|
}
|
|
208
226
|
const valid = await this.mfaService.verify({
|
|
@@ -211,8 +229,10 @@ var MfaGuard = class _MfaGuard {
|
|
|
211
229
|
});
|
|
212
230
|
if (!valid) {
|
|
213
231
|
throw new UnauthorizedException({
|
|
232
|
+
code: AUTH_ERROR_CODES2.MFA_FAILED,
|
|
233
|
+
// `reason` is superseded by `code` and will be removed.
|
|
214
234
|
reason: "MFA_FAILED",
|
|
215
|
-
message: "TOTP
|
|
235
|
+
message: "Invalid TOTP code."
|
|
216
236
|
});
|
|
217
237
|
}
|
|
218
238
|
return true;
|
|
@@ -615,7 +635,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
615
635
|
actions: [
|
|
616
636
|
{
|
|
617
637
|
id: "platform.tenants.suspend",
|
|
618
|
-
label: "
|
|
638
|
+
label: "Suspend tenant",
|
|
619
639
|
actionKey: "tenants.suspend",
|
|
620
640
|
requiredCapability: "tenants.suspend",
|
|
621
641
|
requiresMfa: true,
|
|
@@ -623,7 +643,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
623
643
|
},
|
|
624
644
|
{
|
|
625
645
|
id: "platform.tenants.reactivate",
|
|
626
|
-
label: "
|
|
646
|
+
label: "Reactivate tenant",
|
|
627
647
|
actionKey: "tenants.reactivate",
|
|
628
648
|
requiredCapability: "tenants.reactivate",
|
|
629
649
|
requiresMfa: true,
|
|
@@ -639,7 +659,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
639
659
|
},
|
|
640
660
|
{
|
|
641
661
|
id: "platform.tenants.export",
|
|
642
|
-
label: "
|
|
662
|
+
label: "GDPR export",
|
|
643
663
|
actionKey: "tenants.export",
|
|
644
664
|
requiredCapability: "tenants.export",
|
|
645
665
|
requiresMfa: true,
|
|
@@ -647,7 +667,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
647
667
|
},
|
|
648
668
|
{
|
|
649
669
|
id: "platform.subscriptions.cancel",
|
|
650
|
-
label: "
|
|
670
|
+
label: "Cancel subscription",
|
|
651
671
|
actionKey: "subscriptions.cancel",
|
|
652
672
|
requiredCapability: "subscriptions.cancel",
|
|
653
673
|
requiresMfa: true,
|
|
@@ -655,7 +675,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
655
675
|
},
|
|
656
676
|
{
|
|
657
677
|
id: "platform.pilots.grant",
|
|
658
|
-
label: "
|
|
678
|
+
label: "Set pilot status",
|
|
659
679
|
actionKey: "pilots.grant",
|
|
660
680
|
requiredCapability: "pilots.grant",
|
|
661
681
|
requiresMfa: true,
|
|
@@ -663,7 +683,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
663
683
|
},
|
|
664
684
|
{
|
|
665
685
|
id: "platform.pilots.revoke",
|
|
666
|
-
label: "
|
|
686
|
+
label: "Revoke pilot status",
|
|
667
687
|
actionKey: "pilots.revoke",
|
|
668
688
|
requiredCapability: "pilots.revoke",
|
|
669
689
|
requiresMfa: true,
|
|
@@ -671,7 +691,7 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
671
691
|
},
|
|
672
692
|
{
|
|
673
693
|
id: "platform.pilots.extend",
|
|
674
|
-
label: "
|
|
694
|
+
label: "Extend pilot",
|
|
675
695
|
actionKey: "pilots.extend",
|
|
676
696
|
requiredCapability: "pilots.extend",
|
|
677
697
|
requiresMfa: true,
|
|
@@ -683,102 +703,102 @@ var PLATFORM_CORE_MANIFEST_CONTRIBUTION = {
|
|
|
683
703
|
actions: [
|
|
684
704
|
{
|
|
685
705
|
key: "TENANT_SUSPEND",
|
|
686
|
-
label: "
|
|
706
|
+
label: "Tenant suspended",
|
|
687
707
|
severity: "high"
|
|
688
708
|
},
|
|
689
709
|
{
|
|
690
710
|
key: "TENANT_REACTIVATE",
|
|
691
|
-
label: "
|
|
711
|
+
label: "Tenant reactivated",
|
|
692
712
|
severity: "medium"
|
|
693
713
|
},
|
|
694
714
|
{
|
|
695
715
|
key: "TENANT_IMPERSONATE",
|
|
696
|
-
label: "Impersonation
|
|
716
|
+
label: "Impersonation started",
|
|
697
717
|
severity: "high"
|
|
698
718
|
},
|
|
699
719
|
{
|
|
700
720
|
key: "TENANT_EXPORT",
|
|
701
|
-
label: "
|
|
721
|
+
label: "GDPR export triggered",
|
|
702
722
|
severity: "medium"
|
|
703
723
|
},
|
|
704
724
|
{
|
|
705
725
|
key: "PILOT_CREATE",
|
|
706
|
-
label: "Pilot
|
|
726
|
+
label: "Pilot tenant created",
|
|
707
727
|
severity: "medium"
|
|
708
728
|
},
|
|
709
729
|
{
|
|
710
730
|
key: "PILOT_GRANT",
|
|
711
|
-
label: "Pilot
|
|
731
|
+
label: "Pilot status set",
|
|
712
732
|
severity: "medium"
|
|
713
733
|
},
|
|
714
734
|
{
|
|
715
735
|
key: "PILOT_REVOKE",
|
|
716
|
-
label: "Pilot
|
|
736
|
+
label: "Pilot status revoked",
|
|
717
737
|
severity: "medium"
|
|
718
738
|
},
|
|
719
739
|
{
|
|
720
740
|
key: "PILOT_EXTEND",
|
|
721
|
-
label: "Pilot
|
|
741
|
+
label: "Pilot extended",
|
|
722
742
|
severity: "low"
|
|
723
743
|
},
|
|
724
744
|
{
|
|
725
745
|
key: "PLAN_VERSION_PUBLISH",
|
|
726
|
-
label: "Plan
|
|
746
|
+
label: "Plan version approved",
|
|
727
747
|
severity: "high"
|
|
728
748
|
},
|
|
729
749
|
{
|
|
730
750
|
key: "PLAN_VERSION_TERMINATE",
|
|
731
|
-
label: "Plan
|
|
751
|
+
label: "Plan version terminated",
|
|
732
752
|
severity: "high"
|
|
733
753
|
},
|
|
734
754
|
{
|
|
735
755
|
key: "SUBSCRIPTION_CANCEL",
|
|
736
|
-
label: "Subscription
|
|
756
|
+
label: "Subscription cancelled",
|
|
737
757
|
severity: "high"
|
|
738
758
|
},
|
|
739
759
|
{
|
|
740
760
|
key: "USER_RESET_PASSWORD",
|
|
741
|
-
label: "
|
|
761
|
+
label: "Password reset triggered",
|
|
742
762
|
severity: "medium"
|
|
743
763
|
},
|
|
744
764
|
{
|
|
745
765
|
key: "USER_DEACTIVATE",
|
|
746
|
-
label: "User
|
|
766
|
+
label: "User deactivated",
|
|
747
767
|
severity: "high"
|
|
748
768
|
},
|
|
749
769
|
{
|
|
750
770
|
key: "USER_REASSIGN_ADMIN",
|
|
751
|
-
label: "Admin
|
|
771
|
+
label: "Admin role transferred",
|
|
752
772
|
severity: "high"
|
|
753
773
|
},
|
|
754
774
|
{
|
|
755
775
|
key: "PLATFORM_EMAIL_PROVIDER_CREATE",
|
|
756
|
-
label: "
|
|
776
|
+
label: "Platform sender created",
|
|
757
777
|
severity: "medium"
|
|
758
778
|
},
|
|
759
779
|
{
|
|
760
780
|
key: "PLATFORM_EMAIL_PROVIDER_UPDATE",
|
|
761
|
-
label: "
|
|
781
|
+
label: "Platform sender changed",
|
|
762
782
|
severity: "medium"
|
|
763
783
|
},
|
|
764
784
|
{
|
|
765
785
|
key: "PLATFORM_EMAIL_PROVIDER_DELETE",
|
|
766
|
-
label: "
|
|
786
|
+
label: "Platform sender deleted",
|
|
767
787
|
severity: "medium"
|
|
768
788
|
},
|
|
769
789
|
{
|
|
770
790
|
key: "PLATFORM_EMAIL_PROVIDER_TEST",
|
|
771
|
-
label: "
|
|
791
|
+
label: "Platform sender test send",
|
|
772
792
|
severity: "low"
|
|
773
793
|
},
|
|
774
794
|
{
|
|
775
795
|
key: "EMAIL_HISTORY_DELETE",
|
|
776
|
-
label: "
|
|
796
|
+
label: "Platform email removed from history",
|
|
777
797
|
severity: "medium"
|
|
778
798
|
},
|
|
779
799
|
{
|
|
780
800
|
key: "EMAIL_HISTORY_RESEND",
|
|
781
|
-
label: "
|
|
801
|
+
label: "Platform email resent",
|
|
782
802
|
severity: "low"
|
|
783
803
|
}
|
|
784
804
|
]
|
|
@@ -898,7 +918,7 @@ var AdminManifestModule = class _AdminManifestModule {
|
|
|
898
918
|
];
|
|
899
919
|
if (includeManifestController) {
|
|
900
920
|
if (options.guards === void 0) {
|
|
901
|
-
throw new Error("AdminManifestModule.forRoot: `guards`
|
|
921
|
+
throw new Error("AdminManifestModule.forRoot: `guards` is required when `includeManifestController` is enabled (default `true`). Set `guards: [JwtAuthGuard, SuperAdminGuard]` (or equivalent) \u2014 or `guards: []` to deliberately leave the endpoint unauthenticated. Alternative: `includeManifestController: false` plus registering your own wrapping controller.");
|
|
902
922
|
}
|
|
903
923
|
controllers.push(buildManifestController(options.guards, options.reloadGuards ?? []));
|
|
904
924
|
}
|
|
@@ -1135,6 +1155,7 @@ AdminStatsModule = _ts_decorate11([
|
|
|
1135
1155
|
// src/admin/admin-resources.module.ts
|
|
1136
1156
|
import { Body, Controller as Controller4, Get as Get4, Inject as Inject9, Injectable as Injectable7, Module as Module4, NotFoundException, Param, Post as Post2, Query, Req, UseGuards as UseGuards3 } from "@nestjs/common";
|
|
1137
1157
|
import { IsOptional, IsString, MaxLength } from "class-validator";
|
|
1158
|
+
import { BILLING_ERROR_CODES } from "@saasicat/types";
|
|
1138
1159
|
function _ts_decorate12(decorators, target, key, desc) {
|
|
1139
1160
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1140
1161
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -1180,7 +1201,15 @@ var AdminResourcesService = class {
|
|
|
1180
1201
|
}
|
|
1181
1202
|
async getTenantDetail(slug) {
|
|
1182
1203
|
const tenant = await this.resources.getTenantDetail(slug);
|
|
1183
|
-
if (!tenant)
|
|
1204
|
+
if (!tenant) {
|
|
1205
|
+
throw new NotFoundException({
|
|
1206
|
+
code: BILLING_ERROR_CODES.TENANT_NOT_FOUND,
|
|
1207
|
+
message: `Tenant ${slug} not found`,
|
|
1208
|
+
params: {
|
|
1209
|
+
slug
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1184
1213
|
return tenant;
|
|
1185
1214
|
}
|
|
1186
1215
|
listUsers(filter) {
|
|
@@ -1206,7 +1235,15 @@ var AdminResourcesService = class {
|
|
|
1206
1235
|
}
|
|
1207
1236
|
async setTenantActive(slug, active, subscriptionStatus, actor, audit) {
|
|
1208
1237
|
const result = await this.resources.setTenantActive(slug, active, subscriptionStatus);
|
|
1209
|
-
if (!result)
|
|
1238
|
+
if (!result) {
|
|
1239
|
+
throw new NotFoundException({
|
|
1240
|
+
code: BILLING_ERROR_CODES.TENANT_NOT_FOUND,
|
|
1241
|
+
message: `Tenant ${slug} not found`,
|
|
1242
|
+
params: {
|
|
1243
|
+
slug
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1210
1247
|
await this.audit.log({
|
|
1211
1248
|
actor,
|
|
1212
1249
|
entity: "Tenant",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DiscoverySnapshotNotFoundError,
|
|
3
3
|
loadDiscoverySnapshotFromFile
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-74SCJ4HF.js";
|
|
5
5
|
import {
|
|
6
6
|
ADVISORY_STRICT_MODE_CODES,
|
|
7
7
|
validateBundleDraft,
|
|
8
8
|
validatePlanDraft
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-LXUBCLRK.js";
|
|
10
10
|
import {
|
|
11
11
|
__name
|
|
12
12
|
} from "./chunk-SHUYVCID.js";
|
|
@@ -66,9 +66,9 @@ function runPreflight(input) {
|
|
|
66
66
|
__name(runPreflight, "runPreflight");
|
|
67
67
|
function formatPreflightReport(report) {
|
|
68
68
|
const lines = [];
|
|
69
|
-
lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7
|
|
69
|
+
lines.push(`Preflight (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
|
|
70
70
|
if (report.findings.length === 0) {
|
|
71
|
-
lines.push(" \u2713
|
|
71
|
+
lines.push(" \u2713 No strict-mode violations found.");
|
|
72
72
|
return lines.join("\n");
|
|
73
73
|
}
|
|
74
74
|
let lastEntity = "";
|
|
@@ -138,9 +138,9 @@ function validateSeedAgainstSnapshot(input) {
|
|
|
138
138
|
__name(validateSeedAgainstSnapshot, "validateSeedAgainstSnapshot");
|
|
139
139
|
function formatSeedGateReport(report) {
|
|
140
140
|
const lines = [];
|
|
141
|
-
lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7
|
|
141
|
+
lines.push(`Seed-Gate (Status: ${report.overall.toUpperCase()})`, ` Plans: ${report.counts.planFindings} \xB7 Bundles: ${report.counts.bundleFindings} \xB7 Total: ${report.counts.total}`, "");
|
|
142
142
|
if (report.findings.length === 0) {
|
|
143
|
-
lines.push(" \u2713
|
|
143
|
+
lines.push(" \u2713 Every seeded plan/bundle feature is discovered (and approved where checked).");
|
|
144
144
|
return lines.join("\n");
|
|
145
145
|
}
|
|
146
146
|
let lastEntity = "";
|
|
@@ -174,10 +174,10 @@ function runSeedGateFromFile(options) {
|
|
|
174
174
|
} catch (err) {
|
|
175
175
|
if (err instanceof DiscoverySnapshotNotFoundError) {
|
|
176
176
|
if (mode === "blocking") {
|
|
177
|
-
error(`[seed-gate]
|
|
177
|
+
error(`[seed-gate] ERROR: no discovery snapshot at ${options.snapshotPath} \u2014 required in blocking mode (run the headless scan before seeding, #23).`);
|
|
178
178
|
return exit(4);
|
|
179
179
|
}
|
|
180
|
-
warn(`[seed-gate]
|
|
180
|
+
warn(`[seed-gate] No discovery snapshot at ${options.snapshotPath} \u2014 skipped (report-only).`);
|
|
181
181
|
return null;
|
|
182
182
|
}
|
|
183
183
|
throw err;
|
|
@@ -191,10 +191,10 @@ function runSeedGateFromFile(options) {
|
|
|
191
191
|
log(formatSeedGateReport(report));
|
|
192
192
|
if (report.overall === "error") {
|
|
193
193
|
if (mode === "blocking") {
|
|
194
|
-
error("[seed-gate] blocking:
|
|
194
|
+
error("[seed-gate] blocking: violations found \u2014 aborting the seed (#23).");
|
|
195
195
|
return exit(seedGateExitCode(report));
|
|
196
196
|
}
|
|
197
|
-
warn("[seed-gate] report-only:
|
|
197
|
+
warn("[seed-gate] report-only: violations found, seeding continues anyway.");
|
|
198
198
|
}
|
|
199
199
|
return report;
|
|
200
200
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendImplicitDiscountLineItem
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BSK6YBLI.js";
|
|
4
4
|
import {
|
|
5
5
|
SUBSCRIPTION_CONTRACT_REPOSITORY_TOKEN
|
|
6
6
|
} from "./chunk-NYLON2VC.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
// src/subscription-contract/subscription-contract.service.ts
|
|
15
15
|
import { ConflictException, Inject, Injectable, NotFoundException, UnprocessableEntityException } from "@nestjs/common";
|
|
16
|
+
import { CONTRACT_ERROR_CODES } from "@saasicat/types";
|
|
16
17
|
function _ts_decorate(decorators, target, key, desc) {
|
|
17
18
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
18
19
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -44,7 +45,13 @@ var SubscriptionContractService = class {
|
|
|
44
45
|
async getById(contractId) {
|
|
45
46
|
const row = await this.repo.findById(contractId);
|
|
46
47
|
if (!row) {
|
|
47
|
-
throw new NotFoundException(
|
|
48
|
+
throw new NotFoundException({
|
|
49
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_NOT_FOUND,
|
|
50
|
+
message: `SubscriptionContract '${contractId}' not found`,
|
|
51
|
+
params: {
|
|
52
|
+
contractId
|
|
53
|
+
}
|
|
54
|
+
});
|
|
48
55
|
}
|
|
49
56
|
return row;
|
|
50
57
|
}
|
|
@@ -54,7 +61,14 @@ var SubscriptionContractService = class {
|
|
|
54
61
|
async getActiveInvoiceSnapshotForTenant(tenantId, asOf = /* @__PURE__ */ new Date()) {
|
|
55
62
|
const contract = await this.repo.findActiveByTenantId(tenantId, asOf);
|
|
56
63
|
if (!contract) {
|
|
57
|
-
throw new NotFoundException(
|
|
64
|
+
throw new NotFoundException({
|
|
65
|
+
code: CONTRACT_ERROR_CODES.NO_ACTIVE_SUBSCRIPTION_CONTRACT,
|
|
66
|
+
message: `No active subscription contract for tenant ${tenantId}`,
|
|
67
|
+
params: {
|
|
68
|
+
tenantId,
|
|
69
|
+
asOf: asOf.toISOString()
|
|
70
|
+
}
|
|
71
|
+
});
|
|
58
72
|
}
|
|
59
73
|
return subscriptionContractToInvoiceSnapshot(contract);
|
|
60
74
|
}
|
|
@@ -87,7 +101,14 @@ var SubscriptionContractService = class {
|
|
|
87
101
|
}
|
|
88
102
|
createDataFromOffer(offer, options) {
|
|
89
103
|
if (offer.status !== "consumed") {
|
|
90
|
-
throw new ConflictException(
|
|
104
|
+
throw new ConflictException({
|
|
105
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_NOT_CONSUMED,
|
|
106
|
+
message: `CheckoutOffer '${offer.id}' must be consumed before the contract is created`,
|
|
107
|
+
params: {
|
|
108
|
+
offerId: offer.id,
|
|
109
|
+
status: offer.status
|
|
110
|
+
}
|
|
111
|
+
});
|
|
91
112
|
}
|
|
92
113
|
const lineItems = this.lineItemsFromOffer(offer);
|
|
93
114
|
return {
|
|
@@ -120,8 +141,8 @@ var SubscriptionContractService = class {
|
|
|
120
141
|
const source = offer.lineItems ?? [];
|
|
121
142
|
if (source.length === 0) {
|
|
122
143
|
throw new UnprocessableEntityException({
|
|
123
|
-
code:
|
|
124
|
-
message: "
|
|
144
|
+
code: CONTRACT_ERROR_CODES.CHECKOUT_OFFER_LINE_ITEMS_REQUIRED,
|
|
145
|
+
message: "A checkout offer can yield only one contract, and only once its line items are frozen."
|
|
125
146
|
});
|
|
126
147
|
}
|
|
127
148
|
return appendImplicitDiscountLineItem({
|
|
@@ -159,8 +180,11 @@ var SubscriptionContractService = class {
|
|
|
159
180
|
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
160
181
|
if (Number.isNaN(date.getTime())) {
|
|
161
182
|
throw new UnprocessableEntityException({
|
|
162
|
-
code:
|
|
163
|
-
message: `${field}
|
|
183
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_DATE,
|
|
184
|
+
message: `${field} must be a valid date.`,
|
|
185
|
+
params: {
|
|
186
|
+
field
|
|
187
|
+
}
|
|
164
188
|
});
|
|
165
189
|
}
|
|
166
190
|
return date;
|
|
@@ -180,32 +204,39 @@ var SubscriptionContractService = class {
|
|
|
180
204
|
assertCreateData(data) {
|
|
181
205
|
if (data.lineItems.length === 0) {
|
|
182
206
|
throw new UnprocessableEntityException({
|
|
183
|
-
code:
|
|
184
|
-
message: "
|
|
207
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_LINE_ITEMS_REQUIRED,
|
|
208
|
+
message: "A subscription contract requires at least one line item."
|
|
185
209
|
});
|
|
186
210
|
}
|
|
187
211
|
if (data.effectiveUntil && data.effectiveUntil <= data.effectiveFrom) {
|
|
188
212
|
throw new UnprocessableEntityException({
|
|
189
|
-
code:
|
|
190
|
-
message: "effectiveUntil
|
|
213
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_INVALID_WINDOW,
|
|
214
|
+
message: "effectiveUntil must be after effectiveFrom."
|
|
191
215
|
});
|
|
192
216
|
}
|
|
193
217
|
const planLineItems = data.lineItems.filter((item) => item.kind === "plan");
|
|
194
218
|
if (planLineItems.length !== 1) {
|
|
195
219
|
throw new UnprocessableEntityException({
|
|
196
|
-
code:
|
|
197
|
-
message: "
|
|
220
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_PLAN_LINE_ITEM_REQUIRED,
|
|
221
|
+
message: "A subscription contract requires exactly one plan base item."
|
|
198
222
|
});
|
|
199
223
|
}
|
|
200
224
|
}
|
|
201
225
|
assertTerminable(existing, data) {
|
|
202
226
|
if (existing.status === "terminated" || existing.status === "superseded") {
|
|
203
|
-
throw new ConflictException(
|
|
227
|
+
throw new ConflictException({
|
|
228
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_ALREADY_CLOSED,
|
|
229
|
+
message: `SubscriptionContract '${existing.id}' is already closed`,
|
|
230
|
+
params: {
|
|
231
|
+
contractId: existing.id,
|
|
232
|
+
status: existing.status
|
|
233
|
+
}
|
|
234
|
+
});
|
|
204
235
|
}
|
|
205
236
|
if (data.effectiveUntil <= existing.effectiveFrom) {
|
|
206
237
|
throw new UnprocessableEntityException({
|
|
207
|
-
code:
|
|
208
|
-
message: "effectiveUntil
|
|
238
|
+
code: CONTRACT_ERROR_CODES.SUBSCRIPTION_CONTRACT_TERMINATION_BEFORE_START,
|
|
239
|
+
message: "effectiveUntil must be after the effectiveFrom of the contract."
|
|
209
240
|
});
|
|
210
241
|
}
|
|
211
242
|
}
|
|
@@ -40,7 +40,7 @@ function createDiscountLineItem(input, discountNet) {
|
|
|
40
40
|
kind: "discount",
|
|
41
41
|
sourceKey: promoCode?.code ?? firstPromotion?.id ?? "price-discount",
|
|
42
42
|
sourceVersionId: null,
|
|
43
|
-
titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "
|
|
43
|
+
titleSnapshot: promoCode?.label ?? firstPromotion?.label ?? "Discount",
|
|
44
44
|
descriptionSnapshot: promoCode ? `Promo-Code ${promoCode.code}` : null,
|
|
45
45
|
quantity: 1,
|
|
46
46
|
unit: null,
|
|
@@ -152,7 +152,7 @@ __name(findPlan, "findPlan");
|
|
|
152
152
|
function getPlanOrThrow(catalog, planId) {
|
|
153
153
|
const plan = findPlan(catalog, planId);
|
|
154
154
|
if (!plan) {
|
|
155
|
-
throw new Error(`Plan "${planId}"
|
|
155
|
+
throw new Error(`Plan "${planId}" not found in the catalog (${catalog.projectKey})`);
|
|
156
156
|
}
|
|
157
157
|
return plan;
|
|
158
158
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
PROMO_SUBSCRIPTION_LOOKUP_TOKEN,
|
|
9
9
|
PROMO_TRANSACTION_RUNNER_TOKEN,
|
|
10
10
|
PromoCodesService
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QLJHIIOY.js";
|
|
12
12
|
import {
|
|
13
13
|
AdminAuditService
|
|
14
14
|
} from "./chunk-E56W4U2P.js";
|
|
@@ -58,7 +58,7 @@ var PromoCodeExpirer = class _PromoCodeExpirer {
|
|
|
58
58
|
const codes = await this.promoRepo.expireDueCodes(now);
|
|
59
59
|
const redemptions = await this.redemptionRepo.expireDueRedemptions(now);
|
|
60
60
|
if (codes > 0 || redemptions > 0) {
|
|
61
|
-
this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions}
|
|
61
|
+
this.logger.log(`PromoCodeExpirer: ${codes} Codes, ${redemptions} redemptions expired.`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
};
|
|
@@ -84,6 +84,7 @@ PromoCodeExpirer = _ts_decorate([
|
|
|
84
84
|
|
|
85
85
|
// src/promo/rate-limit.guard.ts
|
|
86
86
|
import { HttpException, HttpStatus, Injectable as Injectable2 } from "@nestjs/common";
|
|
87
|
+
import { REGISTRATION_ERROR_CODES } from "@saasicat/types";
|
|
87
88
|
function _ts_decorate2(decorators, target, key, desc) {
|
|
88
89
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
89
90
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -95,6 +96,15 @@ var IP_WINDOW_MS = 6e4;
|
|
|
95
96
|
var IP_LIMIT = 20;
|
|
96
97
|
var SESSION_WINDOW_MS = 60 * 6e4;
|
|
97
98
|
var SESSION_LIMIT = 50;
|
|
99
|
+
function rateLimited() {
|
|
100
|
+
return new HttpException({
|
|
101
|
+
valid: false,
|
|
102
|
+
// Superseded by `code` — kept until all consumers read `code`.
|
|
103
|
+
reason: "RATE_LIMITED",
|
|
104
|
+
code: REGISTRATION_ERROR_CODES.RATE_LIMITED
|
|
105
|
+
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
106
|
+
}
|
|
107
|
+
__name(rateLimited, "rateLimited");
|
|
98
108
|
var PromoCodeRateLimitGuard = class {
|
|
99
109
|
static {
|
|
100
110
|
__name(this, "PromoCodeRateLimitGuard");
|
|
@@ -106,16 +116,10 @@ var PromoCodeRateLimitGuard = class {
|
|
|
106
116
|
const ipKey = ipFingerprint(req);
|
|
107
117
|
const sessionKey = req.user?.id ?? null;
|
|
108
118
|
if (this.exceeded(this.ipBuckets, ipKey, IP_WINDOW_MS, IP_LIMIT)) {
|
|
109
|
-
throw
|
|
110
|
-
valid: false,
|
|
111
|
-
reason: "RATE_LIMITED"
|
|
112
|
-
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
119
|
+
throw rateLimited();
|
|
113
120
|
}
|
|
114
121
|
if (sessionKey && this.exceeded(this.sessionBuckets, sessionKey, SESSION_WINDOW_MS, SESSION_LIMIT)) {
|
|
115
|
-
throw
|
|
116
|
-
valid: false,
|
|
117
|
-
reason: "RATE_LIMITED"
|
|
118
|
-
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
122
|
+
throw rateLimited();
|
|
119
123
|
}
|
|
120
124
|
return true;
|
|
121
125
|
}
|
|
@@ -179,21 +183,21 @@ var PreviewPromoCodeDto = class {
|
|
|
179
183
|
_ts_decorate3([
|
|
180
184
|
IsString(),
|
|
181
185
|
Matches(CODE_PATTERN, {
|
|
182
|
-
message: 'code
|
|
186
|
+
message: 'code must consist of A\u2013Z, 0\u20139, "-" and "_" (4\u201332 characters)'
|
|
183
187
|
}),
|
|
184
188
|
_ts_metadata2("design:type", String)
|
|
185
189
|
], PreviewPromoCodeDto.prototype, "code", void 0);
|
|
186
190
|
_ts_decorate3([
|
|
187
191
|
IsString(),
|
|
188
192
|
Matches(PLAN_OR_CYCLE_PATTERN, {
|
|
189
|
-
message: "plan
|
|
193
|
+
message: "plan must be SCREAMING_SNAKE_CASE"
|
|
190
194
|
}),
|
|
191
195
|
_ts_metadata2("design:type", String)
|
|
192
196
|
], PreviewPromoCodeDto.prototype, "plan", void 0);
|
|
193
197
|
_ts_decorate3([
|
|
194
198
|
IsString(),
|
|
195
199
|
Matches(PLAN_OR_CYCLE_PATTERN, {
|
|
196
|
-
message: "billingCycle
|
|
200
|
+
message: "billingCycle must be SCREAMING_SNAKE_CASE (e.g. MONTHLY, YEARLY)"
|
|
197
201
|
}),
|
|
198
202
|
_ts_metadata2("design:type", String)
|
|
199
203
|
], PreviewPromoCodeDto.prototype, "billingCycle", void 0);
|