@saasicat/spec 1.0.0-rc.2 → 1.0.0-rc.4

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 CHANGED
@@ -1,8 +1,30 @@
1
1
  # @saasicat/spec
2
2
 
3
+ ## What this is
4
+
3
5
  Language-neutral spec of the SaaS platform — JSON Schemas, OpenAPI and
4
6
  acceptance test scenarios.
5
7
 
8
+ ## What this is not
9
+
10
+ Not runnable code. JSON Schemas, an OpenAPI document, Prisma fragments and SQL
11
+ constraints — a contract any language can implement, which is why the reference
12
+ implementation is a separate package.
13
+
14
+ Not a migration tool. The fragments are merged into your own `schema.prisma`
15
+ by `saasicat schema apply`, and your Prisma or Drizzle migration flow takes it
16
+ from there.
17
+
18
+ ## Entry points
19
+
20
+ | Entry | What is in it |
21
+ | ----------------------------- | ---------------------------------------------------------------- |
22
+ | `.` | The hand-written index: constants and helpers over the schemas. |
23
+ | `./schemas/*.json` | The JSON Schemas — the source of truth for every wire format. |
24
+ | `./prisma-fragments/*.prisma` | One fragment per table group, merged by `saasicat schema apply`. |
25
+ | `./prisma-fragments/` | The directory itself, for tooling that enumerates the fragments. |
26
+ | `./sql/*.sql` | The constraints Prisma cannot express, and the reference schema. |
27
+
6
28
  ## Contents
7
29
 
8
30
  | File | Purpose |
@@ -57,3 +79,9 @@ pnpm add @saasicat/spec
57
79
  live in the DB, curated via the SuperAdmin UI.
58
80
  - **`schemaVersion` is required** in every top-level schema. Major bumps
59
81
  break — that is allowed, but must be documented in a migration note.
82
+
83
+ ## Next
84
+
85
+ - [Data model](../../docs/explanation/data-model.md) — the tables the fragments declare
86
+ - [From capability to contract](../../docs/explanation/capability-to-contract.md) — what the schemas describe
87
+ - [Codegen](../../docs/explanation/adr/0006-spec-to-types-codegen.md) — how types are derived from them
@@ -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: 0.1.0-draft
32
+ version: 1.0.0-rc.4
24
33
  description: |
25
34
  Read and write operations for platform administration:
26
35
  tenants, users, subscriptions, promo codes, audit log,
@@ -141,7 +150,7 @@ paths:
141
150
  # ────────────────────────────────────────────────────────
142
151
  # Dashboard
143
152
  # ────────────────────────────────────────────────────────
144
- /dashboard/stats:
153
+ /stats/dashboard:
145
154
  get:
146
155
  tags: [dashboard]
147
156
  summary: Global platform metrics
@@ -153,53 +162,6 @@ paths:
153
162
  schema:
154
163
  $ref: '#/components/schemas/DashboardStats'
155
164
 
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
165
  # ────────────────────────────────────────────────────────
204
166
  # Tenants
205
167
  # ────────────────────────────────────────────────────────
@@ -234,6 +196,7 @@ paths:
234
196
 
235
197
  post:
236
198
  tags: [tenants]
199
+ x-served-by: app # the tenant model is the application's
237
200
  summary: Create a tenant manually (bypasses the onboarding flow)
238
201
  x-mfa-required: true
239
202
  requestBody:
@@ -262,6 +225,7 @@ paths:
262
225
  schema: { $ref: '#/components/schemas/AdminTenantDetail' }
263
226
  delete:
264
227
  tags: [tenants]
228
+ x-served-by: app # the tenant model is the application's
265
229
  summary: Soft-delete a tenant (set deletedAt)
266
230
  x-mfa-required: true
267
231
  requestBody:
@@ -315,6 +279,7 @@ paths:
315
279
  - $ref: '#/components/parameters/TenantSlug'
316
280
  post:
317
281
  tags: [tenants]
282
+ x-served-by: app # the tenant model is the application's
318
283
  summary: Short-lived JWT as TENANT_ADMIN for support insight
319
284
  x-mfa-required: true
320
285
  requestBody:
@@ -344,6 +309,7 @@ paths:
344
309
  - $ref: '#/components/parameters/TenantSlug'
345
310
  get:
346
311
  tags: [tenants]
312
+ x-served-by: app # the tenant model is the application's
347
313
  summary: GDPR / audit export of all tenant data as a ZIP
348
314
  x-mfa-required: true
349
315
  responses:
@@ -361,6 +327,7 @@ paths:
361
327
  - $ref: '#/components/parameters/TenantSlug'
362
328
  get:
363
329
  tags: [subscriptions]
330
+ x-served-by: app # no admin-side subscription route ships
364
331
  summary: Current subscription + add-ons + effective limits
365
332
  responses:
366
333
  '200':
@@ -371,6 +338,7 @@ paths:
371
338
 
372
339
  patch:
373
340
  tags: [subscriptions]
341
+ x-served-by: app # no admin-side subscription route ships
374
342
  summary: Change plan, custom limits, or pilot flag
