@saasicat/cli 1.0.0-rc.2 → 1.0.0-rc.20

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/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { MfaService, AdminAuditService, AdminManifestService, DiscoverySnapshot, ProviderSpec, DiscoveryScanner } from '@saasicat/nest';
1
+ import { MfaService, AdminAuditService, AdminManifestService, DiscoverySnapshot, IssuerIdentityInspector, ProviderSpec, DiscoveryScanner } from '@saasicat/nest';
2
2
  import { UserPort, PlatformUserDto, AuditQueryPort, AuditEntry, AdminManifest, ManifestAccessPort, PlanCatalog, UserManagementPort } from '@saasicat/core';
3
3
  import { Type, DynamicModule } from '@nestjs/common';
4
4
  import * as TypeScript from 'typescript';
@@ -311,6 +311,28 @@ declare class AdminManifestDoctorCheck implements DoctorCheck {
311
311
  constructor(manifest: AdminManifestService);
312
312
  run(): Promise<DoctorCheckResult>;
313
313
  }
314
+ /**
315
+ * The issuer identity, and what changing it would cost.
316
+ *
317
+ * A start compares the issuer in `config/saas.yaml` with the identity the
318
+ * installation recorded and refuses an undeclared difference, because moving a
319
+ * contract to another legal entity is a transfer and not an edit of a setting.
320
+ * This asks the same question without acting on it and says, while nothing has
321
+ * moved, what a change would cost.
322
+ *
323
+ * It does NOT turn a refusal into a report where the same application also
324
+ * mounts the platform's boot check: that check runs at `init()`, so the CLI
325
+ * process carrying it is refused before any command runs — with the same
326
+ * message, which is the point. The `refused` branch below is for a diagnostic
327
+ * that runs without the hook.
328
+ */
329
+ declare class IssuerIdentityDoctorCheck implements DoctorCheck {
330
+ private readonly issuer;
331
+ readonly id = "platform.issuer-identity";
332
+ readonly label = "Issuer identity against the recorded one";
333
+ constructor(issuer: IssuerIdentityInspector);
334
+ run(): Promise<DoctorCheckResult>;
335
+ }
314
336
  /**
315
337
  * Default list that consumers can spread in `CliContextModule.forRoot({ doctorChecks })`:
316
338
  *
@@ -467,6 +489,15 @@ interface MissingBlockAttribute {
467
489
  /** For `map`: what the consumer maps to instead. */
468
490
  actual?: string;
469
491
  }
