Initial Commit
This commit is contained in:
71
openspec/archive/2026-05-25-api-tenant-provider/proposal.md
Normal file
71
openspec/archive/2026-05-25-api-tenant-provider/proposal.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# `@suaveplan/api-tenant-provider` — abstract tenant enumeration contract
|
||||
|
||||
**Package**: `@suaveplan/api-tenant-provider`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
`perTenant: true` cron / queued jobs (design §5.4) need a way to list every tenant the host knows about so the scheduler can fan one job invocation out per tenant, each with a tenant-scoped context. The same enumeration is needed by background reconciliation, per-tenant cache warmers, and admin tooling. Different deployments source tenant lists from different places: an in-memory list (tests + simple setups), a database row, a directory service, a remote API. This package owns the abstract contract — concrete sources plug behind it (`api-tenant-provider-memory` in Stage 4; production adapters land alongside the host metadata DB or a plugin's own DB).
|
||||
|
||||
Per design §5.4: "`tenant.provider` ships in v1 (in-memory reference impl + adapter slot) so retrofitting tenant enumeration into a stable contract doesn't have to happen later."
|
||||
|
||||
`tenant.provider` is intentionally distinct from `tenant.resolver` (which extracts `tenantId` from a request — that contract lives in `api-types` / consumed by `api-tenant-resolver-*` adapters).
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package. Contract + pagination shape + conformance harness.
|
||||
|
||||
- ADDED: `./provider` — abstract `TenantProvider`: `list(opts?): AsyncIterable<TenantRecord>` (stream-friendly for large tenant counts); `count(): Promise<number>`; `lookup(tenantId): Promise<TenantRecord | null>`; `dispose(): Promise<void>`.
|
||||
- ADDED: `./tenant-record` — `TenantRecord`: `{ id: TenantId, displayName?: string, status: "active" | "suspended" | "archived", createdAt: string }`. Zod schema. Only `active` tenants are enumerated by default; `list({ includeSuspended: true })` surfaces those too.
|
||||
- ADDED: `./pagination` — `ListOpts`: `{ pageSize?: number, includeSuspended?: boolean, cursor?: string }`. The `AsyncIterable` MAY be page-driven internally; callers iterate without knowing.
|
||||
- ADDED: `./capability-id` — adds `"tenant.provider"` to the `CapabilityId` union.
|
||||
- ADDED: `./error-codes` — frozen `TENANT_PROVIDER_CODES`: `TENANT_NOT_FOUND`, `SOURCE_UNAVAILABLE`, `INVALID_CURSOR`.
|
||||
- ADDED: `./testing` — `runTenantProviderConformance(factory)` reusable harness.
|
||||
|
||||
## CapabilityId additions
|
||||
|
||||
This package contributes `"tenant.provider"` to the `CapabilityId` union. Per-tenant jobs declared in manifest implicitly require this capability (the host resolves it automatically when `perTenant: true` is set — plugins don't need to list it manually, but it shows up in the registry).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Concrete sources — `api-tenant-provider-memory` (Stage 4); DB-backed sources are deployment-specific.
|
||||
- Tenant CREATE / UPDATE / DELETE — that's an admin concern, not a foundation contract.
|
||||
- Tenant resolution from a Request — that's `tenant.resolver`, contract in `@suaveplan/api-types`.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| `perTenant: true` fan-out reading from `tenant.provider` | §5.4 |
|
||||
| Provider ships in v1 with in-memory reference impl | §5.4 |
|
||||
| Status filter (`active` default) | Implicit from §5.4 (fan-out should not include suspended tenants by default) |
|
||||
|
||||
### Requirements with no source line
|
||||
|
||||
| Requirement | Justification |
|
||||
|---|---|
|
||||
| `AsyncIterable` over `Array` | Future-proofs against large tenant counts; an array forces full-set load, an iterable streams. Standard genesis pattern. |
|
||||
| `runTenantProviderConformance` harness | Stage-5 deliverable (§13.4). |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/provider/provider.spec-conformance.test.ts` — interface shape; iterator semantics.
|
||||
- `src/tenant-record/tenant-record.test.ts` — Zod accept/reject; `TenantId` brand preserved.
|
||||
- `src/provider/status-filter.test.ts` — default excludes `suspended`/`archived`; `includeSuspended: true` surfaces them.
|
||||
- `src/pagination/pagination.test.ts` — `pageSize` honored; `cursor` round-trips.
|
||||
- `src/capability-id/capability-id.spec-conformance.test.ts` — `"tenant.provider"` in `CapabilityId`.
|
||||
- `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/error`, `@suaveplan/types`, `zod`.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
- (none) — [x] no action needed (reason: net-new; in-memory ref impl lands Stage 4)
|
||||
Reference in New Issue
Block a user