Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
# `@suaveplan/api-auth-provider` — abstract account-lifecycle contract
**Package**: `@suaveplan/api-auth-provider`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 1 (Contracts) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Account lifecycle (register, login, logout, refresh, oauth/*, verify-email, ...) is the second half of the auth split (design §2.3). It is OPTIONAL — sites using hosted IdPs (Clerk, Auth0, Cognito) don't need a provider; they only need a session resolver. Sites running their own auth pick one provider: Better-Auth (`api-auth-better-auth`, Stage 6) or the rolled-our-own reference (`api-auth-rolled`, Stage 8). This package owns the abstract contract: the route surface a provider owns, the path-ownership declaration (default `/auth/*`, configurable), and the collision detection that runs at install time so plugin routes can't accidentally squat on auth paths.
Per design §2.3: "Each provider adapter declares which paths it owns (default `/auth/*`, configurable). Host reserves the prefix and refuses plugin routes that collide."
## What changes
Net-new package. Contract types + the route-collision detector + the conformance harness.
- ADDED: `./provider` — abstract `AuthProviderAdapter`: `pathPrefix: string` (default `/auth`), `routes(router, ctx)` callback registering provider routes, `lifecycle: { install, start, stop, uninstall }` hooks.
- ADDED: `./route-surface` — declarative `ProviderRouteSurface`: the canonical set of paths a provider OWNS (`/register`, `/login`, `/logout`, `/refresh`, `/oauth/:provider/start`, `/oauth/:provider/callback`, `/verify-email/:token`, `/forgot-password`, `/reset-password`). Concrete providers MAY implement a subset; manifest declares which.
- ADDED: `./path-ownership` — `PathOwnership` shape: `{ prefix: string, owned: readonly string[] }`; runtime registry tracking which paths are reserved by an installed provider.
- ADDED: `./collision-detector` — `detectAuthPathCollisions({ provider, plugins }): CollisionReport`; runs at install time. Plugin routes whose prefix overlaps with `provider.pathPrefix` AND aren't `publicRoutes` MUST be reported as `AUTH_PROVIDER.PATH_COLLISION`.
- ADDED: `./capability-id` — adds `"auth.provider"` to the `CapabilityId` union.
- ADDED: `./error-codes` — frozen `AUTH_PROVIDER_CODES`: `PATH_COLLISION`, `MULTIPLE_PROVIDERS_REGISTERED`, `PROVIDER_LIFECYCLE_FAILED`, `OAUTH_CALLBACK_INVALID_STATE`.
- ADDED: `./testing` — `runAuthProviderConformance(factory)` reusable harness.
## CapabilityId additions
This package contributes `"auth.provider"` to the `CapabilityId` union — separate from `"auth.session"` so a plugin can require session resolution without forcing the host to ship account-lifecycle routes.
## Out of scope
- Concrete providers — `api-auth-better-auth` (Stage 6), `api-auth-rolled` (Stage 8).
- The `Principal` shape or session resolution — that's `api-auth` + `api-types`.
- OAuth state-store implementation — provider-specific.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Path ownership (`/auth/*`, configurable) | §2.3 (lines 192-198) |
| Collision detection at install | §2.3 ("Host reserves the prefix and refuses plugin routes that collide") |
| Provider lifecycle hooks | §4 (FSM inherited from generic plugin lifecycle) |
| Provider route surface (register/login/oauth/verify-email) | §2.3 (lines 193-194) |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `MULTIPLE_PROVIDERS_REGISTERED` enforcement | Implicit in §2.3 ("Mix-and-match" describes a single provider + session resolvers chain). Pinned here so the host's behavior is explicit. |
| `runAuthProviderConformance` harness | Stage-5 deliverable (§13.4). |
## Verification approach
- `src/provider/provider.spec-conformance.test.ts` — interface shape.
- `src/route-surface/route-surface.test.ts` — canonical path set documented; providers can declare a subset.
- `src/collision-detector/collision-detector.test.ts` — overlap rules: `/auth/register` collides with plugin route `/auth/x`; `/auth/register` does NOT collide with plugin route `/billing/x`; configurable prefix `/myauth` shifts the collision domain.
- `src/path-ownership/path-ownership.test.ts` — registry rejects double-registration (`MULTIPLE_PROVIDERS_REGISTERED`).
- `src/capability-id/capability-id.spec-conformance.test.ts` — `"auth.provider"` in `CapabilityId`.
- `tests/dep-graph.test.ts` — deps: `@suaveplan/api-types`, `@suaveplan/api-router` (peer, for `Router` type), `@suaveplan/error`, `@suaveplan/types`, `zod`.
## Migration impact
None — additive only.
## Impact
### Affected dependents
- (none) — [x] no action needed (reason: net-new; concrete providers land Stages 6/8)

View File

@@ -0,0 +1,127 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-auth-provider (api-auth-provider)
**Package**: `@suaveplan/api-auth-provider`
**Status**: PROPOSED
> Every requirement carries a `**Verified by:**` line.
## ADDED Requirements
### Requirement: AuthProviderAdapter contract
The package SHALL export `AuthProviderAdapter` interface: `pathPrefix: string` (default `/auth`), `routes(router: Router, ctx: PluginContext): void` to register provider routes, `lifecycle: { install, start, stop, uninstall }` hooks matching the generic plugin lifecycle. The adapter MUST be generic over `Caps extends CapabilityMap`.
#### Scenario: every documented field present
- **WHEN** `expectTypeOf<AuthProviderAdapter>()` is checked
- **THEN** `pathPrefix`, `routes`, and the four lifecycle hooks are declared
**Verified by:** `src/provider/provider.spec-conformance.test.ts::"AuthProviderAdapter declares pathPrefix routes lifecycle"`
---
### Requirement: `import z from "zod"` everywhere
Every Zod import MUST use the default form.
#### Scenario: no named import of z
- **WHEN** the test greps `import z` from `"zod"`
- **THEN** zero matches
**Verified by:** `tests/zod-import-form.test.ts::"every src file uses default-import z"`
---
### Requirement: Canonical provider route surface
The package SHALL export `PROVIDER_ROUTE_SURFACE` listing the canonical paths a provider OWNS (relative to `pathPrefix`): `/register`, `/login`, `/logout`, `/refresh`, `/oauth/:provider/start`, `/oauth/:provider/callback`, `/verify-email/:token`, `/forgot-password`, `/reset-password`. Providers MAY implement a subset; the host does not require every path.
#### Scenario: surface enumerates the documented paths
- **WHEN** the test inspects `PROVIDER_ROUTE_SURFACE`
- **THEN** every documented path is listed
**Verified by:** `src/route-surface/route-surface.test.ts::"canonical surface lists every documented path"`
---
### Requirement: Path-ownership registry
The package SHALL export `PathOwnership` — a runtime registry. `register(provider)` claims `provider.pathPrefix`. A second `register(provider2)` with a different provider MUST throw `AUTH_PROVIDER.MULTIPLE_PROVIDERS_REGISTERED`. Re-registering the SAME provider instance MUST be a no-op (idempotent).
#### Scenario: double registration with different provider rejected
- **WHEN** provider A is registered AND provider B is registered
- **THEN** the second call throws `AUTH_PROVIDER.MULTIPLE_PROVIDERS_REGISTERED`
#### Scenario: re-registering same provider is idempotent
- **WHEN** provider A is registered twice
- **THEN** the second call returns without throwing
**Verified by:** `src/path-ownership/path-ownership.test.ts::"double registration rejected"` AND `src/path-ownership/path-ownership.test.ts::"re-registering same provider is idempotent"`
---
### Requirement: Collision detector
The package SHALL export `detectAuthPathCollisions({ provider, plugins }): CollisionReport`. A plugin route whose prefix overlaps with `provider.pathPrefix` AND is not in `publicRoutes` MUST be reported with code `AUTH_PROVIDER.PATH_COLLISION` AND `details.plugin` AND `details.path`. Plugin routes outside the auth prefix MUST NOT be reported.
#### Scenario: overlap reported
- **WHEN** provider owns `/auth` AND plugin declares route `/auth/custom`
- **THEN** the report includes `{ code: "AUTH_PROVIDER.PATH_COLLISION", details: { plugin: ..., path: "/auth/custom" } }`
#### Scenario: non-overlap not reported
- **WHEN** provider owns `/auth` AND plugin declares route `/billing/invoices`
- **THEN** the report is empty
#### Scenario: configurable prefix shifts collision domain
- **WHEN** provider's `pathPrefix === "/myauth"` AND plugin declares route `/auth/x`
- **THEN** the report is empty AND a plugin route `/myauth/x` would be reported
**Verified by:** `src/collision-detector/collision-detector.test.ts::"overlap with auth prefix reported"` AND `src/collision-detector/collision-detector.test.ts::"non-overlap not reported"` AND `src/collision-detector/collision-detector.test.ts::"configurable prefix shifts collision domain"`
---
### Requirement: CapabilityId augmentation contributes "auth.provider"
The package SHALL augment the `CapabilityId` union to include `"auth.provider"`.
#### Scenario: auth.provider assignable to CapabilityId
- **WHEN** `expectTypeOf<"auth.provider">().toMatchTypeOf<CapabilityId>()` is checked
- **THEN** the assertion passes
**Verified by:** `src/capability-id/capability-id.spec-conformance.test.ts::"auth.provider is a valid CapabilityId after augmentation"`
---
### Requirement: Error codes registry frozen
The package SHALL export `AUTH_PROVIDER_CODES` containing exactly: `PATH_COLLISION`, `MULTIPLE_PROVIDERS_REGISTERED`, `PROVIDER_LIFECYCLE_FAILED`, `OAUTH_CALLBACK_INVALID_STATE`.
#### Scenario: every code reachable
- **WHEN** the test enumerates `AUTH_PROVIDER_CODES`
- **THEN** every code has a production throw site
**Verified by:** `src/error-codes/error-codes.spec-conformance.test.ts::"every AUTH_PROVIDER_CODES entry thrown by production path"`
---
### Requirement: Conformance harness for providers
The package SHALL export `runAuthProviderConformance({ factory, label })`. Every concrete provider in Stages 6/8 MUST pass it.
#### Scenario: harness covers path ownership, route surface, lifecycle, collision
- **WHEN** the harness runs against a stub provider
- **THEN** test categories exist for each documented behavior
**Verified by:** `src/testing/conformance.test.ts::"harness covers path ownership, route surface, lifecycle, collision"`

View File

@@ -0,0 +1,70 @@
# Tasks — `@suaveplan/api-auth-provider`
> Every checkbox MUST reflect work actually done in this change.
## 0. Pre-flight (non-negotiable)
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [x] 1.1 Read design doc §2.3 (full section)
- [x] 1.2 Read `openspec/specs/api-auth-provider/spec.md` (does not exist yet)
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13, 14
- [x] 1.4 Draft `proposal.md`
- [x] 1.5 Source-mapping table
- [x] 1.6 Draft `specs/api-auth-provider/spec.md`
- [x] 1.7 `**Verified by:**` line on every requirement
## 2. Implementation
- [x] 2.1 Scaffold `packages/api/api-auth-provider/` (`"version": "0.1.0"`, `"sideEffects": false`, deps: `@suaveplan/api-types`, `@suaveplan/api-router` peer, `@suaveplan/error`, `@suaveplan/types`, `zod`)
- [x] 2.2 `src/provider/` — `AuthProviderAdapter`
- [x] 2.3 `src/route-surface/` — `ProviderRouteSurface` canonical paths
- [x] 2.4 `src/path-ownership/` — `PathOwnership` registry
- [x] 2.5 `src/collision-detector/` — `detectAuthPathCollisions`
- [x] 2.6 `src/capability-id/` — augmentation adding `"auth.provider"`
- [x] 2.7 `src/error-codes/` — `AUTH_PROVIDER_CODES`
- [x] 2.8 `src/testing/` — `runAuthProviderConformance(factory)`
- [x] 2.9 `src/index.ts` — explicit named re-exports
- [x] 2.10 `package.json` `exports` per subpath
## 3. Tests
- [x] 3.1 Unit tests per feature
- [x] 3.2 Spec-conformance type tests
- [x] 3.3 Property test on collision detector (prefix-tree based)
- [x] 3.4 Every `it(...)` carries `{ timeout: 5000 }`
## 4. Documentation
- [x] 4.1 Sibling `.md` for every implementation `.ts`
- [x] 4.2 `README.md` linking every `.md`
- [x] 4.3 No CLAUDE.md update
## 5. Pre-archive verification
- [x] 5.1 `bun run check` — 0/0/0
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-auth-provider` — green
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-auth-provider` — 100/100/100/100
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-auth-provider` — exit 0
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-auth-provider` — green
## 6. Pre-archive 7-row checklist
- [x] 6.1 Generics on `AuthProviderAdapter<Caps>`; `expectTypeOf` assertions
- [x] 6.2 Zod wired at `pathPrefix` boundary (must be a valid URL path)
- [x] 6.3 Every `AUTH_PROVIDER_CODES` entry thrown
- [x] 6.4 Collision detector strict-mode tested both directions
- [x] 6.5 OTel — N/A (providers emit)
- [x] 6.6 Lifecycle dedup — registration idempotent under same provider instance; tested
- [x] 6.7 No retroactive tick
## 7. Archive
- [x] 7.1 Fold spec delta
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-auth-provider/`
- [x] 7.3 Tick umbrella `1.8`
- [x] 7.4 Refresh `MANIFEST.md`