375
343
  x-mfa-required: true
376
344
  requestBody:
@@ -435,6 +403,7 @@ paths:
435
403
  schema: { type: string }
436
404
  post:
437
405
  tags: [users]
406
+ x-served-by: app # platform users live in the application's identity store
438
407
  summary: Trigger a password-reset email (sends no plaintext passwords)
439
408
  x-mfa-required: true
440
409
  requestBody:
@@ -457,6 +426,7 @@ paths:
457
426
  schema: { type: string }
458
427
  post:
459
428
  tags: [users]
429
+ x-served-by: app # platform users live in the application's identity store
460
430
  summary: Deactivate a user (block login)
461
431
  x-mfa-required: true
462
432
  requestBody:
@@ -476,6 +446,7 @@ paths:
476
446
  - $ref: '#/components/parameters/TenantSlug'
477
447
  post:
478
448
  tags: [users]
449
+ x-served-by: app # the tenant model is the application's
479
450
  summary: Transfer the TENANT_ADMIN role to another user
480
451
  x-mfa-required: true
481
452
  requestBody:
@@ -546,6 +517,7 @@ paths:
546
517
  schema: { type: string, format: uuid }
547
518
  get:
548
519
  tags: [promo-codes]
520
+ x-served-by: app # the detail view joins application data
549
521
  responses:
550
522
  '200':
551
523
  description: Detail
@@ -622,41 +594,217 @@ paths:
622
594
  items: { $ref: '#/components/schemas/AuditEntryDto' }
623
595
 
624
596
  # ────────────────────────────────────────────────────────
625
- # MFA
597
+ # First-run setup (SetupModule, packages/nest/src/setup/*)
598
+ #
599
+ # Public by design: before the first SUPER_ADMIN there is no session to
600
+ # authenticate. The window is open only while zero SUPER_ADMIN exist and
601
+ # is additionally gated by the operator-set SETUP_TOKEN.
626
602
  # ────────────────────────────────────────────────────────
627
- /mfa/setup:
628
- post:
629
- tags: [mfa]
630
- summary: Start TOTP setup (returns QR-code URI + secret)
603
+ /setup/status:
604
+ get:
605
+ tags: [setup]
606
+ security: [] # PUBLIC answers whether the window is still open
607
+ summary: Whether first-run setup is still open
631
608
  responses:
632
609
  '200':
633
- description: Setup data
610
+ description: Status
611
+ content:
612
+ application/json:
613
+ schema:
614
+ type: object
615
+ required: [needsSetup]
616
+ properties:
617
+ needsSetup:
618
+ type: boolean
619
+ description: true as long as no SUPER_ADMIN exists
620
+
621
+ /setup:
622
+ post:
623
+ tags: [setup]
624
+ security: [] # PUBLIC — see the section note
625
+ summary: Create the first SUPER_ADMIN and start its MFA enrolment
626
+ requestBody:
627
+ required: true
628
+ content:
629
+ application/json:
630
+ schema:
631
+ type: object
632
+ required: [token, email]
633
+ properties:
634
+ token: { type: string, description: Must match SETUP_TOKEN }
635
+ email: { type: string, format: email }
636
+ password:
637
+ type: string
638
+ minLength: 8
639
+ description: Generated by the server when omitted
640
+ responses:
641
+ '201':
642
+ description: Created — the response carries the enrolment secret once
634
643
  content:
635
644
  application/json:
636
645
  schema:
637
646
  type: object
647
+ required: [userId, email, otpauthUri, qrDataUrl, secret]
638
648
  properties:
649
+ userId: { type: string }
650
+ email: { type: string, format: email }
639
651
  otpauthUri: { type: string }
640
- secret: { type: string }
641
- recoveryCodes:
642
- type: array
643
- items: { type: string }
652
+ qrDataUrl: { type: string, description: PNG data URL }
653
+ secret: { type: string, description: Base32 TOTP secret }
654
+ generatedPassword:
655
+ type: string
656
+ description: Present only when the server generated it
657
+ '400':
658
+ description: The email is not an address the platform accepts
659
+ content:
660
+ application/json:
661
+ schema:
662
+ $ref: '#/components/schemas/ErrorResponse'
663
+ '401':
664
+ description: The token does not match SETUP_TOKEN
665
+ content:
666
+ application/json:
667
+ schema:
668
+ $ref: '#/components/schemas/ErrorResponse'
669
+ '403':
670
+ description: SETUP_TOKEN is not set — setup is disabled
671
+ content:
672
+ application/json:
673
+ schema:
674
+ $ref: '#/components/schemas/ErrorResponse'
675
+ '409':
676
+ description: >-
677
+ Either the window has closed itself because a SUPER_ADMIN already
678
+ exists (`SETUP_ALREADY_DONE`), or the address is taken by a platform
679
+ user (`EMAIL_EXISTS`). The `code` tells them apart.
680
+ content:
681
+ application/json:
682
+ schema:
683
+ $ref: '#/components/schemas/ErrorResponse'
644
684
 
