@saasicat/spec 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/README.md +30 -0
- package/acceptance/README.md +3 -3
- package/acceptance/mfa/totp-verify-good-and-bad-code.yaml +6 -6
- package/admin-api.openapi.yaml +328 -77
- package/cli-conventions.md +19 -19
- package/index.cjs +3 -0
- package/index.d.cts +3 -1
- package/index.d.ts +2 -0
- package/index.js +9 -1
- package/package.json +4 -2
- package/prisma-fragments/01-subscription.prisma +34 -47
- package/prisma-fragments/03-plan-versions.prisma +3 -4
- package/prisma-fragments/05-bundle.prisma +4 -5
- package/prisma-fragments/06-catalog-entries.prisma +24 -24
- package/prisma-fragments/07-promotion.prisma +2 -3
- package/prisma-fragments/08-subscription-contract.prisma +42 -5
- package/prisma-fragments/09-pending-registration.prisma +32 -27
- package/prisma-fragments/11-subscription-bundle.prisma +17 -0
- package/prisma-fragments/12-applied-settings.prisma +55 -0
- package/prisma-fragments/13-subscriber.prisma +94 -0
- package/prisma-fragments/14-payments.prisma +100 -0
- package/prisma-fragments/README.md +40 -22
- package/schemas/admin-manifest.schema.json +9 -2
- package/schemas/plan-catalog.schema.json +239 -18
- package/schemas/tenant-ledger.schema.json +236 -0
- package/sql/1.0-a-contract-names-its-subscriber.postgres.sql +383 -0
- package/sql/1.0-a-payment-method-is-a-gateway-reference.postgres.sql +177 -0
- package/sql/1.0-a-settings-change-carries-its-order.postgres.sql +73 -0
- package/sql/1.0-line-items-record-their-money.postgres.sql +134 -0
- package/sql/1.0-remove-project-key.postgres.sql +203 -0
- package/sql/1.0-the-applied-settings-are-recorded.postgres.sql +67 -0
- package/sql/constraints.postgres.sql +76 -0
- package/sql/reference-schema.postgres.sql +299 -77
package/admin-api.openapi.yaml
CHANGED
|
@@ -17,10 +17,19 @@ openapi: 3.1.0
|
|
|
17
17
|
# - Tenant end-customer API (self-service plan change) — separate file
|
|
18
18
|
# - Onboarding API — separate file (uses PromoCode preview)
|
|
19
19
|
# - X-Rechnung — its own package, its own OpenAPI file
|
|
20
|
+
#
|
|
21
|
+
# Who serves what: an operation marked `x-served-by: app` is part of this
|
|
22
|
+
# contract but NOT shipped by @saasicat/nest — the tenant model, platform user
|
|
23
|
+
# administration and the promo-code detail view reach into data the application
|
|
24
|
+
# owns, so the application serves those routes and the admin UI calls them.
|
|
25
|
+
# Everything else is served by the reference implementation.
|
|
26
|
+
# `tests/openapi-covers-the-implementation.test.js` holds both directions: no
|
|
27
|
+
# undocumented route, and no documented operation without an implementation or
|
|
28
|
+
# that marker.
|
|
20
29
|
|
|
21
30
|
info:
|
|
22
31
|
title: SaaS Platform SuperAdmin API
|
|
23
|
-
version:
|
|
32
|
+
version: 1.0.0-rc.20
|
|
24
33
|
description: |
|
|
25
34
|
Read and write operations for platform administration:
|
|
26
35
|
tenants, users, subscriptions, promo codes, audit log,
|
|
@@ -48,6 +57,7 @@ tags:
|
|
|
48
57
|
- name: mfa
|
|
49
58
|
- name: manifest
|
|
50
59
|
- name: catalog
|
|
60
|
+
- name: settings
|
|
51
61
|
|
|
52
62
|
security:
|
|
53
63
|
- bearerAuth: []
|
|
@@ -141,7 +151,7 @@ paths:
|
|
|
141
151
|
# ────────────────────────────────────────────────────────
|
|
142
152
|
# Dashboard
|
|
143
153
|
# ────────────────────────────────────────────────────────
|
|
144
|
-
/dashboard
|
|
154
|
+
/stats/dashboard:
|
|
145
155
|
get:
|
|
146
156
|
tags: [dashboard]
|
|
147
157
|
summary: Global platform metrics
|
|
@@ -153,53 +163,6 @@ paths:
|
|
|
153
163
|
schema:
|
|
154
164
|
$ref: '#/components/schemas/DashboardStats'
|
|
155
165
|
|
|
156
|
-
# ────────────────────────────────────────────────────────
|
|
157
|
-
# Plan catalog (read-only — maintained via file + reload endpoint)
|
|
158
|
-
# ────────────────────────────────────────────────────────
|
|
159
|
-
/plans:
|
|
160
|
-
get:
|
|
161
|
-
tags: [plans]
|
|
162
|
-
summary: Currently loaded plan catalog
|
|
163
|
-
responses:
|
|
164
|
-
'200':
|
|
165
|
-
description: Catalog
|
|
166
|
-
content:
|
|
167
|
-
application/json:
|
|
168
|
-
schema:
|
|
169
|
-
$ref: '#/components/schemas/PlanCatalog'
|
|
170
|
-
|
|
171
|
-
/plans/reload:
|
|
172
|
-
post:
|
|
173
|
-
tags: [plans]
|
|
174
|
-
summary: Reload the plan catalog from file (for admin CLI / hot-reload dev)
|
|
175
|
-
x-mfa-required: true
|
|
176
|
-
responses:
|
|
177
|
-
'200':
|
|
178
|
-
description: Loaded
|
|
179
|
-
content:
|
|
180
|
-
application/json:
|
|
181
|
-
schema:
|
|
182
|
-
type: object
|
|
183
|
-
properties:
|
|
184
|
-
loadedAt: { type: string, format: date-time }
|
|
185
|
-
planCount: { type: integer }
|
|
186
|
-
source: { type: string, description: Path or URL }
|
|
187
|
-
|
|
188
|
-
/plans/last-update:
|
|
189
|
-
get:
|
|
190
|
-
tags: [plans]
|
|
191
|
-
summary: When the catalog was last loaded + hash
|
|
192
|
-
responses:
|
|
193
|
-
'200':
|
|
194
|
-
description: Meta
|
|
195
|
-
content:
|
|
196
|
-
application/json:
|
|
197
|
-
schema:
|
|
198
|
-
type: object
|
|
199
|
-
properties:
|
|
200
|
-
loadedAt: { type: string, format: date-time }
|
|
201
|
-
hash: { type: string, description: SHA-256 of the YAML content }
|
|
202
|
-
|
|
203
166
|
# ────────────────────────────────────────────────────────
|
|
204
167
|
# Tenants
|
|
205
168
|
# ────────────────────────────────────────────────────────
|
|
@@ -234,6 +197,7 @@ paths:
|
|
|
234
197
|
|
|
235
198
|
post:
|
|
236
199
|
tags: [tenants]
|
|
200
|
+
x-served-by: app # the tenant model is the application's
|
|
237
201
|
summary: Create a tenant manually (bypasses the onboarding flow)
|
|
238
202
|
x-mfa-required: true
|
|
239
203
|
requestBody:
|
|
@@ -262,6 +226,7 @@ paths:
|
|
|
262
226
|
schema: { $ref: '#/components/schemas/AdminTenantDetail' }
|
|
263
227
|
delete:
|
|
264
228
|
tags: [tenants]
|
|
229
|
+
x-served-by: app # the tenant model is the application's
|
|
265
230
|
summary: Soft-delete a tenant (set deletedAt)
|
|
266
231
|
x-mfa-required: true
|
|
267
232
|
requestBody:
|
|
@@ -315,6 +280,7 @@ paths:
|
|
|
315
280
|
- $ref: '#/components/parameters/TenantSlug'
|
|
316
281
|
post:
|
|
317
282
|
tags: [tenants]
|
|
283
|
+
x-served-by: app # the tenant model is the application's
|
|
318
284
|
summary: Short-lived JWT as TENANT_ADMIN for support insight
|
|
319
285
|
x-mfa-required: true
|
|
320
286
|
requestBody:
|
|
@@ -344,6 +310,7 @@ paths:
|
|
|
344
310
|
- $ref: '#/components/parameters/TenantSlug'
|
|
345
311
|
get:
|
|
346
312
|
tags: [tenants]
|
|
313
|
+
x-served-by: app # the tenant model is the application's
|
|
347
314
|
summary: GDPR / audit export of all tenant data as a ZIP
|
|
348
315
|
x-mfa-required: true
|
|
349
316
|
responses:
|
|
@@ -361,6 +328,7 @@ paths:
|
|
|
361
328
|
- $ref: '#/components/parameters/TenantSlug'
|
|
362
329
|
get:
|
|
363
330
|
tags: [subscriptions]
|
|
331
|
+
x-served-by: app # no admin-side subscription route ships
|
|
364
332
|
summary: Current subscription + add-ons + effective limits
|
|
365
333
|
responses:
|
|
366
334
|
'200':
|
|
@@ -371,6 +339,7 @@ paths:
|
|
|
371
339
|
|
|
372
340
|
patch:
|
|
373
341
|
tags: [subscriptions]
|
|
342
|
+
x-served-by: app # no admin-side subscription route ships
|
|
374
343
|
summary: Change plan, custom limits, or pilot flag
|
|
375
344
|
x-mfa-required: true
|
|
376
345
|
requestBody:
|
|
@@ -435,6 +404,7 @@ paths:
|
|
|
435
404
|
schema: { type: string }
|
|
436
405
|
post:
|
|
437
406
|
tags: [users]
|
|
407
|
+
x-served-by: app # platform users live in the application's identity store
|
|
438
408
|
summary: Trigger a password-reset email (sends no plaintext passwords)
|
|
439
409
|
x-mfa-required: true
|
|
440
410
|
requestBody:
|
|
@@ -457,6 +427,7 @@ paths:
|
|
|
457
427
|
schema: { type: string }
|
|
458
428
|
post:
|
|
459
429
|
tags: [users]
|
|
430
|
+
x-served-by: app # platform users live in the application's identity store
|
|
460
431
|
summary: Deactivate a user (block login)
|
|
461
432
|
x-mfa-required: true
|
|
462
433
|
requestBody:
|
|
@@ -476,6 +447,7 @@ paths:
|
|
|
476
447
|
- $ref: '#/components/parameters/TenantSlug'
|
|
477
448
|
post:
|
|
478
449
|
tags: [users]
|
|
450
|
+
x-served-by: app # the tenant model is the application's
|
|
479
451
|
summary: Transfer the TENANT_ADMIN role to another user
|
|
480
452
|
x-mfa-required: true
|
|
481
453
|
requestBody:
|
|
@@ -546,6 +518,7 @@ paths:
|
|
|
546
518
|
schema: { type: string, format: uuid }
|
|
547
519
|
get:
|
|
548
520
|
tags: [promo-codes]
|
|
521
|
+
x-served-by: app # the detail view joins application data
|
|
549
522
|
responses:
|
|
550
523
|
'200':
|
|
551
524
|
description: Detail
|
|
@@ -622,41 +595,217 @@ paths:
|
|
|
622
595
|
items: { $ref: '#/components/schemas/AuditEntryDto' }
|
|
623
596
|
|
|
624
597
|
# ────────────────────────────────────────────────────────
|
|
625
|
-
#
|
|
598
|
+
# First-run setup (SetupModule, packages/nest/src/setup/*)
|
|
599
|
+
#
|
|
600
|
+
# Public by design: before the first SUPER_ADMIN there is no session to
|
|
601
|
+
# authenticate. The window is open only while zero SUPER_ADMIN exist and
|
|
602
|
+
# is additionally gated by the operator-set SETUP_TOKEN.
|
|
626
603
|
# ────────────────────────────────────────────────────────
|
|
627
|
-
/
|
|
628
|
-
|
|
629
|
-
tags: [
|
|
630
|
-
|
|
604
|
+
/setup/status:
|
|
605
|
+
get:
|
|
606
|
+
tags: [setup]
|
|
607
|
+
security: [] # PUBLIC — answers whether the window is still open
|
|
608
|
+
summary: Whether first-run setup is still open
|
|
631
609
|
responses:
|
|
632
610
|
'200':
|
|
633
|
-
description:
|
|
611
|
+
description: Status
|
|
612
|
+
content:
|
|
613
|
+
application/json:
|
|
614
|
+
schema:
|
|
615
|
+
type: object
|
|
616
|
+
required: [needsSetup]
|
|
617
|
+
properties:
|
|
618
|
+
needsSetup:
|
|
619
|
+
type: boolean
|
|
620
|
+
description: true as long as no SUPER_ADMIN exists
|
|
621
|
+
|
|
622
|
+
/setup:
|
|
623
|
+
post:
|
|
624
|
+
tags: [setup]
|
|
625
|
+
security: [] # PUBLIC — see the section note
|
|
626
|
+
summary: Create the first SUPER_ADMIN and start its MFA enrolment
|
|
627
|
+
requestBody:
|
|
628
|
+
required: true
|
|
629
|
+
content:
|
|
630
|
+
application/json:
|
|
631
|
+
schema:
|
|
632
|
+
type: object
|
|
633
|
+
required: [token, email]
|
|
634
|
+
properties:
|
|
635
|
+
token: { type: string, description: Must match SETUP_TOKEN }
|
|
636
|
+
email: { type: string, format: email }
|
|
637
|
+
password:
|
|
638
|
+
type: string
|
|
639
|
+
minLength: 8
|
|
640
|
+
description: Generated by the server when omitted
|
|
641
|
+
responses:
|
|
642
|
+
'201':
|
|
643
|
+
description: Created — the response carries the enrolment secret once
|
|
634
644
|
content:
|
|
635
645
|
application/json:
|
|
636
646
|
schema:
|
|
637
647
|
type: object
|
|
648
|
+
required: [userId, email, otpauthUri, qrDataUrl, secret]
|
|
638
649
|
properties:
|
|
650
|
+
userId: { type: string }
|
|
651
|
+
email: { type: string, format: email }
|
|
639
652
|
otpauthUri: { type: string }
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
653
|
+
qrDataUrl: { type: string, description: PNG data URL }
|
|
654
|
+
secret: { type: string, description: Base32 TOTP secret }
|
|
655
|
+
generatedPassword:
|
|
656
|
+
type: string
|
|
657
|
+
description: Present only when the server generated it
|
|
658
|
+
'400':
|
|
659
|
+
description: The email is not an address the platform accepts
|
|
660
|
+
content:
|
|
661
|
+
application/json:
|
|
662
|
+
schema:
|
|
663
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
664
|
+
'401':
|
|
665
|
+
description: The token does not match SETUP_TOKEN
|
|
666
|
+
content:
|
|
667
|
+
application/json:
|
|
668
|
+
schema:
|
|
669
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
670
|
+
'403':
|
|
671
|
+
description: SETUP_TOKEN is not set — setup is disabled
|
|
672
|
+
content:
|
|
673
|
+
application/json:
|
|
674
|
+
schema:
|
|
675
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
676
|
+
'409':
|
|
677
|
+
description: >-
|
|
678
|
+
Either the window has closed itself because a SUPER_ADMIN already
|
|
679
|
+
exists (`SETUP_ALREADY_DONE`), or the address is taken by a platform
|
|
680
|
+
user (`EMAIL_EXISTS`). The `code` tells them apart.
|
|
681
|
+
content:
|
|
682
|
+
application/json:
|
|
683
|
+
schema:
|
|
684
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
644
685
|
|
|
645
|
-
/
|
|
686
|
+
/setup/confirm-mfa:
|
|
646
687
|
post:
|
|
647
|
-
tags: [
|
|
648
|
-
|
|
688
|
+
tags: [setup]
|
|
689
|
+
security: [] # PUBLIC — see the section note
|
|
690
|
+
summary: Confirm the enrolment with the first TOTP code, closing the window
|
|
649
691
|
requestBody:
|
|
650
692
|
required: true
|
|
651
693
|
content:
|
|
652
694
|
application/json:
|
|
653
695
|
schema:
|
|
654
696
|
type: object
|
|
655
|
-
required: [code]
|
|
697
|
+
required: [token, userId, code]
|
|
656
698
|
properties:
|
|
699
|
+
token: { type: string }
|
|
700
|
+
userId: { type: string }
|
|
657
701
|
code: { type: string, pattern: '^[0-9]{6}$' }
|
|
658
702
|
responses:
|
|
659
|
-
'200':
|
|
703
|
+
'200':
|
|
704
|
+
description: Activated
|
|
705
|
+
content:
|
|
706
|
+
application/json:
|
|
707
|
+
schema:
|
|
708
|
+
type: object
|
|
709
|
+
required: [ok]
|
|
710
|
+
properties:
|
|
711
|
+
ok: { type: boolean }
|
|
712
|
+
'401':
|
|
713
|
+
description: The token does not match SETUP_TOKEN
|
|
714
|
+
content:
|
|
715
|
+
application/json:
|
|
716
|
+
schema:
|
|
717
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
718
|
+
'403':
|
|
719
|
+
description: SETUP_TOKEN is not set — setup is disabled
|
|
720
|
+
content:
|
|
721
|
+
application/json:
|
|
722
|
+
schema:
|
|
723
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
724
|
+
|
|
725
|
+
# ────────────────────────────────────────────────────────
|
|
726
|
+
# Plan catalog import (PlanCatalogImporterModule)
|
|
727
|
+
# ────────────────────────────────────────────────────────
|
|
728
|
+
/billing/plan-catalog/import:
|
|
729
|
+
post:
|
|
730
|
+
tags: [plans]
|
|
731
|
+
summary: Import a plan catalog YAML into the catalog tables
|
|
732
|
+
requestBody:
|
|
733
|
+
required: true
|
|
734
|
+
content:
|
|
735
|
+
application/json:
|
|
736
|
+
schema:
|
|
737
|
+
type: object
|
|
738
|
+
required: [yamlContent]
|
|
739
|
+
properties:
|
|
740
|
+
yamlContent: { type: string, description: Contents of saas.yaml }
|
|
741
|
+
crossFieldChecks:
|
|
742
|
+
type: boolean
|
|
743
|
+
default: true
|
|
744
|
+
description: Validate references between plans, features and quotas
|
|
745
|
+
responses:
|
|
746
|
+
'201':
|
|
747
|
+
description: Import report — the counters say what was new and what already matched
|
|
748
|
+
content:
|
|
749
|
+
application/json:
|
|
750
|
+
schema:
|
|
751
|
+
type: object
|
|
752
|
+
required:
|
|
753
|
+
- plansCreated
|
|
754
|
+
- plansSkipped
|
|
755
|
+
- planVersionsCreated
|
|
756
|
+
- planVersionsSkipped
|
|
757
|
+
- featureEntriesCreated
|
|
758
|
+
- featureEntriesSkipped
|
|
759
|
+
- warnings
|
|
760
|
+
properties:
|
|
761
|
+
plansCreated: { type: integer }
|
|
762
|
+
plansSkipped: { type: integer }
|
|
763
|
+
planVersionsCreated: { type: integer }
|
|
764
|
+
planVersionsSkipped: { type: integer }
|
|
765
|
+
featureEntriesCreated: { type: integer }
|
|
766
|
+
featureEntriesSkipped: { type: integer }
|
|
767
|
+
warnings: { type: array, items: { type: string } }
|
|
768
|
+
'400':
|
|
769
|
+
description: The YAML failed schema validation
|
|
770
|
+
content:
|
|
771
|
+
application/json:
|
|
772
|
+
schema:
|
|
773
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
774
|
+
|
|
775
|
+
# ────────────────────────────────────────────────────────
|
|
776
|
+
# Subscriptions
|
|
777
|
+
# ────────────────────────────────────────────────────────
|
|
778
|
+
/subscriptions:
|
|
779
|
+
get:
|
|
780
|
+
tags: [subscriptions]
|
|
781
|
+
summary: Every tenant subscription, one row each
|
|
782
|
+
responses:
|
|
783
|
+
'200':
|
|
784
|
+
description: Rows
|
|
785
|
+
content:
|
|
786
|
+
application/json:
|
|
787
|
+
schema:
|
|
788
|
+
type: array
|
|
789
|
+
items:
|
|
790
|
+
type: object
|
|
791
|
+
required: [id, tenant, plan, status, billingCycle]
|
|
792
|
+
properties:
|
|
793
|
+
id: { type: string }
|
|
794
|
+
tenant:
|
|
795
|
+
type: object
|
|
796
|
+
required: [slug, name]
|
|
797
|
+
properties:
|
|
798
|
+
slug: { type: string }
|
|
799
|
+
name: { type: string }
|
|
800
|
+
plan: { type: string }
|
|
801
|
+
status: { type: string }
|
|
802
|
+
billingCycle: { type: string }
|
|
803
|
+
periodEndsAt:
|
|
804
|
+
type: string
|
|
805
|
+
format: date-time
|
|
806
|
+
nullable: true
|
|
807
|
+
monthlyNet: { type: string, nullable: true }
|
|
808
|
+
additionalProperties: true
|
|
660
809
|
|
|
661
810
|
# ────────────────────────────────────────────────────────
|
|
662
811
|
# Catalog V2 — Plan/Bundle/Marketing/Promotion + Discovery
|
|
@@ -1199,6 +1348,42 @@ paths:
|
|
|
1199
1348
|
}
|
|
1200
1349
|
responses: { '200': { description: Synchronized } }
|
|
1201
1350
|
|
|
1351
|
+
# ── The applied configuration (SettingsModule, packages/nest/src/settings/*) ──
|
|
1352
|
+
#
|
|
1353
|
+
# Read-only. What the installation is running on — read off the loaded
|
|
1354
|
+
# catalogue — and the record of it: since when, from where, and what changed
|
|
1355
|
+
# between two starts. The record is a mirror of config/saas.yaml, never a
|
|
1356
|
+
# source, so there is no write operation here.
|
|
1357
|
+
|
|
1358
|
+
/settings:
|
|
1359
|
+
get:
|
|
1360
|
+
tags: [settings]
|
|
1361
|
+
summary: The running configuration, since when it applies, and what changed at boot
|
|
1362
|
+
responses:
|
|
1363
|
+
'200':
|
|
1364
|
+
description: The applied settings and their record
|
|
1365
|
+
content:
|
|
1366
|
+
application/json:
|
|
1367
|
+
schema: { $ref: '#/components/schemas/AppliedSettingsView' }
|
|
1368
|
+
|
|
1369
|
+
/settings/changes/{id}/acknowledge:
|
|
1370
|
+
parameters: [{ name: id, in: path, required: true, schema: { type: string } }]
|
|
1371
|
+
post:
|
|
1372
|
+
tags: [settings]
|
|
1373
|
+
summary: Mark a recorded settings change as seen
|
|
1374
|
+
description: >-
|
|
1375
|
+
The record survives until this is called. The acknowledgement keeps its first
|
|
1376
|
+
author — a second call changes nothing and answers the record as it stands — and
|
|
1377
|
+
is written to the audit log as SETTINGS_CHANGE_ACKNOWLEDGE.
|
|
1378
|
+
responses:
|
|
1379
|
+
'200':
|
|
1380
|
+
description: The change, acknowledged
|
|
1381
|
+
content:
|
|
1382
|
+
application/json:
|
|
1383
|
+
schema: { $ref: '#/components/schemas/SettingsChangeView' }
|
|
1384
|
+
'404':
|
|
1385
|
+
description: No recorded change has this id (`SETTINGS_CHANGE_NOT_FOUND`)
|
|
1386
|
+
|
|
1202
1387
|
# ── Discovery (scan/status, outside /catalog) ──
|
|
1203
1388
|
/discovery:
|
|
1204
1389
|
get:
|
|
@@ -1240,13 +1425,78 @@ components:
|
|
|
1240
1425
|
schema: { type: string, pattern: '^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$' }
|
|
1241
1426
|
|
|
1242
1427
|
schemas:
|
|
1428
|
+
# ────────── Applied settings (packages/nest/src/settings/settings.controller.ts) ──────────
|
|
1429
|
+
AppliedSettingsView:
|
|
1430
|
+
type: object
|
|
1431
|
+
required: [source, fingerprint, settings, recorded, appliedAt, changes]
|
|
1432
|
+
properties:
|
|
1433
|
+
source:
|
|
1434
|
+
{
|
|
1435
|
+
type: string,
|
|
1436
|
+
description: 'Where the running settings came from: the absolute path of the file, or a phrase saying they were handed in as code',
|
|
1437
|
+
}
|
|
1438
|
+
fingerprint:
|
|
1439
|
+
{
|
|
1440
|
+
type: string,
|
|
1441
|
+
description: 'sha256-<hex> over the canonical JSON of `settings`',
|
|
1442
|
+
}
|
|
1443
|
+
settings:
|
|
1444
|
+
type: object
|
|
1445
|
+
additionalProperties: true
|
|
1446
|
+
description: 'The settings subtree of the catalogue as it was resolved — everything but plans and features'
|
|
1447
|
+
recorded:
|
|
1448
|
+
{
|
|
1449
|
+
type: boolean,
|
|
1450
|
+
description: 'Whether this installation keeps a record at all (a persistence adapter provides the port)',
|
|
1451
|
+
}
|
|
1452
|
+
appliedAt:
|
|
1453
|
+
type: [string, 'null']
|
|
1454
|
+
format: date-time
|
|
1455
|
+
description: 'When these values became the running configuration. Null where nothing is recorded, or the record describes an earlier configuration.'
|
|
1456
|
+
changes:
|
|
1457
|
+
type: array
|
|
1458
|
+
description: 'Newest first; empty where nothing is recorded'
|
|
1459
|
+
items: { $ref: '#/components/schemas/SettingsChangeView' }
|
|
1460
|
+
|
|
1461
|
+
SettingsChangeView:
|
|
1462
|
+
type: object
|
|
1463
|
+
required: [id, noticedAt, source, differences, acknowledgedAt, acknowledgedBy]
|
|
1464
|
+
properties:
|
|
1465
|
+
id: { type: string }
|
|
1466
|
+
noticedAt:
|
|
1467
|
+
{
|
|
1468
|
+
type: string,
|
|
1469
|
+
format: date-time,
|
|
1470
|
+
description: 'The start that found the fingerprint moved',
|
|
1471
|
+
}
|
|
1472
|
+
source: { type: string }
|
|
1473
|
+
differences:
|
|
1474
|
+
type: array
|
|
1475
|
+
items: { $ref: '#/components/schemas/SettingsDifference' }
|
|
1476
|
+
acknowledgedAt: { type: [string, 'null'], format: date-time }
|
|
1477
|
+
acknowledgedBy:
|
|
1478
|
+
{
|
|
1479
|
+
type: [string, 'null'],
|
|
1480
|
+
description: 'An actor tag, as the audit log writes it',
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
SettingsDifference:
|
|
1484
|
+
type: object
|
|
1485
|
+
required: [path]
|
|
1486
|
+
properties:
|
|
1487
|
+
path:
|
|
1488
|
+
{
|
|
1489
|
+
type: string,
|
|
1490
|
+
description: 'Dotted, as the loader names a field: tenantBilling.cancellationNoticeDays.monthly',
|
|
1491
|
+
}
|
|
1492
|
+
before: { description: 'Absent where the leaf did not exist before' }
|
|
1493
|
+
after: { description: 'Absent where the leaf no longer exists' }
|
|
1494
|
+
|
|
1243
1495
|
# ────────── Catalog V2 (DTO = SSOT: packages/nest/src/catalog/dto/*) ──────────
|
|
1244
1496
|
CatalogPlanCreate:
|
|
1245
1497
|
type: object
|
|
1246
|
-
required: [
|
|
1498
|
+
required: [planKey, label]
|
|
1247
1499
|
properties:
|
|
1248
|
-
projectKey:
|
|
1249
|
-
{ type: string, pattern: '^[a-z][a-z0-9-]*$', description: 'kebab-case' }
|
|
1250
1500
|
planKey:
|
|
1251
1501
|
{
|
|
1252
1502
|
type: string,
|
|
@@ -1287,9 +1537,8 @@ components:
|
|
|
1287
1537
|
|
|
1288
1538
|
CatalogBundleCreate:
|
|
1289
1539
|
type: object
|
|
1290
|
-
required: [
|
|
1540
|
+
required: [bundleKey, label]
|
|
1291
1541
|
properties:
|
|
1292
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1293
1542
|
bundleKey: { type: string, pattern: '^[A-Z][A-Z0-9_]*$', description: 'no hyphen' }
|
|
1294
1543
|
label: { type: string, minLength: 1, maxLength: 120 }
|
|
1295
1544
|
description: { type: string, maxLength: 2000 }
|
|
@@ -1347,9 +1596,8 @@ components:
|
|
|
1347
1596
|
|
|
1348
1597
|
CatalogMarketingProjectionCreate:
|
|
1349
1598
|
type: object
|
|
1350
|
-
required: [
|
|
1599
|
+
required: [targetType, targetVersionId, displayLabel, description]
|
|
1351
1600
|
properties:
|
|
1352
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1353
1601
|
targetType: { type: string, enum: [PLAN, BUNDLE] }
|
|
1354
1602
|
targetVersionId: { type: string }
|
|
1355
1603
|
locale: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$', default: de }
|
|
@@ -1465,9 +1713,8 @@ components:
|
|
|
1465
1713
|
|
|
1466
1714
|
CatalogMarketingSettingsUpdate:
|
|
1467
1715
|
type: object
|
|
1468
|
-
required: [
|
|
1716
|
+
required: [activeLocales]
|
|
1469
1717
|
properties:
|
|
1470
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1471
1718
|
activeLocales:
|
|
1472
1719
|
type: array
|
|
1473
1720
|
uniqueItems: true
|
|
@@ -1475,9 +1722,8 @@ components:
|
|
|
1475
1722
|
|
|
1476
1723
|
CatalogPromotionCreate:
|
|
1477
1724
|
type: object
|
|
1478
|
-
required: [
|
|
1725
|
+
required: [internalLabel, type, value, validFrom, validTo]
|
|
1479
1726
|
properties:
|
|
1480
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1481
1727
|
internalLabel: { type: string, maxLength: 120 }
|
|
1482
1728
|
type: { type: string, enum: [percent, amount, intro, freeMonths] }
|
|
1483
1729
|
value:
|
|
@@ -1550,10 +1796,15 @@ components:
|
|
|
1550
1796
|
# ────────── Plan-Catalog (mirror of plans.yaml) ──────────
|
|
1551
1797
|
PlanCatalog:
|
|
1552
1798
|
type: object
|
|
1553
|
-
required: [schemaVersion,
|
|
1799
|
+
required: [schemaVersion, app, currency, vatRate, plans]
|
|
1554
1800
|
properties:
|
|
1555
1801
|
schemaVersion: { type: integer, const: 1 }
|
|
1556
|
-
|
|
1802
|
+
app:
|
|
1803
|
+
type: object
|
|
1804
|
+
required: [name]
|
|
1805
|
+
description: 'App identity — the only place the application is named'
|
|
1806
|
+
properties:
|
|
1807
|
+
name: { type: string, minLength: 1 }
|
|
1557
1808
|
currency: { type: string, pattern: '^[A-Z]{3}$' }
|
|
1558
1809
|
vatRate: { type: number, format: decimal, description: 'in percent' }
|
|
1559
1810
|
features:
|
package/cli-conventions.md
CHANGED
|
@@ -27,7 +27,7 @@ for the audit log:
|
|
|
27
27
|
- Without an identity → writing commands are rejected with exit code `2`
|
|
28
28
|
(see §6).
|
|
29
29
|
|
|
30
|
-
Read commands (e.g. `…
|
|
30
|
+
Read commands (e.g. `… tenant list`) are allowed without an identity, but
|
|
31
31
|
write `actor=anonymous` to the local log.
|
|
32
32
|
|
|
33
33
|
## 2. Mandatory MFA for Critical Operations
|
|
@@ -35,9 +35,9 @@ write `actor=anonymous` to the local log.
|
|
|
35
35
|
The following operations **MUST** prompt for a TOTP code
|
|
36
36
|
(Google Authenticator) before execution:
|
|
37
37
|
|
|
38
|
-
- `…
|
|
38
|
+
- `… plans apply` (PlanCatalog mutation)
|
|
39
39
|
- `… pilot create|grant|revoke` (pilot override)
|
|
40
|
-
- `…
|
|
40
|
+
- `… tenant suspend|impersonate` (tenant security operations)
|
|
41
41
|
- `… plan-version publish` (PlanVersion publication)
|
|
42
42
|
- `… user reassign-admin` (last-admin escalation)
|
|
43
43
|
- `… admin mfa-reset` (MFA reset of another SUPER_ADMIN)
|
|
@@ -57,12 +57,12 @@ is not `NODE_ENV=development` and not a localhost DB), every
|
|
|
57
57
|
writing command must be confirmed interactively:
|
|
58
58
|
|
|
59
59
|
```text
|
|
60
|
-
?
|
|
60
|
+
? Type production to confirm: production
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
- Alternative: `--yes` / `-y` skips the confirmation (for CI/CD).
|
|
64
64
|
- Plus `--dry-run` is the default for destructive commands like
|
|
65
|
-
`…
|
|
65
|
+
`… plans apply` and `… discounts delete`. Only `--apply` or
|
|
66
66
|
`--yes` applies.
|
|
67
67
|
|
|
68
68
|
`production` detection should run through the consumer's implementation of the
|
|
@@ -110,11 +110,11 @@ redefine them, because cron/CI scripts pattern-match on them:
|
|
|
110
110
|
| 4 | Connectivity error (DB unreachable, sidecar service down) |
|
|
111
111
|
| 5 | Permission error (user is not allowed to perform this operation — e.g. a SUPER_ADMIN operation, but the user is only a TENANT_ADMIN) |
|
|
112
112
|
| 6 | Conflict (optimistic-lock mismatch, idempotency violation) |
|
|
113
|
-
| 7 | Drift detected (e.g. `
|
|
113
|
+
| 7 | Drift detected (e.g. `plans diff` finds differences; `manifest check` finds inconsistencies) |
|
|
114
114
|
| 99 | Internal error (uncaught exception, bug reports welcome) |
|
|
115
115
|
|
|
116
116
|
Read commands return `0` even for an empty result set (no drift =
|
|
117
|
-
no error). Drift-detection commands (`
|
|
117
|
+
no error). Drift-detection commands (`plans diff`, `manifest check`)
|
|
118
118
|
return `7` when drift is found — CI gates can react to that.
|
|
119
119
|
|
|
120
120
|
## 7. Consumer Plugin API
|
|
@@ -136,23 +136,23 @@ Plugin commands automatically inherit:
|
|
|
136
136
|
|
|
137
137
|
```bash
|
|
138
138
|
# Read operation, no identity required
|
|
139
|
-
$ myapp
|
|
139
|
+
$ myapp tenant list --output=json | jq '.[] | select(.status=="ACTIVE")'
|
|
140
140
|
|
|
141
141
|
# Writing with identity, dry-run as default
|
|
142
|
-
$ myapp
|
|
143
|
-
ℹ Diff: 2
|
|
144
|
-
ℹ Dry
|
|
142
|
+
$ myapp plans apply config/plans.yaml
|
|
143
|
+
ℹ Diff: 2 plans updated, 1 bundle added.
|
|
144
|
+
ℹ Dry run — pass --apply to write.
|
|
145
145
|
|
|
146
146
|
# Writing with mandatory MFA
|
|
147
|
-
$ myapp
|
|
148
|
-
ℹ
|
|
149
|
-
? TOTP
|
|
150
|
-
✓ PlanCatalog
|
|
147
|
+
$ myapp plans apply config/plans.yaml --apply
|
|
148
|
+
ℹ MFA confirmation required.
|
|
149
|
+
? TOTP code: 482 159
|
|
150
|
+
✓ PlanCatalog updated. AuditLog: PLAN_CATALOG_UPDATE.
|
|
151
151
|
|
|
152
152
|
# Production confirm
|
|
153
153
|
$ NODE_ENV=production myapp pilot grant pilot-schmidt --as=admin@example.com
|
|
154
|
-
?
|
|
155
|
-
ℹ
|
|
156
|
-
? TOTP
|
|
157
|
-
✓ Pilot
|
|
154
|
+
? Type production to confirm: production
|
|
155
|
+
ℹ MFA confirmation required.
|
|
156
|
+
? TOTP code: 217 998
|
|
157
|
+
✓ Pilot grant stored.
|
|
158
158
|
```
|