From d3a8443ebc606b36e13cb2312069fd3364aa3b70 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 01:59:38 +0000 Subject: [PATCH] =?UTF-8?q?feat(spec)!:=20=E5=88=A0=E9=99=A4=20./system=20?= =?UTF-8?q?=E7=9A=84=20declared-only=20tenant-provisioning=20=E5=AE=B6?= =?UTF-8?q?=E6=97=8F=20=E2=80=94=E2=80=94=20C16=20=E5=8F=8C=E6=BA=90?= =?UTF-8?q?=E6=B8=85=E8=B4=A6,TenantPlan=20=E5=94=AF=E4=B8=80=E7=9C=9F?= =?UTF-8?q?=E6=BA=90=E5=9C=A8=20./cloud=20(#4739)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TenantPlan(Schema)` 曾由 `./cloud` 与 `./system` 各自导出一个声明:cloud 侧 5 值(free/starter/pro/enterprise/custom)嵌 `EnvironmentSchema.plan` / `TenantContextSchema.plan` / `ProvisionTenantRequestSchema.plan`,cloud 仓 service-tenant 实际消费;system 侧 3 值子集只嵌 `TenantProvisioningRequest/ Result`,而该 provisioning 协议(连同 contracts 的 `IProvisioningService` / `ITenantRouter`)objectstack / cloud / objectui 三仓 import 语句级实测零实现 零调用 —— declared-only,已被 cloud 的 `Provision*` 家族整体取代。按维护者 裁决(#4739,路线 B)删 system 侧全家族,cloud 侧一字未动。 - 删 system/provisioning.zod.ts(+测试):TenantPlan / TenantRegion / TenantProvisioningStatus / ProvisioningStep / TenantProvisioningRequest / TenantProvisioningResult 六 def 全家 - 删 contracts/provisioning-service.ts、contracts/tenant-router.ts(+测试): 两文件除三个 declared-only 接口外无他物,整文件退役;两 index 留 #4739 指路注释(#4212/#4451 同款) - dual-source-exports.baseline.json:删 TenantPlan / TenantPlanSchema 两行 (12 → 10,rebase 携带 C6 #4745 落地后的起点) - authorable-surface.json:21 行随三个整 def 出账(#4650 门禁判定三组 「def no longer emitted」自证路径,输出见 PR) - json-schema.manifest.json:删六 def 键(#2978 蓄意移除通道) - api-surface.json:恰好 15 行删除(./system 12 + ./contracts 3),零新增 - docs:system/provisioning.mdx 与幽灵页 cloud/provisioning.mdx 随生成器 消失,cloud/TenantPlan 文档回到真家 cloud/tenant.mdx(#4696 病灶随重名 根因消除);docs-import-surface.baseline.json 经生成器收缩一行 - 回归 pin(cloud/tenant.test.ts):compiler-API 断言全入口(package.json exports 枚举)任何叫 TenantPlan(Schema) 的导出唯一解析到 cloud 声明、 ./system 不再命名之、三个契约接口全包消失,防空转守卫;S1(重加声明)/ S2(禁止的 re-export 路线)sabotage 均红 - changeset:@objectstack/spec major(移除 15 个已发布导出名,TS2305), 零元数据迁移;strictness-ledger 实跑零变化(该文件不在台账辖区) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- .changeset/tenant-plan-dual-source-c16.md | 48 +++++ content/docs/references/cloud/meta.json | 1 - .../docs/references/cloud/provisioning.mdx | 32 --- content/docs/references/cloud/tenant.mdx | 17 +- content/docs/references/system/index.mdx | 1 - content/docs/references/system/meta.json | 1 - .../docs/references/system/provisioning.mdx | 142 ------------- packages/spec/api-surface.json | 15 -- packages/spec/authorable-surface.json | 21 -- .../spec/docs-import-surface.baseline.json | 1 - .../spec/dual-source-exports.baseline.json | 4 +- packages/spec/json-schema.manifest.json | 6 - packages/spec/src/cloud/tenant.test.ts | 191 ++++++++++++++++++ packages/spec/src/contracts/index.ts | 9 +- .../contracts/provisioning-service.test.ts | 147 -------------- .../src/contracts/provisioning-service.ts | 82 -------- .../spec/src/contracts/tenant-router.test.ts | 85 -------- packages/spec/src/contracts/tenant-router.ts | 69 ------- packages/spec/src/system/index.ts | 10 +- packages/spec/src/system/provisioning.test.ts | 173 ---------------- packages/spec/src/system/provisioning.zod.ts | 155 -------------- 21 files changed, 271 insertions(+), 939 deletions(-) create mode 100644 .changeset/tenant-plan-dual-source-c16.md delete mode 100644 content/docs/references/cloud/provisioning.mdx delete mode 100644 content/docs/references/system/provisioning.mdx create mode 100644 packages/spec/src/cloud/tenant.test.ts delete mode 100644 packages/spec/src/contracts/provisioning-service.test.ts delete mode 100644 packages/spec/src/contracts/provisioning-service.ts delete mode 100644 packages/spec/src/contracts/tenant-router.test.ts delete mode 100644 packages/spec/src/contracts/tenant-router.ts delete mode 100644 packages/spec/src/system/provisioning.test.ts delete mode 100644 packages/spec/src/system/provisioning.zod.ts diff --git a/.changeset/tenant-plan-dual-source-c16.md b/.changeset/tenant-plan-dual-source-c16.md new file mode 100644 index 0000000000..45fa4eba34 --- /dev/null +++ b/.changeset/tenant-plan-dual-source-c16.md @@ -0,0 +1,48 @@ +--- +"@objectstack/spec": major +--- + +BREAKING(spec): 删除 `@objectstack/spec/system` 的整个 tenant-provisioning 家族(`TenantPlan(Schema)` / `TenantRegion(Schema)` / `TenantProvisioningStatus(Enum)` / `ProvisioningStep(Schema)` / `TenantProvisioningRequest(Schema)` / `TenantProvisioningResult(Schema)`)及 `@objectstack/spec/contracts` 的 `IProvisioningService` / `ITenantRouter` / `ResolvedTenantContext` —— `TenantPlan(Schema)` 现在全包唯一地指 `@objectstack/spec/cloud` 的 5 值声明 (#4739, #4535 C16) + +`TenantPlan` + `TenantPlanSchema` 过去被两个入口导出,但**不是同一个声明**,拿到哪个词表只取决于 import 路径 —— #4411 陷阱: + +| 入口 | 声明位置 | 词表 | 状态 | +|:--|:--|:--|:--| +| `@objectstack/spec/cloud`(**不变,唯一真源**) | `cloud/tenant.zod.ts` | `free / starter / pro / enterprise / custom`(5 值) | **活**:嵌入 `EnvironmentSchema.plan`、`TenantContextSchema.plan`、`ProvisionTenantRequestSchema.plan`,cloud 仓 service-tenant 经这些 schema 实际消费 | +| `@objectstack/spec/system`(**本次删除**) | `system/provisioning.zod.ts` | `free / pro / enterprise`(3 值子集) | **declared-only**:仅嵌入 `TenantProvisioningRequest/Result`,该 provisioning 协议三仓(objectstack / cloud / objectui)零实现零调用 | + +system 侧不是孤立的一个枚举,而是一整套从未实现的 provisioning 协议:`TenantProvisioningRequest/Result` 加上 `contracts` 里的 `IProvisioningService` / `ITenantRouter` 契约,import 语句级三仓实测**零实现、零调用、零 importer**(cloud 仓 `service-tenant` 的 `TenantProvisioningService` 是重名的本地类,消费的是 `@objectstack/spec/cloud` 的 `ProvisionTenantRequest`,与被删家族无关)。它已被 cloud 入口的 `Provision*` 家族整体取代,故按维护者裁决(#4739,路线 B)删除 system 侧全家族,cloud 侧一字未动。 + +## FROM → TO + +```ts +// FROM —— 编译期起将以 TS2305 失败(实测三仓零命中,预期无人受影响) +import { TenantPlanSchema, type TenantPlan } from '@objectstack/spec/system'; +// TO —— 唯一声明(注意词表从 3 值子集变为 5 值全集:多出 starter / custom) +import { TenantPlanSchema, type TenantPlan } from '@objectstack/spec/cloud'; +``` + +```ts +// FROM —— TS2305 +import type { TenantProvisioningRequest, TenantProvisioningResult } from '@objectstack/spec/system'; +import type { IProvisioningService, ITenantRouter, ResolvedTenantContext } from '@objectstack/spec/contracts'; +// TO —— 活的 provisioning 协议在 cloud 入口 +import type { ProvisionTenantRequest, ProvisionTenantResponse, TenantContext } from '@objectstack/spec/cloud'; +``` + +- `TenantRegion(Schema)` / `TenantProvisioningStatus(Enum)` / `ProvisioningStep(Schema)`:无同名替代物。region 与 step 追踪从未接入任何运行时;租户生命周期状态的活表面是 `@objectstack/spec/cloud` 的 `TenantDatabaseStatusSchema` 与 `EnvironmentSchema.status`。 +- 类型收窄注意:改从 `./cloud` import 后,`TenantPlan` 联合类型从 3 值**变宽**为 5 值 —— 对 plan 做穷举 switch 的代码要补 `starter` / `custom` 两个分支(这正是双源曾经隐藏的漂移)。 + +## 定级理由(逐条自证,未照抄前例) + +定 **major**,因为这是**已发布导出名的移除**:外部对上述 15 个导出名(`./system` 12 个 + `./contracts` 3 个)的 import 会以 TS2305 编译失败(与 C14 / C6 同形)。 + +同时它是**零元数据迁移**: + +- 被删 6 个 def(`system/ProvisioningStep` / `system/TenantPlan` / `system/TenantProvisioningRequest` / `system/TenantProvisioningResult` / `system/TenantProvisioningStatusEnum` / `system/TenantRegion`)均不从 `BUILTIN_METADATA_TYPE_SCHEMAS` 元数据根可达 —— #4650 门禁对 `authorable-surface.json` 被删 21 行的实跑判定是三组「def no longer emitted by this build」自证路径(输出见 PR),**无 tombstone、无 ADR-0087 conversion / migration**(`spec-changes.json` / upgrade-guide 两 gate 零变化)。 +- 已存 `sys_metadata` 数据、运行时校验行为全部不受影响;`cloud/tenant.zod.ts` 与 `system/tenant.zod.ts`(`system/Tenant` 行级租户记录,自带内联枚举、不引用被删文件)一字未动。 +- JSON Schema 产物中上述 6 个 def 停止发布(`json-schema.manifest.json` 同步删键,#2978 蓄意移除通道)。 + +## 基线 12 → 10 + +`dual-source-exports.baseline.json` 删掉 `TenantPlan — [./cloud (type)] ≠ [./system (type)]` 与 `TenantPlanSchema — [./cloud (const)] ≠ [./system (const)]` 两行,其余行一字未动。 diff --git a/content/docs/references/cloud/meta.json b/content/docs/references/cloud/meta.json index 59edce314b..baaceae978 100644 --- a/content/docs/references/cloud/meta.json +++ b/content/docs/references/cloud/meta.json @@ -7,7 +7,6 @@ "environment-package", "package", "package-version", - "provisioning", "template-manifest", "---Marketplace & Distribution---", "app-store", diff --git a/content/docs/references/cloud/provisioning.mdx b/content/docs/references/cloud/provisioning.mdx deleted file mode 100644 index f6e92b618a..0000000000 --- a/content/docs/references/cloud/provisioning.mdx +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Provisioning -description: Provisioning protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -## TypeScript Usage - -```typescript -import { TenantPlanSchema } from '@objectstack/spec/cloud'; -import type { TenantPlan } from '@objectstack/spec/cloud'; - -// Validate data -const result = TenantPlanSchema.parse(data); -``` - ---- - -## TenantPlan - -### Allowed Values - -* `free` -* `starter` -* `pro` -* `enterprise` -* `custom` - - ---- - diff --git a/content/docs/references/cloud/tenant.mdx b/content/docs/references/cloud/tenant.mdx index 7729d3e67b..0525c031a6 100644 --- a/content/docs/references/cloud/tenant.mdx +++ b/content/docs/references/cloud/tenant.mdx @@ -28,8 +28,8 @@ Design decisions: ## TypeScript Usage ```typescript -import { PackageInstallationSchema, PackageInstallationStatusSchema, ProvisionTenantRequestSchema, ProvisionTenantResponseSchema, TenantContextSchema, TenantDatabaseSchema, TenantDatabaseStatusSchema, TenantIdentificationSourceSchema, TenantRoutingConfigSchema } from '@objectstack/spec/cloud'; -import type { PackageInstallation, PackageInstallationStatus, ProvisionTenantRequest, ProvisionTenantResponse, TenantContext, TenantDatabase, TenantDatabaseStatus, TenantIdentificationSource, TenantRoutingConfig } from '@objectstack/spec/cloud'; +import { PackageInstallationSchema, PackageInstallationStatusSchema, ProvisionTenantRequestSchema, ProvisionTenantResponseSchema, TenantContextSchema, TenantDatabaseSchema, TenantDatabaseStatusSchema, TenantIdentificationSourceSchema, TenantPlanSchema, TenantRoutingConfigSchema } from '@objectstack/spec/cloud'; +import type { PackageInstallation, PackageInstallationStatus, ProvisionTenantRequest, ProvisionTenantResponse, TenantContext, TenantDatabase, TenantDatabaseStatus, TenantIdentificationSource, TenantPlan, TenantRoutingConfig } from '@objectstack/spec/cloud'; // Validate data const result = PackageInstallationSchema.parse(data); @@ -161,6 +161,19 @@ const result = PackageInstallationSchema.parse(data); * `default` +--- + +## TenantPlan + +### Allowed Values + +* `free` +* `starter` +* `pro` +* `enterprise` +* `custom` + + --- ## TenantRoutingConfig diff --git a/content/docs/references/system/index.mdx b/content/docs/references/system/index.mdx index 111ea39759..c3cc7fdce0 100644 --- a/content/docs/references/system/index.mdx +++ b/content/docs/references/system/index.mdx @@ -31,7 +31,6 @@ This section contains all protocol schemas for the system layer of ObjectStack. - diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json index 56a5dde8a0..52397a038f 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -10,7 +10,6 @@ "environment-artifact", "license", "migration", - "provisioning", "registry-config", "settings-client", "settings-manifest", diff --git a/content/docs/references/system/provisioning.mdx b/content/docs/references/system/provisioning.mdx deleted file mode 100644 index 937d7a5a73..0000000000 --- a/content/docs/references/system/provisioning.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Provisioning -description: Provisioning protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -Tenant Provisioning Protocol - -Defines the schemas for the "Register → Instant ObjectOS" provisioning pipeline: - -1. User registers → ProvisioningRequest created - -2. Turso database created → Schema synced → Seed data applied - -3. Tenant status transitions: provisioning → active - -Provisioning is designed to be: - -- **Idempotent**: Re-running the same request produces the same result - -- **Observable**: Each step has explicit status tracking - -- **Fast**: Target 2-5 seconds for complete provisioning - - -**Source:** `packages/spec/src/system/provisioning.zod.ts` - - -## TypeScript Usage - -```typescript -import { ProvisioningStepSchema, TenantPlanSchema, TenantProvisioningRequestSchema, TenantProvisioningResultSchema, TenantProvisioningStatusEnum, TenantRegionSchema } from '@objectstack/spec/system'; -import type { ProvisioningStep, TenantPlan, TenantProvisioningRequest, TenantProvisioningResult, TenantRegion } from '@objectstack/spec/system'; - -// Validate data -const result = ProvisioningStepSchema.parse(data); -``` - ---- - -## ProvisioningStep - -Individual provisioning step status - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **name** | `string` | ✅ | Step name (e.g., create_database, sync_schema) | -| **status** | `Enum<'pending' \| 'running' \| 'completed' \| 'failed' \| 'skipped'>` | ✅ | Step status | -| **startedAt** | `string` | optional | Step start time | -| **completedAt** | `string` | optional | Step completion time | -| **durationMs** | `integer` | optional | Step duration in ms | -| **error** | `string` | optional | Error message on failure | - - ---- - -## TenantPlan - -Tenant subscription plan - -### Allowed Values - -* `free` -* `pro` -* `enterprise` - - ---- - -## TenantProvisioningRequest - -Tenant provisioning request - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **orgId** | `string` | ✅ | Organization ID | -| **plan** | `Enum<'free' \| 'pro' \| 'enterprise'>` | ✅ | Tenant subscription plan | -| **region** | `Enum<'us-east' \| 'us-west' \| 'eu-west' \| 'eu-central' \| 'ap-southeast' \| 'ap-northeast'>` | ✅ | Available deployment region | -| **displayName** | `string` | optional | Tenant display name | -| **adminEmail** | `string` | optional | Initial admin user email | -| **metadata** | `Record` | optional | Additional metadata | - - ---- - -## TenantProvisioningResult - -Tenant provisioning result - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **tenantId** | `string` | ✅ | Provisioned tenant ID | -| **connectionUrl** | `string` | ✅ | Database connection URL | -| **status** | `Enum<'provisioning' \| 'active' \| 'suspended' \| 'failed' \| 'destroying'>` | ✅ | Tenant provisioning lifecycle status | -| **region** | `Enum<'us-east' \| 'us-west' \| 'eu-west' \| 'eu-central' \| 'ap-southeast' \| 'ap-northeast'>` | ✅ | Available deployment region | -| **plan** | `Enum<'free' \| 'pro' \| 'enterprise'>` | ✅ | Tenant subscription plan | -| **steps** | `{ name: string; status: Enum<'pending' \| 'running' \| 'completed' \| 'failed' \| 'skipped'>; startedAt?: string; completedAt?: string; … }[]` | ✅ | Pipeline step statuses | -| **totalDurationMs** | `integer` | optional | Total provisioning duration | -| **provisionedAt** | `string` | optional | Provisioning completion time | -| **error** | `string` | optional | Error message on failure | - - ---- - -## TenantProvisioningStatusEnum - -Tenant provisioning lifecycle status - -### Allowed Values - -* `provisioning` -* `active` -* `suspended` -* `failed` -* `destroying` - - ---- - -## TenantRegion - -Available deployment region - -### Allowed Values - -* `us-east` -* `us-west` -* `eu-west` -* `eu-central` -* `ap-southeast` -* `ap-northeast` - - ---- - diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 0df9f27c84..b68d83f137 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1162,8 +1162,6 @@ "PlanSchema (const)", "PresignedUrlConfig (type)", "PresignedUrlConfigSchema (const)", - "ProvisioningStep (type)", - "ProvisioningStepSchema (const)", "PushNotification (type)", "PushNotificationSchema (const)", "QueueConfig (type)", @@ -1324,18 +1322,8 @@ "TenantIsolationConfig (type)", "TenantIsolationConfigSchema (const)", "TenantIsolationLevel (type)", - "TenantPlan (type)", - "TenantPlanSchema (const)", - "TenantProvisioningRequest (type)", - "TenantProvisioningRequestSchema (const)", - "TenantProvisioningResult (type)", - "TenantProvisioningResultSchema (const)", - "TenantProvisioningStatus (type)", - "TenantProvisioningStatusEnum (const)", "TenantQuota (type)", "TenantQuotaSchema (const)", - "TenantRegion (type)", - "TenantRegionSchema (const)", "TenantSchema (const)", "TenantSecurityPolicy (type)", "TenantSecurityPolicyInput (type)", @@ -3752,7 +3740,6 @@ "IPackageService (interface)", "IPluginLifecycleEvents (interface)", "IPluginValidator (interface)", - "IProvisioningService (interface)", "IPubSub (interface)", "IQueueService (interface)", "IRealtimeService (interface)", @@ -3772,7 +3759,6 @@ "IStartupOrchestrator (interface)", "IStorageService (interface)", "ITeamGraphService (interface)", - "ITenantRouter (interface)", "ImportObjectOpts (interface)", "ImportObjectResult (interface)", "InboxListResult (interface)", @@ -3853,7 +3839,6 @@ "ReportSchedule (interface)", "ResolveDependenciesInput (interface)", "ResolveShareLinkResult (interface)", - "ResolvedTenantContext (interface)", "ResumeSignal (interface)", "RlsMembershipContext (interface)", "RollbackInput (interface)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 44a722918a..3fbabd7ab8 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -6569,12 +6569,6 @@ "system/PresignedUrlConfig:operation", "system/PresignedUrlConfig:responseContentDisposition", "system/PresignedUrlConfig:responseContentType", - "system/ProvisioningStep:completedAt", - "system/ProvisioningStep:durationMs", - "system/ProvisioningStep:error", - "system/ProvisioningStep:name", - "system/ProvisioningStep:startedAt", - "system/ProvisioningStep:status", "system/PushNotification:actions", "system/PushNotification:badge", "system/PushNotification:body", @@ -6887,21 +6881,6 @@ "system/TenantConnectionConfig:authToken", "system/TenantConnectionConfig:group", "system/TenantConnectionConfig:url", - "system/TenantProvisioningRequest:adminEmail", - "system/TenantProvisioningRequest:displayName", - "system/TenantProvisioningRequest:metadata", - "system/TenantProvisioningRequest:orgId", - "system/TenantProvisioningRequest:plan", - "system/TenantProvisioningRequest:region", - "system/TenantProvisioningResult:connectionUrl", - "system/TenantProvisioningResult:error", - "system/TenantProvisioningResult:plan", - "system/TenantProvisioningResult:provisionedAt", - "system/TenantProvisioningResult:region", - "system/TenantProvisioningResult:status", - "system/TenantProvisioningResult:steps", - "system/TenantProvisioningResult:tenantId", - "system/TenantProvisioningResult:totalDurationMs", "system/TenantQuota:apiRateLimit", "system/TenantQuota:maxDeploymentsPerDay", "system/TenantQuota:maxObjects", diff --git a/packages/spec/docs-import-surface.baseline.json b/packages/spec/docs-import-surface.baseline.json index 336f91189c..ce1ed1a8a7 100644 --- a/packages/spec/docs-import-surface.baseline.json +++ b/packages/spec/docs-import-surface.baseline.json @@ -103,7 +103,6 @@ "system/ServiceConfig — no type export", "system/ServiceCriticality — no type export", "system/ServiceStatus — no type export", - "system/TenantProvisioningStatusEnum — no type export", "ui/AIChatWindowProps — no type export", "ui/ActionType — no type export", "ui/BreakpointColumnMap — no type export", diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index 0deda44bb0..6b3e88cf66 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -10,8 +10,6 @@ "EnvironmentArtifactInput — [./cloud (type)] ≠ [./system (type)]", "EnvironmentArtifactSchema — [./cloud (const)] ≠ [./system (const)]", "PackageDependency — [./cloud (type)] ≠ [./kernel (type)]", - "PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]", - "TenantPlan — [./cloud (type)] ≠ [./system (type)]", - "TenantPlanSchema — [./cloud (const)] ≠ [./system (const)]" + "PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]" ] } diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index fed09a6893..b1a87203ef 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1392,7 +1392,6 @@ "system/PackagePublishResult", "system/Plan", "system/PresignedUrlConfig", - "system/ProvisioningStep", "system/PushNotification", "system/QueueConfig", "system/QuotaEnforcementResult", @@ -1465,12 +1464,7 @@ "system/TenantConnectionConfig", "system/TenantIsolationConfig", "system/TenantIsolationLevel", - "system/TenantPlan", - "system/TenantProvisioningRequest", - "system/TenantProvisioningResult", - "system/TenantProvisioningStatusEnum", "system/TenantQuota", - "system/TenantRegion", "system/TenantSecurityPolicy", "system/TenantUsage", "system/TextCRDTOperation", diff --git a/packages/spec/src/cloud/tenant.test.ts b/packages/spec/src/cloud/tenant.test.ts new file mode 100644 index 0000000000..2387404307 --- /dev/null +++ b/packages/spec/src/cloud/tenant.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { TenantPlanSchema } from './tenant.zod'; + +// ─── [#4739] `TenantPlan(Schema)` has ONE declaration — ./cloud ────────────── +// +// `./cloud` and `./system` both exported a `TenantPlan` + `TenantPlanSchema`, +// for two different declarations (the #4411 trap — which vocabulary a consumer +// got depended on the import path): +// +// cloud/tenant.zod.ts → 5-value plan tier (free / starter / pro / +// enterprise / custom) — the LIVE declaration: embedded in +// `EnvironmentSchema.plan`, `TenantContextSchema.plan`, +// `ProvisionTenantRequestSchema.plan`, and consumed by the cloud repo's +// service-tenant through those schemas. +// system/provisioning.zod.ts (removed) → 3-value subset (free / pro / +// enterprise) embedded only in `TenantProvisioningRequest/Result` — a +// provisioning protocol with zero implementations and zero importers in +// any repo (objectstack / cloud / objectui), superseded by the cloud +// `Provision*` family. Its companion contracts `IProvisioningService` and +// `ITenantRouter` (./contracts) were equally declared-only and retired in +// the same change. +// +// Maintainer ruling on #4739 (ledger #4535 C16): route B — delete the +// system-side provisioning family, cloud keeps the name. NOT a re-export +// convergence: the ruling is that the name LEAVES ./system entirely, so a +// future `export { TenantPlanSchema } from '../cloud/...'` in ./system is a +// forbidden route even though the dual-source gate (symbol identity) would +// not flag it — the uniqueness pin below catches exactly that (S2 sabotage). +// +// #4642 established that a compile-time conditional-type pin in this package +// is a no-op (tsconfig excludes `**/*.test.ts`; vitest never enables +// `typecheck`), so the load-bearing pin is the compiler-API test below, with +// anti-vacuity guards; sabotage-verified in the PR. +describe('[#4739] `TenantPlan(Schema)` resolves to the ./cloud declaration everywhere', () => { + it('resolves the export surface: only ./cloud declares `TenantPlan(Schema)`; the provisioning family is gone', async () => { + const ts = (await import('typescript')).default; + const { resolve, relative, dirname } = await import('node:path'); + const { fileURLToPath } = await import('node:url'); + const { readFileSync } = await import('node:fs'); + + const specDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + // Every public entry point, read from package.json's exports map so a + // future entry cannot silently escape the uniqueness pin below. + const pkg = JSON.parse(readFileSync(resolve(specDir, 'package.json'), 'utf8')) as { + exports: Record; + }; + const entries: Record = {}; + for (const sub of Object.keys(pkg.exports)) { + if (sub === '.') entries[sub] = resolve(specDir, 'src/index.ts'); + else if (/^\.\/[a-z-]+$/.test(sub)) entries[sub] = resolve(specDir, `src/${sub.slice(2)}/index.ts`); + // './openapi.json' / './package.json' are not TypeScript entry points. + } + // Anti-vacuity: the enumeration must have found the real surface. + expect(Object.keys(entries)).toContain('./cloud'); + expect(Object.keys(entries)).toContain('./system'); + expect(Object.keys(entries)).toContain('./contracts'); + expect(Object.keys(entries).length).toBeGreaterThan(10); + + const program = ts.createProgram(Object.values(entries), { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + noEmit: true, + }); + const checker = program.getTypeChecker(); + const unalias = (s: import('typescript').Symbol) => + s.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s; + + const exportsOf = (sub: string) => { + const sf = program.getSourceFile(entries[sub]); + const moduleSym = sf && checker.getSymbolAtLocation(sf); + // Without this guard a resolution failure would make every assertion + // below pass vacuously — the exact way a gate goes dormant (#4642). + expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy(); + return checker.getExportsOfModule(moduleSym!); + }; + + const originOf = (sym: import('typescript').Symbol, label: string) => { + const decl = unalias(sym).declarations?.[0]; + expect(decl, `${label} must have a declaration`).toBeTruthy(); + const declFile = decl!.getSourceFile(); + return `${relative(specDir, declFile.fileName)}:${ + declFile.getLineAndCharacterOfPosition(decl!.getStart()).line + 1 + }`; + }; + + // 1. The removed side: `./system` still has a non-trivial surface — so + // the `not.toContain` cannot pass by resolving nothing — and no longer + // names any of the provisioning family, while its surviving + // multi-tenant neighbours stand. + const systemExports = exportsOf('./system'); + expect(systemExports.length, './system must export a non-trivial surface').toBeGreaterThan(100); + const systemNames = systemExports.map((e) => e.getName()); + for (const gone of [ + 'TenantPlan', 'TenantPlanSchema', + 'TenantRegion', 'TenantRegionSchema', + 'TenantProvisioningStatus', 'TenantProvisioningStatusEnum', + 'ProvisioningStep', 'ProvisioningStepSchema', + 'TenantProvisioningRequest', 'TenantProvisioningRequestSchema', + 'TenantProvisioningResult', 'TenantProvisioningResultSchema', + ]) { + expect(systemNames, `./system must not name ${gone}`).not.toContain(gone); + } + expect(systemNames).toContain('TenantSchema'); + expect(systemNames).toContain('TenantIsolationLevel'); + + // 2. The removed contracts: `./contracts` keeps its surface but the + // declared-only provisioning pair is gone. + const contractsExports = exportsOf('./contracts'); + expect(contractsExports.length, './contracts must export a non-trivial surface').toBeGreaterThan(50); + const contractsNames = contractsExports.map((e) => e.getName()); + for (const gone of ['IProvisioningService', 'ITenantRouter', 'ResolvedTenantContext']) { + expect(contractsNames, `./contracts must not name ${gone}`).not.toContain(gone); + } + expect(contractsNames).toContain('ISchemaDiffService'); + + // 3. The surviving side: `./cloud` exports the const and its inferred + // type, declared in cloud/tenant.zod.ts. + const cloudExports = exportsOf('./cloud'); + const cloudConst = cloudExports.find((e) => e.getName() === 'TenantPlanSchema'); + const cloudType = cloudExports.find((e) => e.getName() === 'TenantPlan'); + expect(cloudConst, './cloud must export `TenantPlanSchema`').toBeTruthy(); + expect(cloudType, './cloud must export `TenantPlan`').toBeTruthy(); + const constOrigin = originOf(cloudConst!, './cloud TenantPlanSchema'); + const typeOrigin = originOf(cloudType!, './cloud TenantPlan'); + expect(constOrigin).toMatch(/^src\/cloud\/tenant\.zod\.ts:\d+$/); + expect(typeOrigin).toMatch(/^src\/cloud\/tenant\.zod\.ts:\d+$/); + + // 4. Uniqueness — the dual-source pin proper: across EVERY public entry, + // an export named `TenantPlan` / `TenantPlanSchema` must resolve to + // that ONE cloud declaration — and `./system` must not be a holder at + // all, so even a same-symbol re-export (invisible to the dual-source + // gate) violates the ruling and turns this red. + const holders: Record = { TenantPlan: [], TenantPlanSchema: [] }; + const canonical: Record = { + TenantPlan: typeOrigin, + TenantPlanSchema: constOrigin, + }; + for (const sub of Object.keys(entries)) { + for (const name of Object.keys(holders)) { + for (const sym of exportsOf(sub).filter((e) => e.getName() === name)) { + holders[name].push(sub); + expect( + originOf(sym, `${sub} ${name}`), + `${sub} must resolve \`${name}\` to the cloud declaration`, + ).toBe(canonical[name]); + } + } + } + expect(holders.TenantPlanSchema).toContain('./cloud'); + expect(holders.TenantPlanSchema).not.toContain('./system'); + expect(holders.TenantPlan).toContain('./cloud'); + expect(holders.TenantPlan).not.toContain('./system'); + + // 5. The retired contract interfaces exist in NO entry any more. + for (const sub of Object.keys(entries)) { + const names = exportsOf(sub).map((e) => e.getName()); + for (const gone of ['IProvisioningService', 'ITenantRouter', 'ResolvedTenantContext']) { + expect(names, `${sub} must not name ${gone}`).not.toContain(gone); + } + } + }); + + it('keeps the runtime namespaces consistent with the compiler view', async () => { + const cloud = await import('./index'); + const system = await import('../system/index'); + expect('TenantPlanSchema' in cloud).toBe(true); + expect('TenantPlanSchema' in system).toBe(false); + expect('TenantProvisioningRequestSchema' in system).toBe(false); + expect('TenantProvisioningResultSchema' in system).toBe(false); + expect('TenantRegionSchema' in system).toBe(false); + expect('ProvisioningStepSchema' in system).toBe(false); + }); + + it('what the name now unambiguously means: the 5-value cloud plan vocabulary', () => { + // The full surviving vocabulary — an enum def has no authorable keys, so + // the vocabulary IS the surface this pin protects. + for (const plan of ['free', 'starter', 'pro', 'enterprise', 'custom']) { + expect(() => TenantPlanSchema.parse(plan)).not.toThrow(); + } + // `starter` / `custom` are the values the deleted 3-value system subset + // rejected: their acceptance proves the survivor is the cloud declaration, + // not the provisioning one. + expect(TenantPlanSchema.parse('starter')).toBe('starter'); + // And it is still a closed enum, not an open string. + expect(() => TenantPlanSchema.parse('solo')).toThrow(); + expect(() => TenantPlanSchema.parse('')).toThrow(); + }); +}); diff --git a/packages/spec/src/contracts/index.ts b/packages/spec/src/contracts/index.ts index be0b3d0187..d428edbef9 100644 --- a/packages/spec/src/contracts/index.ts +++ b/packages/spec/src/contracts/index.ts @@ -54,11 +54,16 @@ export * from './knowledge-adapter.js'; export * from './embedder.js'; // Provisioning & Deployment -export * from './provisioning-service.js'; +// './provisioning-service.js' (IProvisioningService) and './tenant-router.js' +// (ITenantRouter / ResolvedTenantContext) removed (#4739, v17): both +// contracts had zero implementations and zero call sites in any repo +// (objectstack / cloud / objectui) — declared-only fiction over the +// system-side provisioning family retired in the same change (#4535 C16). +// The living provisioning contract is the `Provision*` family in +// `@objectstack/spec/cloud`, consumed by the cloud services (service-tenant). export * from './schema-diff-service.js'; export * from './external-datasource-service.js'; export * from './deploy-pipeline-service.js'; -export * from './tenant-router.js'; export * from './app-lifecycle-service.js'; export * from './seed-loader-service.js'; export * from './crypto-provider.js'; diff --git a/packages/spec/src/contracts/provisioning-service.test.ts b/packages/spec/src/contracts/provisioning-service.test.ts deleted file mode 100644 index 08276a09cc..0000000000 --- a/packages/spec/src/contracts/provisioning-service.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { describe, it, expect } from 'vitest'; -import type { IProvisioningService } from './provisioning-service'; -import type { TenantProvisioningResult, TenantProvisioningStatus } from '../system/provisioning.zod'; - -describe('Provisioning Service Contract', () => { - it('should allow a minimal IProvisioningService implementation with all required methods', () => { - const service: IProvisioningService = { - provisionTenant: async () => ({ - tenantId: 'tenant_001', - connectionUrl: 'libsql://tenant-001.turso.io', - status: 'active', - region: 'us-east', - plan: 'free', - steps: [], - }), - suspendTenant: async () => {}, - resumeTenant: async () => {}, - destroyTenant: async () => {}, - getTenantStatus: async () => 'active', - migrateTenantPlan: async () => {}, - }; - - expect(typeof service.provisionTenant).toBe('function'); - expect(typeof service.suspendTenant).toBe('function'); - expect(typeof service.resumeTenant).toBe('function'); - expect(typeof service.destroyTenant).toBe('function'); - expect(typeof service.getTenantStatus).toBe('function'); - expect(typeof service.migrateTenantPlan).toBe('function'); - }); - - it('should provision a new tenant', async () => { - const tenants = new Map(); - let counter = 0; - - const service: IProvisioningService = { - provisionTenant: async (request) => { - const tenantId = `tenant_${++counter}`; - const result: TenantProvisioningResult = { - tenantId, - connectionUrl: `libsql://${tenantId}.turso.io`, - status: 'active', - region: request.region ?? 'us-east', - plan: request.plan ?? 'free', - steps: [ - { name: 'create_database', status: 'completed', durationMs: 500 }, - { name: 'sync_schema', status: 'completed', durationMs: 800 }, - { name: 'seed_data', status: 'completed', durationMs: 200 }, - ], - totalDurationMs: 1500, - provisionedAt: new Date().toISOString(), - }; - tenants.set(tenantId, result); - return result; - }, - suspendTenant: async () => {}, - resumeTenant: async () => {}, - destroyTenant: async () => {}, - getTenantStatus: async (tenantId) => { - const t = tenants.get(tenantId); - return t?.status ?? 'failed'; - }, - migrateTenantPlan: async () => {}, - }; - - const result = await service.provisionTenant({ - orgId: 'org_abc', - plan: 'pro', - region: 'eu-west', - }); - - expect(result.tenantId).toBe('tenant_1'); - expect(result.status).toBe('active'); - expect(result.plan).toBe('pro'); - expect(result.region).toBe('eu-west'); - expect(result.steps).toHaveLength(3); - expect(result.totalDurationMs).toBe(1500); - - const status = await service.getTenantStatus('tenant_1'); - expect(status).toBe('active'); - }); - - it('should manage tenant lifecycle (suspend/resume/destroy)', async () => { - const statuses = new Map([ - ['tenant_001', 'active'], - ]); - - const service: IProvisioningService = { - provisionTenant: async () => ({ - tenantId: 'tenant_001', - connectionUrl: 'libsql://tenant-001.turso.io', - status: 'active', - region: 'us-east', - plan: 'free', - steps: [], - }), - suspendTenant: async (tenantId) => { - statuses.set(tenantId, 'suspended'); - }, - resumeTenant: async (tenantId) => { - statuses.set(tenantId, 'active'); - }, - destroyTenant: async (tenantId) => { - statuses.set(tenantId, 'destroying'); - }, - getTenantStatus: async (tenantId) => statuses.get(tenantId) ?? 'failed', - migrateTenantPlan: async () => {}, - }; - - expect(await service.getTenantStatus('tenant_001')).toBe('active'); - - await service.suspendTenant('tenant_001'); - expect(await service.getTenantStatus('tenant_001')).toBe('suspended'); - - await service.resumeTenant('tenant_001'); - expect(await service.getTenantStatus('tenant_001')).toBe('active'); - - await service.destroyTenant('tenant_001'); - expect(await service.getTenantStatus('tenant_001')).toBe('destroying'); - }); - - it('should migrate a tenant plan', async () => { - let currentPlan = 'free'; - - const service: IProvisioningService = { - provisionTenant: async () => ({ - tenantId: 'tenant_001', - connectionUrl: 'libsql://tenant-001.turso.io', - status: 'active', - region: 'us-east', - plan: currentPlan as any, - steps: [], - }), - suspendTenant: async () => {}, - resumeTenant: async () => {}, - destroyTenant: async () => {}, - getTenantStatus: async () => 'active', - migrateTenantPlan: async (_tenantId, newPlan) => { - currentPlan = newPlan; - }, - }; - - await service.migrateTenantPlan('tenant_001', 'enterprise'); - expect(currentPlan).toBe('enterprise'); - }); -}); diff --git a/packages/spec/src/contracts/provisioning-service.ts b/packages/spec/src/contracts/provisioning-service.ts deleted file mode 100644 index 21eb39ab6d..0000000000 --- a/packages/spec/src/contracts/provisioning-service.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * IProvisioningService - Tenant Provisioning Service Contract - * - * Defines the interface for the "Register → Instant ObjectOS" provisioning pipeline. - * Manages the complete tenant lifecycle: create → active → suspend → resume → destroy. - * - * The provisioning service orchestrates: - * 1. Tenant database creation (e.g. via a Turso platform client in cloud builds) - * 2. Schema synchronization via ISchemaDiffService - * 3. Seed data population - * 4. Tenant record registration in the control plane - * - * Follows Dependency Inversion Principle - consumers depend on this interface, - * not on concrete provisioning implementations. - */ - -import type { - TenantProvisioningRequest, - TenantProvisioningResult, - TenantProvisioningStatus, - TenantPlan, -} from '../system/provisioning.zod.js'; - -// ========================================================================== -// Service Interface -// ========================================================================== - -export interface IProvisioningService { - /** - * Provision a new tenant with an isolated database. - * Creates the Turso database, syncs schema, seeds data, and registers the tenant. - * Target latency: 2-5 seconds. - * - * @param request - Provisioning request with org, plan, and region - * @returns Provisioning result with tenant ID, connection URL, and step statuses - */ - provisionTenant(request: TenantProvisioningRequest): Promise; - - /** - * Suspend a tenant (e.g., unpaid, policy violation). - * Revokes tokens and sets the database to read-only mode. - * - * @param tenantId - Tenant to suspend - */ - suspendTenant(tenantId: string): Promise; - - /** - * Resume a previously suspended tenant. - * Restores full read-write access and issues new tokens. - * - * @param tenantId - Tenant to resume - */ - resumeTenant(tenantId: string): Promise; - - /** - * Permanently destroy a tenant and its database. - * Optionally creates a final backup before deletion. - * Respects the grace period defined in lifecycle hooks. - * - * @param tenantId - Tenant to destroy - */ - destroyTenant(tenantId: string): Promise; - - /** - * Get the current provisioning status of a tenant. - * - * @param tenantId - Tenant to query - * @returns Current provisioning status - */ - getTenantStatus(tenantId: string): Promise; - - /** - * Migrate a tenant to a different subscription plan. - * Updates quotas and resource limits accordingly. - * - * @param tenantId - Tenant to migrate - * @param newPlan - Target subscription plan - */ - migrateTenantPlan(tenantId: string, newPlan: TenantPlan): Promise; -} diff --git a/packages/spec/src/contracts/tenant-router.test.ts b/packages/spec/src/contracts/tenant-router.test.ts deleted file mode 100644 index e378036fcf..0000000000 --- a/packages/spec/src/contracts/tenant-router.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { describe, it, expect } from 'vitest'; -import type { ITenantRouter, ResolvedTenantContext } from './tenant-router'; - -describe('Tenant Router Contract', () => { - it('should allow a minimal ITenantRouter implementation with all required methods', () => { - const router: ITenantRouter = { - resolveTenant: async () => null, - getTenantClient: async () => ({}), - invalidateCache: () => {}, - }; - - expect(typeof router.resolveTenant).toBe('function'); - expect(typeof router.getTenantClient).toBe('function'); - expect(typeof router.invalidateCache).toBe('function'); - }); - - it('should resolve a tenant context from a session', async () => { - const tenants = new Map([ - ['session_abc', { - tenantId: 'tenant_001', - plan: 'pro', - region: 'us-east', - dbUrl: 'libsql://tenant-001.turso.io', - status: 'active', - }], - ]); - - const router: ITenantRouter = { - resolveTenant: async (session) => { - const s = session as { sessionId: string }; - return tenants.get(s.sessionId) ?? null; - }, - getTenantClient: async () => ({}), - invalidateCache: () => {}, - }; - - const ctx = await router.resolveTenant({ sessionId: 'session_abc' }); - expect(ctx).not.toBeNull(); - expect(ctx!.tenantId).toBe('tenant_001'); - expect(ctx!.plan).toBe('pro'); - expect(ctx!.region).toBe('us-east'); - expect(ctx!.status).toBe('active'); - }); - - it('should return null for unknown sessions', async () => { - const router: ITenantRouter = { - resolveTenant: async () => null, - getTenantClient: async () => ({}), - invalidateCache: () => {}, - }; - - const ctx = await router.resolveTenant({ sessionId: 'unknown' }); - expect(ctx).toBeNull(); - }); - - it('should get a tenant client by tenantId', async () => { - const router: ITenantRouter = { - resolveTenant: async () => null, - getTenantClient: async (tenantId) => ({ tenantId, connected: true }), - invalidateCache: () => {}, - }; - - const client = await router.getTenantClient('tenant_001') as any; - expect(client.tenantId).toBe('tenant_001'); - expect(client.connected).toBe(true); - }); - - it('should invalidate cache for a tenant', () => { - const cache = new Set(['tenant_001', 'tenant_002']); - - const router: ITenantRouter = { - resolveTenant: async () => null, - getTenantClient: async () => ({}), - invalidateCache: (tenantId) => { - cache.delete(tenantId); - }, - }; - - router.invalidateCache('tenant_001'); - expect(cache.has('tenant_001')).toBe(false); - expect(cache.has('tenant_002')).toBe(true); - }); -}); diff --git a/packages/spec/src/contracts/tenant-router.ts b/packages/spec/src/contracts/tenant-router.ts deleted file mode 100644 index f491fefce0..0000000000 --- a/packages/spec/src/contracts/tenant-router.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * ITenantRouter - Tenant-Aware Request Routing Contract - * - * Resolves the correct tenant context and database client for each request. - * Works with the multi-tenant router to map sessions to tenant databases. - * - * Request flow: - * 1. Extract tenant identity from request (header, subdomain, JWT) - * 2. Look up tenant configuration from control plane - * 3. Return or create a cached database client for the tenant - * 4. Attach tenant context to the request for downstream services - */ - -import type { TenantProvisioningStatus, TenantPlan, TenantRegion } from '../system/provisioning.zod.js'; - -// ========================================================================== -// Types -// ========================================================================== - -/** - * Resolved tenant context attached to each request. - */ -export interface ResolvedTenantContext { - /** Unique tenant identifier */ - tenantId: string; - /** Tenant subscription plan */ - plan: TenantPlan; - /** Deployment region */ - region: TenantRegion; - /** Database connection URL */ - dbUrl: string; - /** Current provisioning status */ - status: TenantProvisioningStatus; -} - -// ========================================================================== -// Service Interface -// ========================================================================== - -export interface ITenantRouter { - /** - * Resolve tenant context from an authenticated session. - * Extracts tenant identity using the configured resolver strategy - * and looks up the full tenant configuration. - * - * @param session - Authenticated session (implementation-specific) - * @returns Resolved tenant context or null if tenant not found - */ - resolveTenant(session: unknown): Promise; - - /** - * Get or create a database client for a specific tenant. - * Uses connection caching with LRU eviction. - * - * @param tenantId - Tenant identifier - * @returns Database client instance (implementation-specific) - */ - getTenantClient(tenantId: string): Promise; - - /** - * Invalidate the cached connection for a tenant. - * Called when tenant configuration changes (e.g., plan migration, suspension). - * - * @param tenantId - Tenant whose cache to invalidate - */ - invalidateCache(tenantId: string): void; -} diff --git a/packages/spec/src/system/index.ts b/packages/spec/src/system/index.ts index 3ec4130945..c0ac38762e 100644 --- a/packages/spec/src/system/index.ts +++ b/packages/spec/src/system/index.ts @@ -77,7 +77,15 @@ export * from './license.zod'; export * from './registry-config.zod'; // Provisioning & Deployment -export * from './provisioning.zod'; +// './provisioning.zod' removed (#4739, v17, dual-source #4535 C16): the +// system-side tenant-provisioning family (TenantPlan / TenantRegion / +// TenantProvisioningStatus / ProvisioningStep / TenantProvisioningRequest / +// TenantProvisioningResult) was declared-only — zero implementations and zero +// importers across objectstack / cloud / objectui — and its +// `TenantPlan(Schema)` collided with the live declaration in `./cloud`. The +// living provisioning surface is the `Provision*` family in +// `@objectstack/spec/cloud` (cloud/tenant.zod.ts, cloud/environment.zod.ts), +// which the cloud services actually consume. export * from './deploy-bundle.zod'; export * from './app-install.zod'; export * from './environment-artifact.zod'; diff --git a/packages/spec/src/system/provisioning.test.ts b/packages/spec/src/system/provisioning.test.ts deleted file mode 100644 index c020b30903..0000000000 --- a/packages/spec/src/system/provisioning.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - TenantProvisioningStatusEnum, - TenantPlanSchema, - TenantRegionSchema, - ProvisioningStepSchema, - TenantProvisioningRequestSchema, - TenantProvisioningResultSchema, - type TenantProvisioningStatus, - type TenantPlan, - type TenantRegion, - type ProvisioningStep, - type TenantProvisioningRequest, - type TenantProvisioningResult, -} from './provisioning.zod'; - -describe('TenantProvisioningStatusEnum', () => { - it('should accept valid statuses', () => { - const statuses: TenantProvisioningStatus[] = ['provisioning', 'active', 'suspended', 'failed', 'destroying']; - statuses.forEach((s) => { - expect(() => TenantProvisioningStatusEnum.parse(s)).not.toThrow(); - }); - }); - - it('should reject invalid status', () => { - expect(() => TenantProvisioningStatusEnum.parse('pending')).toThrow(); - }); -}); - -describe('TenantPlanSchema', () => { - it('should accept valid plans', () => { - const plans: TenantPlan[] = ['free', 'pro', 'enterprise']; - plans.forEach((p) => { - expect(() => TenantPlanSchema.parse(p)).not.toThrow(); - }); - }); - - it('should reject invalid plan', () => { - expect(() => TenantPlanSchema.parse('basic')).toThrow(); - }); -}); - -describe('TenantRegionSchema', () => { - it('should accept valid regions', () => { - const regions: TenantRegion[] = ['us-east', 'us-west', 'eu-west', 'eu-central', 'ap-southeast', 'ap-northeast']; - regions.forEach((r) => { - expect(() => TenantRegionSchema.parse(r)).not.toThrow(); - }); - }); - - it('should reject invalid region', () => { - expect(() => TenantRegionSchema.parse('us-south')).toThrow(); - }); -}); - -describe('ProvisioningStepSchema', () => { - it('should accept a completed step', () => { - const step: ProvisioningStep = { - name: 'create_database', - status: 'completed', - startedAt: '2026-01-01T00:00:00Z', - completedAt: '2026-01-01T00:00:02Z', - durationMs: 2000, - }; - const parsed = ProvisioningStepSchema.parse(step); - expect(parsed.name).toBe('create_database'); - expect(parsed.status).toBe('completed'); - }); - - it('should accept a pending step with minimal fields', () => { - const step = { name: 'sync_schema', status: 'pending' }; - expect(() => ProvisioningStepSchema.parse(step)).not.toThrow(); - }); - - it('should accept a failed step with error', () => { - const step = { - name: 'create_database', - status: 'failed', - error: 'Turso API rate limit exceeded', - }; - expect(() => ProvisioningStepSchema.parse(step)).not.toThrow(); - }); - - it('should accept all step statuses', () => { - const statuses = ['pending', 'running', 'completed', 'failed', 'skipped']; - statuses.forEach((status) => { - expect(() => ProvisioningStepSchema.parse({ name: 'test', status })).not.toThrow(); - }); - }); - - it('should reject empty step name', () => { - expect(() => ProvisioningStepSchema.parse({ name: '', status: 'pending' })).toThrow(); - }); -}); - -describe('TenantProvisioningRequestSchema', () => { - it('should accept full request', () => { - const request: TenantProvisioningRequest = { - orgId: 'org_123', - plan: 'pro', - region: 'eu-west', - displayName: 'Acme Corp', - adminEmail: 'admin@acme.com', - metadata: { industry: 'tech' }, - }; - const parsed = TenantProvisioningRequestSchema.parse(request); - expect(parsed.orgId).toBe('org_123'); - expect(parsed.plan).toBe('pro'); - expect(parsed.region).toBe('eu-west'); - }); - - it('should accept minimal request with defaults', () => { - const request = { orgId: 'org_456' }; - const parsed = TenantProvisioningRequestSchema.parse(request); - expect(parsed.plan).toBe('free'); - expect(parsed.region).toBe('us-east'); - expect(parsed.displayName).toBeUndefined(); - }); - - it('should reject missing orgId', () => { - expect(() => TenantProvisioningRequestSchema.parse({})).toThrow(); - }); - - it('should reject invalid email', () => { - expect(() => TenantProvisioningRequestSchema.parse({ - orgId: 'org_789', - adminEmail: 'not-an-email', - })).toThrow(); - }); -}); - -describe('TenantProvisioningResultSchema', () => { - it('should accept full result', () => { - const result: TenantProvisioningResult = { - tenantId: 'tenant_abc', - connectionUrl: 'libsql://tenant-abc-myorg.turso.io', - status: 'active', - region: 'us-east', - plan: 'free', - steps: [ - { name: 'create_database', status: 'completed', durationMs: 1500 }, - { name: 'sync_schema', status: 'completed', durationMs: 800 }, - ], - totalDurationMs: 2300, - provisionedAt: '2026-01-01T00:00:03Z', - }; - const parsed = TenantProvisioningResultSchema.parse(result); - expect(parsed.tenantId).toBe('tenant_abc'); - expect(parsed.status).toBe('active'); - expect(parsed.steps).toHaveLength(2); - }); - - it('should accept failed result with error', () => { - const result = { - tenantId: 'tenant_fail', - connectionUrl: 'libsql://failed.turso.io', - status: 'failed', - region: 'us-east', - plan: 'free', - error: 'Database creation timeout', - }; - const parsed = TenantProvisioningResultSchema.parse(result); - expect(parsed.status).toBe('failed'); - expect(parsed.error).toBe('Database creation timeout'); - expect(parsed.steps).toEqual([]); - }); - - it('should reject missing required fields', () => { - expect(() => TenantProvisioningResultSchema.parse({ - tenantId: 'test', - })).toThrow(); - }); -}); diff --git a/packages/spec/src/system/provisioning.zod.ts b/packages/spec/src/system/provisioning.zod.ts deleted file mode 100644 index c9ba36fa1b..0000000000 --- a/packages/spec/src/system/provisioning.zod.ts +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; - -/** - * Tenant Provisioning Protocol - * - * Defines the schemas for the "Register → Instant ObjectOS" provisioning pipeline: - * 1. User registers → ProvisioningRequest created - * 2. Turso database created → Schema synced → Seed data applied - * 3. Tenant status transitions: provisioning → active - * - * Provisioning is designed to be: - * - **Idempotent**: Re-running the same request produces the same result - * - **Observable**: Each step has explicit status tracking - * - **Fast**: Target 2-5 seconds for complete provisioning - */ - -// ========================================================================== -// 1. Enums & Constants -// ========================================================================== - -/** - * Tenant provisioning lifecycle status. - */ -import { lazySchema } from '../shared/lazy-schema'; -export const TenantProvisioningStatusEnum = z.enum([ - 'provisioning', // Database creation in progress - 'active', // Fully provisioned and operational - 'suspended', // Temporarily disabled (billing, policy) - 'failed', // Provisioning failed (requires retry or manual intervention) - 'destroying', // Deletion in progress -]).describe('Tenant provisioning lifecycle status'); - -export type TenantProvisioningStatus = z.infer; - -/** - * Tenant subscription plan. - */ -export const TenantPlanSchema = lazySchema(() => z.enum([ - 'free', // Free tier with limited quotas - 'pro', // Professional tier with higher quotas - 'enterprise', // Enterprise tier with custom quotas and SLAs -]).describe('Tenant subscription plan')); - -export type TenantPlan = z.infer; - -/** - * Available deployment regions. - */ -export const TenantRegionSchema = lazySchema(() => z.enum([ - 'us-east', // US East (Virginia) - 'us-west', // US West (Oregon) - 'eu-west', // EU West (Ireland) - 'eu-central', // EU Central (Frankfurt) - 'ap-southeast',// Asia Pacific (Singapore) - 'ap-northeast',// Asia Pacific (Tokyo) -]).describe('Available deployment region')); - -export type TenantRegion = z.infer; - -// ========================================================================== -// 2. Provisioning Step Tracking -// ========================================================================== - -/** - * Individual provisioning step status. - * Tracks the progress of each step in the provisioning pipeline. - */ -export const ProvisioningStepSchema = lazySchema(() => z.object({ - /** Step identifier */ - name: z.string().min(1).describe('Step name (e.g., create_database, sync_schema)'), - - /** Step execution status */ - status: z.enum(['pending', 'running', 'completed', 'failed', 'skipped']).describe('Step status'), - - /** When the step started (ISO 8601) */ - startedAt: z.string().datetime().optional().describe('Step start time'), - - /** When the step completed (ISO 8601) */ - completedAt: z.string().datetime().optional().describe('Step completion time'), - - /** Duration in milliseconds */ - durationMs: z.number().int().min(0).optional().describe('Step duration in ms'), - - /** Error message if the step failed */ - error: z.string().optional().describe('Error message on failure'), -}).describe('Individual provisioning step status')); - -export type ProvisioningStep = z.infer; - -// ========================================================================== -// 3. Provisioning Request & Result -// ========================================================================== - -/** - * Tenant Provisioning Request. - * Input for creating a new tenant with its isolated database. - */ -export const TenantProvisioningRequestSchema = lazySchema(() => z.object({ - /** Organization ID that owns this tenant */ - orgId: z.string().min(1).describe('Organization ID'), - - /** Requested subscription plan */ - plan: TenantPlanSchema.default('free'), - - /** Preferred deployment region */ - region: TenantRegionSchema.default('us-east'), - - /** Optional tenant display name */ - displayName: z.string().optional().describe('Tenant display name'), - - /** Optional initial admin user email */ - adminEmail: z.string().email().optional().describe('Initial admin user email'), - - /** Optional metadata to attach to the tenant */ - metadata: z.record(z.string(), z.unknown()).optional().describe('Additional metadata'), -}).describe('Tenant provisioning request')); - -export type TenantProvisioningRequest = z.infer; - -/** - * Tenant Provisioning Result. - * Output after provisioning completes (or fails). - */ -export const TenantProvisioningResultSchema = lazySchema(() => z.object({ - /** Unique tenant identifier */ - tenantId: z.string().min(1).describe('Provisioned tenant ID'), - - /** Database connection URL (libsql:// or https://) */ - connectionUrl: z.string().min(1).describe('Database connection URL'), - - /** Current provisioning status */ - status: TenantProvisioningStatusEnum, - - /** Deployment region */ - region: TenantRegionSchema, - - /** Active subscription plan */ - plan: TenantPlanSchema, - - /** Provisioning pipeline steps with status */ - steps: z.array(ProvisioningStepSchema).default([]).describe('Pipeline step statuses'), - - /** Total provisioning duration in milliseconds */ - totalDurationMs: z.number().int().min(0).optional().describe('Total provisioning duration'), - - /** Provisioned timestamp (ISO 8601) */ - provisionedAt: z.string().datetime().optional().describe('Provisioning completion time'), - - /** Error message if provisioning failed */ - error: z.string().optional().describe('Error message on failure'), -}).describe('Tenant provisioning result')); - -export type TenantProvisioningResult = z.infer;