# OpenSpec Change: enterprise-auth - **Change ID**: enterprise-auth - **Package**: `@suaveplan/auth` - **Category**: universal - **Target path**: `universal/auth/auth` - **Status**: PENDING - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/auth.md` --- ## Summary Deliver `@suaveplan/auth` on the genesis branch from scratch at enterprise quality. The package provides a framework-agnostic authentication layer: JWT token lifecycle (issue, verify, refresh, revoke), API key generation and validation, session management, and request middleware for permission checking. The main-branch implementation was tightly coupled to Better-Auth and SurrealDB; the genesis rewrite is provider-agnostic with pluggable adapters for the underlying auth store, structured errors via `@suaveplan/error`, Zod option schemas, OTel instrumentation, `AbortSignal` cancellation on async operations, injectable clock and ID generator for determinism, and property-based tests. --- ## Current-state audit (genesis) The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Base class for `AuthError`; `defineCodes` for `AUTH.*` error codes | | `@suaveplan/types` (`workspace:^`) | `Clock` (injectable for token timestamps), `IdGenerator` (token/session IDs), `LoggerLike`, `StorageLike` | | `@suaveplan/events` (`workspace:^`) | `EventBusLike` for auth lifecycle events (login, logout, token revoked) | | `@suaveplan/crypto` (`workspace:^`) | Signing tokens, HMAC for API keys, constant-time comparison | | `@suaveplan/crypto-keys` (`workspace:^`) | Key pair management for JWT signing | | `@suaveplan/logger` (`workspace:^`) | Structured logging with trace correlation | | `@suaveplan/retry` (`workspace:^`) | Retry with backoff on transient storage errors | | `@suaveplan/security-codes` (`workspace:^`) | API key entropy validation | | `@suaveplan/idempotency` (`workspace:^`) | Idempotency guard on token creation / API key generation | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | Spans, counters, histograms | | `tslib` (`catalog:build`) | TypeScript runtime helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check`, `mitata` (all `catalog:testing`), `@suaveplan/testing` (`workspace:^`). --- ## Preserved public API surface (from main branch) All symbols below are carried forward. Signatures that referenced Better-Auth or SurrealDB types are replaced with framework-agnostic interfaces; existing call sites using the adapter pattern compile unchanged. ### API Keys ```ts interface ApiKey { id: string key: string name: string userId: string permissions: string[] expiresAt?: Date createdAt: Date lastUsedAt?: Date revokedAt?: Date } interface ApiKeyRequest { name: string userId: string permissions?: string[] expiresIn?: number // ms } interface ApiKeyGeneration { apiKey: ApiKey rawKey: string // returned once only; never stored in plain text } interface ApiKeyValidationResult { valid: boolean apiKey?: ApiKey error?: string } function createApiKey(request: ApiKeyRequest, options?: ApiKeyOptions): Promise function validateApiKey(rawKey: string, options?: ApiKeyOptions): Promise function revokeApiKey(keyId: string, options?: ApiKeyOptions): Promise function listApiKeys(userId: string, options?: ApiKeyOptions): Promise function rotateApiKey(keyId: string, options?: ApiKeyOptions): Promise function updateApiKeyPermissions(keyId: string, permissions: string[], options?: ApiKeyOptions): Promise function hasPermission(apiKey: ApiKey, permission: string): boolean function validateApiKeyMiddleware(options?: ApiKeyMiddlewareOptions): MiddlewareFn ``` ### JWT ```ts interface JWTPayload { sub: string iat: number exp: number jti: string sessionId?: string } interface AuthenticatedJWTRequest { user: JWTPayload } interface TokenValidationResult { valid: boolean payload?: JWTPayload error?: string } function validateSessionToken(token: string, options?: JWTOptions): Promise function refreshToken(refreshToken: string, options?: JWTOptions): Promise<{ accessToken: string; refreshToken: string }> function revokeToken(jti: string, options?: JWTOptions): Promise function revokeAllUserTokens(userId: string, options?: JWTOptions): Promise function isTokenBlacklisted(jti: string, options?: JWTOptions): Promise function getUserSessions(userId: string, options?: JWTOptions): Promise function verifyTokenMiddleware(options?: JWTOptions): MiddlewareFn function optionalTokenMiddleware(options?: JWTOptions): MiddlewareFn ``` ### Middleware ```ts interface UserContext { userId: string sessionId?: string permissions: string[] metadata?: Record } interface AuthenticatedRequest { user: UserContext } interface PermissionCheckResult { allowed: boolean reason?: string } function getUserFromSession(sessionToken: string, options?: MiddlewareOptions): Promise function checkPermission(user: UserContext, permission: string): PermissionCheckResult function requirePermission(permission: string, options?: MiddlewareOptions): MiddlewareFn function requireAllPermissions(permissions: string[], options?: MiddlewareOptions): MiddlewareFn function requireAnyPermission(permissions: string[], options?: MiddlewareOptions): MiddlewareFn function withPermissions(permissions: string[], options?: MiddlewareOptions): MiddlewareFn ``` ### Adapter interface ```ts interface AuthAdapter { // API key storage storeApiKey(apiKey: ApiKey): Promise findApiKeyByHash(hash: string): Promise revokeApiKey(keyId: string): Promise listApiKeysByUser(userId: string): Promise // Token blacklist blacklistToken(jti: string, expiresAt: Date): Promise isBlacklisted(jti: string): Promise // Sessions createSession(session: SessionInfo): Promise findSession(sessionId: string): Promise deleteSession(sessionId: string): Promise deleteUserSessions(userId: string): Promise listUserSessions(userId: string): Promise } interface SessionInfo { sessionId: string userId: string createdAt: Date expiresAt: Date userAgent?: string ip?: string } // In-memory adapter (no external storage dependency) class MemoryAuthAdapter implements AuthAdapter ``` ### Error codes ```ts type AuthErrorCode = | "AUTH_TOKEN_INVALID" | "AUTH_TOKEN_EXPIRED" | "AUTH_TOKEN_BLACKLISTED" | "AUTH_SESSION_NOT_FOUND" | "AUTH_SESSION_EXPIRED" | "AUTH_API_KEY_INVALID" | "AUTH_API_KEY_EXPIRED" | "AUTH_API_KEY_REVOKED" | "AUTH_PERMISSION_DENIED" | "AUTH_STORAGE_ERROR" class AuthError extends SuaveplanError function registerAuthErrorCodes(): void ``` --- ## New additions for genesis ### 1. Provider-agnostic design The genesis rewrite removes all direct Better-Auth and SurrealDB dependencies. The `AuthAdapter` interface (above) is the single integration point. A `MemoryAuthAdapter` ships in the package for use in tests and single-node deployments. Framework-specific adapters (SurrealDB, Redis, PostgreSQL) live in separate packages. ### 2. Pluggable middleware interface `MiddlewareFn` is a framework-agnostic `(req, res, next) => void | Promise` interface with a `createMiddlewareAdapter(framework)` bridge. Hono and plain Node adapters ship out of the box. ### 3. Clock and IdGenerator injection All token/session factories accept optional `{ clock?: Clock; idGenerator?: IdGenerator }` so timestamps and IDs are deterministic in tests. No bare `Date.now()` or `crypto.randomUUID()` at module scope. ### 4. EventBus integration Lifecycle events emitted on an optional `EventBusLike` (from `@suaveplan/types`): | Event | Payload | |---|---| | `auth:token:issued` | `{ userId, jti, expiresAt }` | | `auth:token:revoked` | `{ jti, reason }` | | `auth:session:created` | `{ sessionId, userId }` | | `auth:session:deleted` | `{ sessionId, userId }` | | `auth:apikey:created` | `{ keyId, userId }` | | `auth:apikey:revoked` | `{ keyId, userId }` | ### 5. OTel instrumentation - `auth.token.issue` span + `auth.tokens_issued_total` counter - `auth.token.verify` span + `auth.token_verify_total` counter (label `valid: bool`) - `auth.apikey.create` span + `auth.apikeys_created_total` counter - `auth.apikey.validate` span + `auth.apikey_validate_total` counter (label `valid: bool`) - `auth.session.create` / `auth.session.delete` counters - `auth.permission.check` histogram (duration_ms) ### 6. Zod schemas `ApiKeyRequestSchema`, `JWTOptionsSchema`, `ApiKeyOptionsSchema`, `MiddlewareOptionsSchema`, `AuthAdapterConfigSchema`. ### 7. Property-based tests (fast-check) - API key round-trip: `createApiKey` → `validateApiKey` → `valid === true` for any valid `ApiKeyRequest`. - Token blacklist monotonicity: once a `jti` is blacklisted it is always blacklisted. - Permission check associativity: `requireAllPermissions([a, b])` iff `checkPermission(user, a).allowed && checkPermission(user, b).allowed`. ### 8. Benchmark suite - `createApiKey` throughput (cold + warm adapter path) - `validateApiKey` throughput against a MemoryAdapter with 1 000 stored keys - `validateSessionToken` with HMAC verification --- ## File layout ```text universal/auth/auth/ package.json tsconfig.json vitest.config.ts src/ index.ts # Public barrel error-codes.ts # AUTH.* codes + registerAuthErrorCodes constants/ index.ts types/ auth.types.ts # All public interfaces and type aliases auth.types.md adapters/ auth-adapter/ auth-adapter.ts # AuthAdapter interface auth-adapter.md auth-adapter.types.ts index.ts memory-adapter/ memory-adapter.ts # MemoryAuthAdapter memory-adapter.md memory-adapter.test.ts index.ts api-keys/ api-keys/ api-keys.ts # createApiKey, validateApiKey, etc. api-keys.md api-keys.types.ts api-keys.test.ts index.ts jwt/ token-lifecycle/ token-lifecycle.ts # validateSessionToken, refreshToken, revokeToken, etc. token-lifecycle.md token-lifecycle.types.ts token-lifecycle.test.ts index.ts middleware/ auth-middleware/ auth-middleware.ts # requirePermission, withPermissions, etc. auth-middleware.md auth-middleware.types.ts auth-middleware.test.ts index.ts schemas/ auth.schemas.ts # Zod option schemas auth.schemas.md auth.schemas.test.ts __benchmarks__/ auth.bench.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream packages**: `@suaveplan/permissions` consumes `UserContext` from this package. Future API plugin packages will depend on the middleware types. **Breaking changes from main-branch API**: The SurrealDB-specific `surrealAdapter` export is removed; callers must use a dedicated `@suaveplan/auth-adapter-surrealdb` package (not in scope for this change). All other exported symbols are preserved or have equivalent replacements. `AuthAdapter` is a new addition. **Workspace dep prerequisites**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/crypto`, `@suaveplan/crypto-keys`, `@suaveplan/logger`, `@suaveplan/retry`, `@suaveplan/security-codes`, `@suaveplan/idempotency` must all be published to Verdaccio before this package can be built or tested.