645
- /mfa/confirm:
685
+ /setup/confirm-mfa:
646
686
  post:
647
- tags: [mfa]
648
- summary: Confirm setup with the first code
687
+ tags: [setup]
688
+ security: [] # PUBLIC — see the section note
689
+ summary: Confirm the enrolment with the first TOTP code, closing the window
649
690
  requestBody:
650
691
  required: true
651
692
  content:
652
693
  application/json:
653
694
  schema:
654
695
  type: object
655
- required: [code]
696
+ required: [token, userId, code]
656
697
  properties:
698
+ token: { type: string }
699
+ userId: { type: string }
657
700
  code: { type: string, pattern: '^[0-9]{6}$' }
658
701
  responses:
659
- '200': { description: Activated }
702
+ '200':
703
+ description: Activated
704
+ content:
705
+ application/json:
706
+ schema:
707
+ type: object
708
+ required: [ok]
709
+ properties:
710
+ ok: { type: boolean }
711
+ '401':
712
+ description: The token does not match SETUP_TOKEN
713
+ content:
714
+ application/json:
715
+ schema:
716
+ $ref: '#/components/schemas/ErrorResponse'
717
+ '403':
718
+ description: SETUP_TOKEN is not set — setup is disabled
719
+ content:
720
+ application/json:
721
+ schema:
722
+ $ref: '#/components/schemas/ErrorResponse'
723
+
724
+ # ────────────────────────────────────────────────────────
725
+ # Plan catalog import (PlanCatalogImporterModule)
726
+ # ────────────────────────────────────────────────────────
727
+ /billing/plan-catalog/import:
728
+ post:
729
+ tags: [plans]
730
+ summary: Import a plan catalog YAML into the catalog tables
731
+ requestBody:
732
+ required: true
733
+ content:
734
+ application/json:
735
+ schema:
736
+ type: object
737
+ required: [yamlContent]
738
+ properties:
739
+ yamlContent: { type: string, description: Contents of saas.yaml }
740
+ crossFieldChecks:
741
+ type: boolean
742
+ default: true
743
+ description: Validate references between plans, features and quotas
744
+ responses:
745
+ '201':
746
+ description: Import report — the counters say what was new and what already matched
747
+ content:
748
+ application/json:
749
+ schema:
750
+ type: object
751
+ required:
752
+ - plansCreated
753
+ - plansSkipped
754
+ - planVersionsCreated
755
+ - planVersionsSkipped
756
+ - featureEntriesCreated
757
+ - featureEntriesSkipped
758
+ - warnings
759
+ properties:
760
+ plansCreated: { type: integer }
761
+ plansSkipped: { type: integer }
762
+ planVersionsCreated: { type: integer }
763
+ planVersionsSkipped: { type: integer }
764
+ featureEntriesCreated: { type: integer }
765
+ featureEntriesSkipped: { type: integer }
766
+ warnings: { type: array, items: { type: string } }
767
+ '400':
768
+ description: The YAML failed schema validation
769
+ content:
770
+ application/json:
771
+ schema:
772
+ $ref: '#/components/schemas/ErrorResponse'
773
+
774
+ # ────────────────────────────────────────────────────────
775
+ # Subscriptions
776
+ # ────────────────────────────────────────────────────────
777
+ /subscriptions:
778
+ get:
779
+ tags: [subscriptions]
780
+ summary: Every tenant subscription, one row each
781
+ responses:
782
+ '200':
783
+ description: Rows
784
+ content:
785
+ application/json:
786
+ schema:
787
+ type: array
788
+ items:
789
+ type: object
790
+ required: [id, tenant, plan, status, billingCycle]
791
+ properties:
792
+ id: { type: string }
793
+ tenant:
794
+ type: object
795
+ required: [slug, name]
796
+ properties:
797
+ slug: { type: string }
798
+ name: { type: string }
799
+ plan: { type: string }
800
+ status: { type: string }
801
+ billingCycle: { type: string }
802
+ periodEndsAt:
803
+ type: string
804
+ format: date-time
805
+ nullable: true
806
+ monthlyNet: { type: string, nullable: true }
807
+ additionalProperties: true
660
808
 
661
809
  # ────────────────────────────────────────────────────────
662
810
  # Catalog V2 — Plan/Bundle/Marketing/Promotion + Discovery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/spec",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "Language-neutral spec of the SaaS platform: JSON Schemas, OpenAPI contract, Prisma fragments, acceptance scenarios.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",
@@ -11,7 +11,7 @@ related:
11
11
  # Prisma Fragments
12
12
 
13
13
  Prisma-DSL rendering of the SaaSiCat data model, **derived from the normative
14
- sources**: the logical data model in [`docs/data-model.md`](../../../docs/data-model.md)
14
+ sources**: the logical data model in [`docs/explanation/data-model.md`](../../../docs/explanation/data-model.md)
15
15
  and the PostgreSQL artifacts in [`../sql/`](../sql/)
16
16
  (`reference-schema.postgres.sql` is generated FROM these fragments via
17
17
  `pnpm run gen:sql`; `constraints.postgres.sql` carries the invariants Prisma