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;