492
+ /**
493
+ * A relation that deletes a record the platform keeps past its tenant, together
494
+ * with the tenant.
495
+ */
496
+ interface TenantCascade {
497
+ model: string;
498
+ /** The relation field that cascades, e.g. `tenant`. */
499
+ field: string;
500
+ }
470
501
  interface SchemaCheckReport {
471
502
  /** Platform models the consumer does not carry — informational. */
472
503
  absentModels: string[];
@@ -480,6 +511,11 @@ interface SchemaCheckReport {
480
511
  * `@@index`, `@@unique`, and a diverging `@@map`.
481
512
  */
482
513
  missingBlockAttributes: MissingBlockAttribute[];
514
+ /**
515
+ * Relations that cascade from the tenant onto a model the platform keeps
516
+ * after the tenant is gone, such as a contract.
517
+ */
518
+ tenantCascades: TenantCascade[];
483
519
  /** Models present in both schemas, i.e. actually compared. */
484
520
  checkedModelCount: number;
485
521
  /** Enums present in both schemas, i.e. actually compared. */
@@ -514,11 +550,24 @@ declare function parseSchema(schema: string): ParsedSchema;
514
550
  * the check.
515
551
  */
516
552
  declare function breaksContract(attribute: MissingBlockAttribute): boolean;
553
+ /**
554
+ * The platform models that outlive their tenant: the ones whose fragment
555
+ * declares `tenantId` and deliberately comments no relation to `Tenant`.
556
+ *
557
+ * Read off the fragments rather than listed, so the next model kept past its
558
+ * tenant is covered by being written that way. Every other model with a
559
+ * `tenantId` ships the pointer a consumer enables, cascade and all.
560
+ */
561
+ declare function modelsKeptPastTheTenant(specSchema: string): string[];
517
562
  /**
518
563
  * Compares a consumer schema against the canonical fragments. `specSchema` is
519
564
  * the concatenation of the fragments the check should cover.
565
+ *
566
+ * @param knownModels Every model the shipped fragments declare, where the spec
567
+ * passed in is a narrowed selection of them (`schema check --fragments=…`).
568
+ * Omitted, the spec is taken to be the whole of it.
520
569
  */
521
- declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
570
+ declare function checkSchema(specSchema: string, appSchema: string, knownModels?: ReadonlySet<string>): SchemaCheckReport;
522
571
 
523
572
  /** Marks the block this tool appends, and lets it recognise its own work. */
524
573
  declare const CONSTRAINTS_MARKER = "-- saasicat:constraints";
@@ -719,9 +768,9 @@ declare function assertModelsExist(declaredModels: readonly string[], models: Fk
719
768
 
720
769
  /** What the caller asked for. */
721
770
  interface InitOptions {
722
- /** The catalogue this app administers. Also the storage-key prefix. */
723
- projectKey: string;
724
- /** Human name in the manifest and the YAML. Defaults to `projectKey`. */
771
+ /** Slug of the application: npm package name, storage-key prefix, id prefix. */
772
+ appKey: string;
773
+ /** Human name in the manifest and the YAML. Defaults to `appKey`. */
725
774
  appName?: string;
726
775
  /** Admin API prefix, e.g. `/api/v1/admin`. */
727
776
  apiBase?: string;
@@ -854,8 +903,7 @@ declare function judgeModuleResolution(value: string | null): ModuleResolutionVe
854
903
  */
855
904
  declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
856
905
 
857
- /** The pattern `plan-catalog.schema.json` puts on `projectKey`. */
858
- declare function projectKeyPattern(): RegExp;
906
+ declare function appKeyPattern(): RegExp;
859
907
  /** The pattern it puts on the keys inside a plan's `quotas` object. */
860
908
  declare function quotaKeyPattern(): RegExp;
861
909
  /** How many quotas a plan must declare — 1 today, and read rather than assumed. */
@@ -866,10 +914,25 @@ declare function minimumQuotasPerPlan(): number;
866
914
  * The message carries the pattern rather than a prose paraphrase, because the
867
915
  * paraphrase is what goes stale.
868
916
  */
869
- declare function assertValidProjectKey(projectKey: string): void;
917
+ declare function assertValidAppKey(appKey: string): void;
870
918
  /** The same, for a quota key. `additionalProperties: false` makes it a hard rule. */
871
919
  declare function assertValidQuotaKey(quotaKey: string): void;
872
920
 
921
+ interface WrittenSetting {
922
+ /** Dotted path as it reads in the file, e.g. `tenantBilling.cancellationNoticeDays.monthly`. */
923
+ key: string;
924
+ /** The value as JSON — `0`, `[]`, `"EUR"` — so an empty list is visible as one. */
925
+ value: string;
926
+ }
927
+ /**
928
+ * The settings a generated `config/saas.yaml` carries.
929
+ *
930
+ * Loaded with the platform's own loader, not a YAML parse: a document `init`
931
+ * writes that the platform would refuse is a bug worth failing the generation
932
+ * for, rather than one the first boot reports after every file exists.
933
+ */
934
+ declare function settingsWrittenTo(catalogYaml: string, source?: string): WrittenSetting[];
935
+
873
936
  /** One entry of the move table: where a file was, and where it went. */
874
937
  interface MoveTable {
875
938
  readonly moves: Readonly<Record<string, string>>;
@@ -981,6 +1044,147 @@ interface ManifestRewriteOptions {
981
1044
  }
982
1045
  declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
983
1046
 
1047
+ interface ProjectKeyResult {
1048
+ readonly text: string;
1049
+ /** How many occurrences were taken out. */
1050
+ readonly rewritten: number;
1051
+ /**
1052
+ * 1-based line numbers of the occurrences left in place.
1053
+ *
1054
+ * Reported rather than removed: the codemod could not tell them from a
1055
+ * consumer's own field, and a wrong deletion is worse than a named one.
1056
+ */
1057
+ readonly undecided: readonly number[];
1058
+ }
1059
+ /**
1060
+ * Rewrites one source file.
1061
+ *
1062
+ * `yaml` switches to the config form: there the field is a top-level key in a
1063
+ * file the platform owns the schema of, so it is decidable without an anchor.
1064
+ */
1065
+ declare function removeProjectKey(text: string, kind?: 'source' | 'yaml'): ProjectKeyResult;
1066
+
1067
+ /**
1068
+ * The settings that belong in `config/saas.yaml#tenantBilling`, read off the
1069
+ * schema that defines them.
1070
+ *
1071
+ * Not a list here, and not a copy of the one in `@saasicat/nest`: both derive
1072
+ * from the same schema, so the day a third setting moves into that block, this
1073
+ * codemod names it and the module refuses it without either being edited.
1074
+ */
1075
+ declare const SETTINGS_THAT_MOVED: readonly string[];
1076
+ type MovedSetting = string;
1077
+ interface MovedSettingOccurrence {
1078
+ /** Which setting it is, so the report can say where it goes. */
1079
+ readonly setting: MovedSetting;
1080
+ /** 1-based line number. */
1081
+ readonly line: number;
1082
+ }
1083
+ interface MovedSettingsResult {
1084
+ readonly occurrences: readonly MovedSettingOccurrence[];
1085
+ }
1086
+ /**
1087
+ * Files a moved setting can actually be passed in.
1088
+ *
1089
+ * The codemod walk includes Markdown, and both consumers keep large
1090
+ * documentation folders — an upgrade note that mentions `cancellationNoticeDays`
1091
+ * would land in the report beside the line somebody has to change, and a report
1092
+ * that mixes the two is one nobody reads twice. Prose cannot pass a module
1093
+ * option, so prose is not scanned.
1094
+ */
1095
+ declare const SCANNED_FOR_MOVED_SETTINGS: RegExp;
1096
+ /**
1097
+ * Every occurrence of a moved setting in one source file.
1098
+ *
1099
+ * Matched on word boundaries alone: a longer name that contains it
1100
+ * (`cancellationNoticeDaysV2`) is not reported, and everything else is —
1101
+ * including a mention in a comment or a string.
1102
+ *
1103
+ * That last part is deliberate, and it is the opposite trade from the one this
1104
+ * comment used to claim. Requiring a colon would read as "only a property",
1105
+ * and it would then miss `{ cancellationNoticeDays }` and
1106
+ * `const { cancellationNoticeDays } = options` — two ordinary ways to pass the
1107
+ * same option. Telling a comment from code needs the grammar, which this does
1108
+ * not have. So it over-reports inside code, where the cost is a glance, rather
1109
+ * than under-reporting, where the cost is somebody not learning that their
1110
+ * value is about to stop being read.
1111
+ *
1112
+ * A property access (`config.cancellationNoticeDays`) is reported too: reading
1113
+ * the value back from module options is the same migration, one step further
1114
+ * along.
1115
+ */
1116
+ declare function findMovedSettings(text: string): MovedSettingsResult;
1117
+ /**
1118
+ * Where a setting goes, for the report.
1119
+ *
1120
+ * One sentence per setting rather than one for both: they end up in the same
1121
+ * block and mean different things, and "move these two to the file" is the
1122
+ * instruction people follow halfway.
1123
+ */
1124
+ declare const WHERE_IT_GOES: Record<string, string>;
1125
+
1126
+ /** Which files a `dbCatalog` can be passed in: code, not prose. */
1127
+ declare const SCANNED_FOR_DB_CATALOG: RegExp;
1128
+ type DbCatalogShape = 'values' | 'mixed' | 'reference';
1129
+ interface DbCatalogOccurrence {
1130
+ /** 1-based line of the `dbCatalog:` property. */
1131
+ readonly line: number;
1132
+ /**
1133
+ * `values` — an object literal with no `path`: the old shape whole.
1134
+ * `mixed` — a `path` with something beside it that the option does not
1135
+ * take: an upgrade that stopped halfway, and the platform refuses it too.
1136
+ * `reference` — anything else on the right of the colon: a variable, a
1137
+ * call, a spread. This cannot see what it carries, so it is named for a
1138
+ * person to look at rather than passed over.
1139
+ */
1140
+ readonly shape: DbCatalogShape;
1141
+ /**
1142
+ * The members that are not what the option takes — every one of a
1143
+ * `values` block, the ones left beside the path of a `mixed` one, and
1144
+ * nothing for a `reference`. A spread is listed as `...name`, because
1145
+ * what it carries is decided elsewhere.
1146
+ */
1147
+ readonly leftovers: readonly string[];
1148
+ }
1149
+ interface DbCatalogResult {
1150
+ readonly occurrences: readonly DbCatalogOccurrence[];
1151
+ }
1152
+ /**
1153
+ * Every `dbCatalog:` property in one source file, with what stands to its right.
1154
+ *
1155
+ * A property in code, which means the name followed by a colon, outside any
1156
+ * comment or string. The other codemod in this family reports every
1157
+ * word-boundary mention of a setting, including one in a comment, on the
1158
+ * reasoning that over-reporting inside code costs a glance. That reasoning
1159
+ * does not carry here: `saasicat init` writes the sentence "pass `dbCatalog`
1160
+ * instead" into every generated `app.module.ts`, so a mention is the normal
1161
+ * case and a report of it would be noise on every upgrade — and a block
1162
+ * commented out, or quoted as a sample, is migration work that does not
1163
+ * exist. An OPTIONAL type member (`dbCatalog?:`) is not a property either: the
1164
+ * `?` stands where the colon would. A required one (`dbCatalog: DbCatalogOptions`)
1165
+ * is the same tokens as a value passed in from elsewhere, and is reported as a
1166
+ * `reference` for that reason — a glance, which is what `reference` is for. A
1167
+ * shorthand `{ dbCatalog }` is not seen — the value it carries is elsewhere,
1168
+ * and the module's refusal names it at boot.
1169
+ *
1170
+ * What counts as migrated is read off `DB_CATALOG_MEMBERS`, the list the
1171
+ * platform's own refusal reads, so the two cannot disagree about a block.
1172
+ */
1173
+ declare function findDbCatalogBlocks(text: string): DbCatalogResult;
1174
+ /**
1175
+ * What is wrong with one occurrence, in the words the report uses.
1176
+ *
1177
+ * Beside the shapes rather than in the printer, because one of them has no
1178
+ * leftovers to name: a `values` block whose only member is one the option
1179
+ * takes — `dbCatalog: { env: process.env }` — or one emptied mid-edit is
1180
+ * genuinely broken and genuinely refused at boot, but "carries the values:"
1181
+ * with nothing after it names a file to go and look at without saying what to
1182
+ * look for.
1183
+ */
1184
+ declare function describeDbCatalogOccurrence(occurrence: Pick<DbCatalogOccurrence, 'shape' | 'leftovers'>): string;
1185
+ /** What to write instead, for the report. */
1186
+ declare const WHERE_DB_CATALOG_GOES: string;
1187
+
984
1188
  interface PatchAppModuleOptions {
985
1189
  /** Import specifier for the persistence bundle, or null when not generated. */
986
1190
  persistenceImport: string | null;
@@ -1048,15 +1252,18 @@ interface CliContextModuleOptions {
1048
1252
  * — `<app> doctor` runs but returns no content. Consumers register
1049
1253
  * project-specific checks.
1050
1254
  *
1051
- * Note: if `defaultDoctorChecks: true`, the 4 platform standard checks
1052
- * (PlanCatalog/Discovery/UserPort/AdminManifest) are registered **in
1053
- * addition** to this list.
1255
+ * Note: if `defaultDoctorChecks: true`, the platform's own checks
1256
+ * `PLATFORM_DOCTOR_CHECK_PROVIDERS` are registered **in addition** to
1257
+ * this list. They are named there rather than here: a second list of them
1258
+ * in prose is the copy that goes stale.
1054
1259
  */
1055
1260
  doctorChecks?: ProviderSpec<DoctorCheck[]>;
1056
1261
  /**
1057
- * Default `false`. If `true`, the platform registers the four standard
1058
- * checks from `PLATFORM_DOCTOR_CHECK_PROVIDERS` in addition to
1059
- * `doctorChecks`. Recommended for all apps that take `<app> doctor` seriously.
1262
+ * Default `false`. If `true`, the platform registers the checks from
1263
+ * `PLATFORM_DOCTOR_CHECK_PROVIDERS` in addition to `doctorChecks`. They
1264
+ * resolve what `SaaSiCatModule` provides, so an application that wires the
1265
+ * platform's modules by hand registers the ones it can serve instead.
1266
+ * Recommended for all apps that take `<app> doctor` seriously.
1060
1267
  */
1061
1268
  defaultDoctorChecks?: boolean;
1062
1269
  /**
@@ -1224,4 +1431,4 @@ declare class UserCommands extends CommandRunner {
1224
1431
  parsePassword(val: string): string;
1225
1432
  }
1226
1433
 
1227
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidProjectKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, projectKeyPattern, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
1434
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, type DbCatalogOccurrence, type DbCatalogResult, type DbCatalogShape, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, IssuerIdentityDoctorCheck, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, type MovedSetting, type MovedSettingOccurrence, type MovedSettingsResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, SCANNED_FOR_DB_CATALOG, SCANNED_FOR_MOVED_SETTINGS, SETTINGS_THAT_MOVED, type SchemaCheckReport, type TenantCascade, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WHERE_DB_CATALOG_GOES, WHERE_IT_GOES, WhoAmIFlow, type WhoAmIResult, type WrittenSetting, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, describeDbCatalogOccurrence, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findDbCatalogBlocks, findFkPointers, findMovedSettings, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, modelsKeptPastTheTenant, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, settingsWrittenTo, stripLineComment, structuralOnly, tablesAddressedBy };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MfaService, AdminAuditService, AdminManifestService, DiscoverySnapshot, ProviderSpec, DiscoveryScanner } from '@saasicat/nest';
1
+ import { MfaService, AdminAuditService, AdminManifestService, DiscoverySnapshot, IssuerIdentityInspector, ProviderSpec, DiscoveryScanner } from '@saasicat/nest';
2
2
  import { UserPort, PlatformUserDto, AuditQueryPort, AuditEntry, AdminManifest, ManifestAccessPort, PlanCatalog, UserManagementPort } from '@saasicat/core';
3
3
  import { Type, DynamicModule } from '@nestjs/common';
4
4
  import * as TypeScript from 'typescript';
@@ -311,6 +311,28 @@ declare class AdminManifestDoctorCheck implements DoctorCheck {
311
311
  constructor(manifest: AdminManifestService);
312
312
  run(): Promise<DoctorCheckResult>;
313
313
  }
314
+ /**
315
+ * The issuer identity, and what changing it would cost.
316
+ *
317
+ * A start compares the issuer in `config/saas.yaml` with the identity the
318
+ * installation recorded and refuses an undeclared difference, because moving a
319
+ * contract to another legal entity is a transfer and not an edit of a setting.
320
+ * This asks the same question without acting on it and says, while nothing has
321
+ * moved, what a change would cost.
322
+ *
323
+ * It does NOT turn a refusal into a report where the same application also
324
+ * mounts the platform's boot check: that check runs at `init()`, so the CLI
325
+ * process carrying it is refused before any command runs — with the same
326
+ * message, which is the point. The `refused` branch below is for a diagnostic
327
+ * that runs without the hook.
328
+ */
329
+ declare class IssuerIdentityDoctorCheck implements DoctorCheck {
330
+ private readonly issuer;
331
+ readonly id = "platform.issuer-identity";
332
+ readonly label = "Issuer identity against the recorded one";
333
+ constructor(issuer: IssuerIdentityInspector);
334
+ run(): Promise<DoctorCheckResult>;
335
+ }
314
336
  /**
315
337
  * Default list that consumers can spread in `CliContextModule.forRoot({ doctorChecks })`:
316
338
  *
@@ -467,6 +489,15 @@ interface MissingBlockAttribute {
467
489
  /** For `map`: what the consumer maps to instead. */
468
490
  actual?: string;
469
491
  }
492
+ /**
493
+ * A relation that deletes a record the platform keeps past its tenant, together
494
+ * with the tenant.
495
+ */
496
+ interface TenantCascade {
497
+ model: string;
498
+ /** The relation field that cascades, e.g. `tenant`. */
499
+ field: string;
500
+ }
470
501
  interface SchemaCheckReport {
471
502
  /** Platform models the consumer does not carry — informational. */
472
503
  absentModels: string[];
@@ -480,6 +511,11 @@ interface SchemaCheckReport {
480
511
  * `@@index`, `@@unique`, and a diverging `@@map`.
481
512
  */
482
513
  missingBlockAttributes: MissingBlockAttribute[];
514
+ /**
515
+ * Relations that cascade from the tenant onto a model the platform keeps
516
+ * after the tenant is gone, such as a contract.
517
+ */
518
+ tenantCascades: TenantCascade[];
483
519
  /** Models present in both schemas, i.e. actually compared. */
484
520
  checkedModelCount: number;
485
521
  /** Enums present in both schemas, i.e. actually compared. */
@@ -514,11 +550,24 @@ declare function parseSchema(schema: string): ParsedSchema;
514
550
  * the check.
515
551
  */
516
552
  declare function breaksContract(attribute: MissingBlockAttribute): boolean;
553
+ /**
554
+ * The platform models that outlive their tenant: the ones whose fragment
555
+ * declares `tenantId` and deliberately comments no relation to `Tenant`.
556
+ *
557
+ * Read off the fragments rather than listed, so the next model kept past its
558
+ * tenant is covered by being written that way. Every other model with a
559
+ * `tenantId` ships the pointer a consumer enables, cascade and all.
560
+ */
561
+ declare function modelsKeptPastTheTenant(specSchema: string): string[];
517
562
  /**
518
563
  * Compares a consumer schema against the canonical fragments. `specSchema` is
519
564
  * the concatenation of the fragments the check should cover.
565
+ *
566
+ * @param knownModels Every model the shipped fragments declare, where the spec
567
+ * passed in is a narrowed selection of them (`schema check --fragments=…`).
568
+ * Omitted, the spec is taken to be the whole of it.
520
569
  */
521
- declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
570
+ declare function checkSchema(specSchema: string, appSchema: string, knownModels?: ReadonlySet<string>): SchemaCheckReport;
522
571
 
523
572
  /** Marks the block this tool appends, and lets it recognise its own work. */
524
573
  declare const CONSTRAINTS_MARKER = "-- saasicat:constraints";
@@ -719,9 +768,9 @@ declare function assertModelsExist(declaredModels: readonly string[], models: Fk
719
768
 
720
769
  /** What the caller asked for. */
721
770
  interface InitOptions {
722
- /** The catalogue this app administers. Also the storage-key prefix. */
723
- projectKey: string;
724
- /** Human name in the manifest and the YAML. Defaults to `projectKey`. */
771
+ /** Slug of the application: npm package name, storage-key prefix, id prefix. */
772
+ appKey: string;
773
+ /** Human name in the manifest and the YAML. Defaults to `appKey`. */
725
774
  appName?: string;
726
775
  /** Admin API prefix, e.g. `/api/v1/admin`. */
727
776
  apiBase?: string;
@@ -854,8 +903,7 @@ declare function judgeModuleResolution(value: string | null): ModuleResolutionVe
854
903
  */
855
904
  declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
856
905
 
857
- /** The pattern `plan-catalog.schema.json` puts on `projectKey`. */
858
- declare function projectKeyPattern(): RegExp;
906
+ declare function appKeyPattern(): RegExp;
859
907
  /** The pattern it puts on the keys inside a plan's `quotas` object. */
860
908
  declare function quotaKeyPattern(): RegExp;
861
909
  /** How many quotas a plan must declare — 1 today, and read rather than assumed. */
@@ -866,10 +914,25 @@ declare function minimumQuotasPerPlan(): number;
866
914
  * The message carries the pattern rather than a prose paraphrase, because the
867
915
  * paraphrase is what goes stale.
868
916
  */
869
- declare function assertValidProjectKey(projectKey: string): void;
917
+ declare function assertValidAppKey(appKey: string): void;
870
918
  /** The same, for a quota key. `additionalProperties: false` makes it a hard rule. */
871
919
  declare function assertValidQuotaKey(quotaKey: string): void;
872
920
 
921
+ interface WrittenSetting {
922
+ /** Dotted path as it reads in the file, e.g. `tenantBilling.cancellationNoticeDays.monthly`. */
923
+ key: string;
924
+ /** The value as JSON — `0`, `[]`, `"EUR"` — so an empty list is visible as one. */
925
+ value: string;
926
+ }
927
+ /**
928
+ * The settings a generated `config/saas.yaml` carries.
929
+ *
930
+ * Loaded with the platform's own loader, not a YAML parse: a document `init`
931
+ * writes that the platform would refuse is a bug worth failing the generation
932
+ * for, rather than one the first boot reports after every file exists.
933
+ */
934
+ declare function settingsWrittenTo(catalogYaml: string, source?: string): WrittenSetting[];
935
+
873
936
  /** One entry of the move table: where a file was, and where it went. */
874
937
  interface MoveTable {
875
938
  readonly moves: Readonly<Record<string, string>>;
@@ -981,6 +1044,147 @@ interface ManifestRewriteOptions {
981
1044
  }
982
1045
  declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
983
1046
 
1047
+ interface ProjectKeyResult {
1048
+ readonly text: string;
1049
+ /** How many occurrences were taken out. */
1050
+ readonly rewritten: number;
1051
+ /**
1052
+ * 1-based line numbers of the occurrences left in place.
1053
+ *
1054
+ * Reported rather than removed: the codemod could not tell them from a
1055
+ * consumer's own field, and a wrong deletion is worse than a named one.
1056
+ */
1057
+ readonly undecided: readonly number[];
1058
+ }
1059
+ /**
1060
+ * Rewrites one source file.
1061
+ *
1062
+ * `yaml` switches to the config form: there the field is a top-level key in a
1063
+ * file the platform owns the schema of, so it is decidable without an anchor.
1064
+ */
1065
+ declare function removeProjectKey(text: string, kind?: 'source' | 'yaml'): ProjectKeyResult;
1066
+
1067
+ /**
1068
+ * The settings that belong in `config/saas.yaml#tenantBilling`, read off the
1069
+ * schema that defines them.
1070
+ *
1071
+ * Not a list here, and not a copy of the one in `@saasicat/nest`: both derive
1072
+ * from the same schema, so the day a third setting moves into that block, this
1073
+ * codemod names it and the module refuses it without either being edited.
1074
+ */
1075
+ declare const SETTINGS_THAT_MOVED: readonly string[];
1076
+ type MovedSetting = string;
1077
+ interface MovedSettingOccurrence {
1078
+ /** Which setting it is, so the report can say where it goes. */
1079
+ readonly setting: MovedSetting;
1080
+ /** 1-based line number. */
1081
+ readonly line: number;
1082
+ }
1083
+ interface MovedSettingsResult {
1084
+ readonly occurrences: readonly MovedSettingOccurrence[];
1085
+ }
1086
+ /**
1087
+ * Files a moved setting can actually be passed in.
1088
+ *
1089
+ * The codemod walk includes Markdown, and both consumers keep large
1090
+ * documentation folders — an upgrade note that mentions `cancellationNoticeDays`
1091
+ * would land in the report beside the line somebody has to change, and a report
1092
+ * that mixes the two is one nobody reads twice. Prose cannot pass a module
1093
+ * option, so prose is not scanned.
1094
+ */
1095
+ declare const SCANNED_FOR_MOVED_SETTINGS: RegExp;
1096
+ /**
1097
+ * Every occurrence of a moved setting in one source file.
1098
+ *
1099
+ * Matched on word boundaries alone: a longer name that contains it
1100
+ * (`cancellationNoticeDaysV2`) is not reported, and everything else is —
1101
+ * including a mention in a comment or a string.
1102
+ *
1103
+ * That last part is deliberate, and it is the opposite trade from the one this
1104
+ * comment used to claim. Requiring a colon would read as "only a property",
1105
+ * and it would then miss `{ cancellationNoticeDays }` and
1106
+ * `const { cancellationNoticeDays } = options` — two ordinary ways to pass the
1107
+ * same option. Telling a comment from code needs the grammar, which this does
1108
+ * not have. So it over-reports inside code, where the cost is a glance, rather
1109
+ * than under-reporting, where the cost is somebody not learning that their
1110
+ * value is about to stop being read.
1111
+ *
1112
+ * A property access (`config.cancellationNoticeDays`) is reported too: reading
1113
+ * the value back from module options is the same migration, one step further
1114
+ * along.
1115
+ */
1116
+ declare function findMovedSettings(text: string): MovedSettingsResult;
1117
+ /**
1118
+ * Where a setting goes, for the report.
1119
+ *
1120
+ * One sentence per setting rather than one for both: they end up in the same
1121
+ * block and mean different things, and "move these two to the file" is the
1122
+ * instruction people follow halfway.
1123
+ */
1124
+ declare const WHERE_IT_GOES: Record<string, string>;
1125
+
1126
+ /** Which files a `dbCatalog` can be passed in: code, not prose. */
1127
+ declare const SCANNED_FOR_DB_CATALOG: RegExp;
1128
+ type DbCatalogShape = 'values' | 'mixed' | 'reference';
1129
+ interface DbCatalogOccurrence {
1130
+ /** 1-based line of the `dbCatalog:` property. */
1131
+ readonly line: number;
1132
+ /**
1133
+ * `values` — an object literal with no `path`: the old shape whole.
1134
+ * `mixed` — a `path` with something beside it that the option does not
1135
+ * take: an upgrade that stopped halfway, and the platform refuses it too.
1136
+ * `reference` — anything else on the right of the colon: a variable, a
1137
+ * call, a spread. This cannot see what it carries, so it is named for a
1138
+ * person to look at rather than passed over.
1139
+ */
1140
+ readonly shape: DbCatalogShape;
1141
+ /**
1142
+ * The members that are not what the option takes — every one of a
1143
+ * `values` block, the ones left beside the path of a `mixed` one, and
1144
+ * nothing for a `reference`. A spread is listed as `...name`, because
1145
+ * what it carries is decided elsewhere.
1146
+ */
1147
+ readonly leftovers: readonly string[];
1148
+ }
1149
+ interface DbCatalogResult {
1150
+ readonly occurrences: readonly DbCatalogOccurrence[];
1151
+ }
1152
+ /**
1153
+ * Every `dbCatalog:` property in one source file, with what stands to its right.
1154
+ *
1155
+ * A property in code, which means the name followed by a colon, outside any
1156
+ * comment or string. The other codemod in this family reports every
1157
+ * word-boundary mention of a setting, including one in a comment, on the
1158
+ * reasoning that over-reporting inside code costs a glance. That reasoning
1159
+ * does not carry here: `saasicat init` writes the sentence "pass `dbCatalog`
1160
+ * instead" into every generated `app.module.ts`, so a mention is the normal
1161
+ * case and a report of it would be noise on every upgrade — and a block
1162
+ * commented out, or quoted as a sample, is migration work that does not
1163
+ * exist. An OPTIONAL type member (`dbCatalog?:`) is not a property either: the
1164
+ * `?` stands where the colon would. A required one (`dbCatalog: DbCatalogOptions`)
1165
+ * is the same tokens as a value passed in from elsewhere, and is reported as a
1166
+ * `reference` for that reason — a glance, which is what `reference` is for. A
1167
+ * shorthand `{ dbCatalog }` is not seen — the value it carries is elsewhere,
1168
+ * and the module's refusal names it at boot.
1169
+ *
1170
+ * What counts as migrated is read off `DB_CATALOG_MEMBERS`, the list the
1171
+ * platform's own refusal reads, so the two cannot disagree about a block.
1172
+ */
1173
+ declare function findDbCatalogBlocks(text: string): DbCatalogResult;
1174
+ /**
1175
+ * What is wrong with one occurrence, in the words the report uses.
1176
+ *
1177
+ * Beside the shapes rather than in the printer, because one of them has no
1178
+ * leftovers to name: a `values` block whose only member is one the option
1179
+ * takes — `dbCatalog: { env: process.env }` — or one emptied mid-edit is
1180
+ * genuinely broken and genuinely refused at boot, but "carries the values:"
1181
+ * with nothing after it names a file to go and look at without saying what to
1182
+ * look for.
1183
+ */
1184
+ declare function describeDbCatalogOccurrence(occurrence: Pick<DbCatalogOccurrence, 'shape' | 'leftovers'>): string;
1185
+ /** What to write instead, for the report. */
1186
+ declare const WHERE_DB_CATALOG_GOES: string;
1187
+
984
1188
  interface PatchAppModuleOptions {
985
1189
  /** Import specifier for the persistence bundle, or null when not generated. */
986
1190
  persistenceImport: string | null;
@@ -1048,15 +1252,18 @@ interface CliContextModuleOptions {
1048
1252
  * — `<app> doctor` runs but returns no content. Consumers register
1049
1253
  * project-specific checks.
1050
1254
  *
1051
- * Note: if `defaultDoctorChecks: true`, the 4 platform standard checks
1052
- * (PlanCatalog/Discovery/UserPort/AdminManifest) are registered **in
1053
- * addition** to this list.
1255
+ * Note: if `defaultDoctorChecks: true`, the platform's own checks
1256
+ * `PLATFORM_DOCTOR_CHECK_PROVIDERS` are registered **in addition** to
1257
+ * this list. They are named there rather than here: a second list of them
1258
+ * in prose is the copy that goes stale.
1054
1259
  */
1055
1260
  doctorChecks?: ProviderSpec<DoctorCheck[]>;
1056
1261
  /**
1057
- * Default `false`. If `true`, the platform registers the four standard
1058
- * checks from `PLATFORM_DOCTOR_CHECK_PROVIDERS` in addition to
1059
- * `doctorChecks`. Recommended for all apps that take `<app> doctor` seriously.
1262
+ * Default `false`. If `true`, the platform registers the checks from
1263
+ * `PLATFORM_DOCTOR_CHECK_PROVIDERS` in addition to `doctorChecks`. They
1264
+ * resolve what `SaaSiCatModule` provides, so an application that wires the
1265
+ * platform's modules by hand registers the ones it can serve instead.
1266
+ * Recommended for all apps that take `<app> doctor` seriously.
1060
1267
  */
1061
1268
  defaultDoctorChecks?: boolean;
1062
1269
  /**
@@ -1224,4 +1431,4 @@ declare class UserCommands extends CommandRunner {
1224
1431
  parsePassword(val: string): string;
1225
1432
  }
1226
1433
 
1227
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidProjectKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, projectKeyPattern, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
1434
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, type DbCatalogOccurrence, type DbCatalogResult, type DbCatalogShape, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, IssuerIdentityDoctorCheck, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, type MovedSetting, type MovedSettingOccurrence, type MovedSettingsResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, SCANNED_FOR_DB_CATALOG, SCANNED_FOR_MOVED_SETTINGS, SETTINGS_THAT_MOVED, type SchemaCheckReport, type TenantCascade, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WHERE_DB_CATALOG_GOES, WHERE_IT_GOES, WhoAmIFlow, type WhoAmIResult, type WrittenSetting, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, describeDbCatalogOccurrence, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findDbCatalogBlocks, findFkPointers, findMovedSettings, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, modelsKeptPastTheTenant, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, settingsWrittenTo, stripLineComment, structuralOnly, tablesAddressedBy };