Initial Commit
This commit is contained in:
325
openspec/archive/2026-04-24-enterprise-auth/proposal.md
Normal file
325
openspec/archive/2026-04-24-enterprise-auth/proposal.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# 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<ApiKeyGeneration>
|
||||
function validateApiKey(rawKey: string, options?: ApiKeyOptions): Promise<ApiKeyValidationResult>
|
||||
function revokeApiKey(keyId: string, options?: ApiKeyOptions): Promise<void>
|
||||
function listApiKeys(userId: string, options?: ApiKeyOptions): Promise<ApiKey[]>
|
||||
function rotateApiKey(keyId: string, options?: ApiKeyOptions): Promise<ApiKeyGeneration>
|
||||
function updateApiKeyPermissions(keyId: string, permissions: string[], options?: ApiKeyOptions): Promise<ApiKey>
|
||||
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<TokenValidationResult>
|
||||
function refreshToken(refreshToken: string, options?: JWTOptions): Promise<{ accessToken: string; refreshToken: string }>
|
||||
function revokeToken(jti: string, options?: JWTOptions): Promise<void>
|
||||
function revokeAllUserTokens(userId: string, options?: JWTOptions): Promise<void>
|
||||
function isTokenBlacklisted(jti: string, options?: JWTOptions): Promise<boolean>
|
||||
function getUserSessions(userId: string, options?: JWTOptions): Promise<SessionInfo[]>
|
||||
function verifyTokenMiddleware(options?: JWTOptions): MiddlewareFn
|
||||
function optionalTokenMiddleware(options?: JWTOptions): MiddlewareFn
|
||||
```
|
||||
|
||||
### Middleware
|
||||
|
||||
```ts
|
||||
interface UserContext {
|
||||
userId: string
|
||||
sessionId?: string
|
||||
permissions: string[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface AuthenticatedRequest {
|
||||
user: UserContext
|
||||
}
|
||||
|
||||
interface PermissionCheckResult {
|
||||
allowed: boolean
|
||||
reason?: string
|
||||
}
|
||||
|
||||
function getUserFromSession(sessionToken: string, options?: MiddlewareOptions): Promise<UserContext | null>
|
||||
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<void>
|
||||
findApiKeyByHash(hash: string): Promise<ApiKey | null>
|
||||
revokeApiKey(keyId: string): Promise<void>
|
||||
listApiKeysByUser(userId: string): Promise<ApiKey[]>
|
||||
// Token blacklist
|
||||
blacklistToken(jti: string, expiresAt: Date): Promise<void>
|
||||
isBlacklisted(jti: string): Promise<boolean>
|
||||
// Sessions
|
||||
createSession(session: SessionInfo): Promise<void>
|
||||
findSession(sessionId: string): Promise<SessionInfo | null>
|
||||
deleteSession(sessionId: string): Promise<void>
|
||||
deleteUserSessions(userId: string): Promise<void>
|
||||
listUserSessions(userId: string): Promise<SessionInfo[]>
|
||||
}
|
||||
|
||||
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<AuthErrorCode>
|
||||
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<void>` 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.
|
||||
176
openspec/archive/2026-04-24-enterprise-auth/tasks.md
Normal file
176
openspec/archive/2026-04-24-enterprise-auth/tasks.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Tasks: enterprise-auth
|
||||
|
||||
**Change ID**: enterprise-auth
|
||||
**Package**: `@suaveplan/auth`
|
||||
**Target path**: `universal/auth/auth`
|
||||
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/crypto`, `@suaveplan/crypto-keys`, `@suaveplan/logger`, `@suaveplan/retry`, `@suaveplan/security-codes`, `@suaveplan/idempotency`
|
||||
|
||||
---
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [x] Create directory `universal/auth/auth/` with `src/`, `tests/`, `src/__benchmarks__/`, and all feature subdirectories per the layout in `proposal.md`.
|
||||
- [x] Write `package.json` at version `0.1.0` with:
|
||||
- `"type": "module"`, `"sideEffects": false`
|
||||
- `exports` map with a top-level `"."` barrel plus one subpath per feature module (`"./adapters/auth-adapter/auth-adapter"`, `"./adapters/memory-adapter/memory-adapter"`, `"./api-keys/api-keys/api-keys"`, `"./jwt/token-lifecycle/token-lifecycle"`, `"./middleware/auth-middleware/auth-middleware"`, `"./schemas/auth.schemas"`, `"./types/auth.types"`)
|
||||
- `dependencies`: `@suaveplan/error workspace:^`, `@suaveplan/types workspace:^`, `@suaveplan/events workspace:^`, `@suaveplan/crypto workspace:^`, `@suaveplan/crypto-keys workspace:^`, `@suaveplan/logger workspace:^`, `@suaveplan/retry workspace:^`, `@suaveplan/security-codes workspace:^`, `@suaveplan/idempotency workspace:^`, `zod catalog:forms`, `@opentelemetry/api catalog:observability`, `tslib catalog:build`
|
||||
- `devDependencies`: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `fast-check catalog:testing`, `mitata catalog:testing`, `@suaveplan/testing workspace:^`
|
||||
- [x] Write `tsconfig.json` extending `@suaveplan/typescript-config/library.json`.
|
||||
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` (pool: `forks`).
|
||||
- [x] Add `lint`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check`, `bench` scripts to `package.json`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Types and errors
|
||||
|
||||
### 2a. Types (`src/types/auth.types.ts` + `src/types/auth.types.md`)
|
||||
|
||||
- [x] Define all public interfaces: `ApiKey`, `ApiKeyRequest`, `ApiKeyGeneration`, `ApiKeyValidationResult`, `JWTPayload`, `AuthenticatedJWTRequest`, `TokenValidationResult`, `UserContext`, `AuthenticatedRequest`, `PermissionCheckResult`, `SessionInfo`, `ApiKeyOptions`, `JWTOptions`, `MiddlewareOptions`, `ApiKeyMiddlewareOptions`, `MiddlewareFn`.
|
||||
- [x] Add genesis-only optional fields: `clock?: Clock` and `idGenerator?: IdGenerator` on all factory option interfaces; `bus?: EventBusLike` on `ApiKeyOptions`, `JWTOptions`, `MiddlewareOptions`.
|
||||
- [x] Write `src/types/auth.types.md` (≥200 words, simple + advanced examples, full API reference for every type).
|
||||
|
||||
### 2b. Error codes (`src/error-codes.ts`)
|
||||
|
||||
- [x] Define `AuthErrorCode` union — ten codes: `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`.
|
||||
- [x] Define `AUTH_ERROR_METADATA` with `httpStatus`, `severity`, `retryable`, `userMessageKey`, `category`, `description` for each code.
|
||||
- [x] Export `registerAuthErrorCodes()` — calls `ErrorCodes.registerMany(AUTH_ERROR_METADATA)`. Call as eager side-effect at file bottom.
|
||||
- [x] Export `AuthError` class extending `SuaveplanError<AuthErrorCode>`.
|
||||
|
||||
### 2c. Zod schemas (`src/schemas/auth.schemas.ts` + `src/schemas/auth.schemas.md`)
|
||||
|
||||
- [x] Write Zod schemas: `ApiKeyRequestSchema`, `JWTOptionsSchema`, `ApiKeyOptionsSchema`, `MiddlewareOptionsSchema`, `AuthAdapterConfigSchema`.
|
||||
- [x] Write `src/schemas/auth.schemas.md`.
|
||||
- [x] Write `src/schemas/auth.schemas.test.ts` — parse valid/invalid for each schema.
|
||||
|
||||
---
|
||||
|
||||
## 3. AuthAdapter interface
|
||||
|
||||
- [x] Implement `AuthAdapter` interface in `src/adapters/auth-adapter/auth-adapter.ts` with the full contract from `proposal.md`.
|
||||
- [x] Write `auth-adapter.md` (≥200 words, integration guide, expected error behaviour for each method).
|
||||
- [x] Write `auth-adapter.types.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 4. MemoryAuthAdapter
|
||||
|
||||
- [x] Implement `MemoryAuthAdapter` in `src/adapters/memory-adapter/memory-adapter.ts`:
|
||||
- `Map`-backed storage for API keys (by hash), token blacklist (by jti + TTL), sessions (by sessionId).
|
||||
- TTL cleanup: expired tokens removed lazily on lookup (no setInterval at module scope).
|
||||
- Accepts optional `{ clock?: Clock }` constructor option for deterministic expiry checks.
|
||||
- [x] Write `memory-adapter.md`.
|
||||
- [x] Write `memory-adapter.test.ts` — store/find/revoke API keys, blacklist tokens, create/find/delete sessions, TTL expiry with injected FakeClock, list operations. Each `it` has `{ timeout: 5000 }`.
|
||||
|
||||
---
|
||||
|
||||
## 5. API key management
|
||||
|
||||
File: `src/api-keys/api-keys/api-keys.ts`
|
||||
|
||||
- [x] Implement `createApiKey(request, options?)`:
|
||||
- Generate `rawKey` using `@suaveplan/crypto`'s `randomBytes` (32 bytes, base58-encoded).
|
||||
- Compute HMAC-SHA256 of `rawKey` with a secret from options; store the hash (never the plaintext).
|
||||
- Accept `idGenerator` for key ID, `clock` for `createdAt`.
|
||||
- Emit `auth:apikey:created` on optional `bus`.
|
||||
- Instrument with `auth.apikey.create` OTel span.
|
||||
- [x] Implement `validateApiKey(rawKey, options?)`:
|
||||
- Hash the incoming key and look up via adapter; check revoked/expired status.
|
||||
- Update `lastUsedAt` on successful validation.
|
||||
- Instrument with `auth.apikey.validate` OTel span, `auth.apikey_validate_total` counter.
|
||||
- [x] Implement `revokeApiKey`, `listApiKeys`, `rotateApiKey`, `updateApiKeyPermissions`, `hasPermission`, `validateApiKeyMiddleware`.
|
||||
- [x] Write `api-keys.md` (≥200 words, security model explanation, rotation guide).
|
||||
- [x] Write `api-keys.test.ts` (≤500 lines): creation/validation round-trip, revocation, rotation, expiry, permission check, middleware path. Each `it` has `{ timeout: 5000 }`.
|
||||
|
||||
---
|
||||
|
||||
## 6. JWT token lifecycle
|
||||
|
||||
File: `src/jwt/token-lifecycle/token-lifecycle.ts`
|
||||
|
||||
- [x] Implement `validateSessionToken(token, options?)`:
|
||||
- Decode and verify JWT signature using `@suaveplan/crypto-keys`.
|
||||
- Check expiry via injectable `clock`; check blacklist via adapter.
|
||||
- Return `TokenValidationResult` with structured error reason.
|
||||
- Instrument with `auth.token.verify` span.
|
||||
- [x] Implement `refreshToken(refreshToken, options?)`:
|
||||
- Validate refresh token; issue new access + refresh token pair.
|
||||
- Blacklist the old refresh token jti.
|
||||
- Emit `auth:token:issued` event on `bus`.
|
||||
- [x] Implement `revokeToken`, `revokeAllUserTokens`, `isTokenBlacklisted`, `getUserSessions`, `verifyTokenMiddleware`, `optionalTokenMiddleware`.
|
||||
- [x] Write `token-lifecycle.md` (≥200 words, refresh flow diagram, blacklist strategy).
|
||||
- [x] Write `token-lifecycle.test.ts` (≤500 lines): issue/verify round-trip, expired token, blacklisted token, refresh flow, revoke all, middleware verify/optional paths. `{ timeout: 5000 }` on sync, `{ timeout: 30000 }` on async.
|
||||
|
||||
---
|
||||
|
||||
## 7. Permission middleware
|
||||
|
||||
File: `src/middleware/auth-middleware/auth-middleware.ts`
|
||||
|
||||
- [x] Implement `getUserFromSession`, `checkPermission`, `requirePermission`, `requireAllPermissions`, `requireAnyPermission`, `withPermissions`.
|
||||
- [x] `checkPermission` uses exact string match and wildcard `*` (e.g. `resource:*` grants all actions on resource).
|
||||
- [x] All middleware functions return framework-agnostic `MiddlewareFn`; include a `createMiddlewareAdapter` bridge for Hono and Node.
|
||||
- [x] Instrument `auth.permission.check` histogram (duration_ms).
|
||||
- [x] Write `auth-middleware.md` (≥200 words, wildcard semantics, framework adapter guide).
|
||||
- [x] Write `auth-middleware.test.ts` (≤500 lines): exact match, wildcard match, requireAll/requireAny combinations, denied path, session lookup failures. `{ timeout: 5000 }` on each `it`.
|
||||
|
||||
---
|
||||
|
||||
## 8. OTel instrumentation (cross-cutting review)
|
||||
|
||||
- [x] Confirm all spans, counters, and histograms listed in `proposal.md §5` are wired in each module.
|
||||
- [x] Add `tests/otel.test.ts`: wire in-memory OTel exporter; assert counter increments for key create, key validate, token issue, token verify, session create, permission check.
|
||||
|
||||
---
|
||||
|
||||
## 9. Property-based tests (`tests/property.test.ts`)
|
||||
|
||||
Use `fast-check`. All test timeouts `{ timeout: 30000 }`.
|
||||
|
||||
- [x] **API key round-trip**: for any valid `ApiKeyRequest`, `createApiKey` then `validateApiKey` returns `valid === true`.
|
||||
- [x] **Blacklist monotonicity**: once a `jti` is blacklisted via `revokeToken`, all subsequent `isTokenBlacklisted` calls return `true` regardless of adapter state mutations.
|
||||
- [x] **Permission check AND**: `checkPermission` with `requireAllPermissions([a, b])` is `true` iff both individual checks are `true` for arbitrary permission strings and user contexts.
|
||||
- [x] **MemoryAdapter eviction**: after TTL elapses (injected FakeClock advanced), `isBlacklisted` returns `false` for expired entries.
|
||||
|
||||
---
|
||||
|
||||
## 10. Benchmarks (`src/__benchmarks__/auth.bench.ts`)
|
||||
|
||||
Use `mitata`.
|
||||
|
||||
- [x] `createApiKey_throughput`: 1 000 iterations against `MemoryAuthAdapter`.
|
||||
- [x] `validateApiKey_warm`: 10 000 lookups on an adapter with 1 000 pre-populated keys.
|
||||
- [x] `validateSessionToken_hmac`: 10 000 verifications of a pre-issued token.
|
||||
- [x] Document placeholder numbers in `README.md` under `## Benchmarks`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Docs
|
||||
|
||||
Per-file `.md` (all ≥200 words, simple + advanced examples, API reference):
|
||||
|
||||
- [x] `src/types/auth.types.md`
|
||||
- [x] `src/adapters/auth-adapter/auth-adapter.md`
|
||||
- [x] `src/adapters/memory-adapter/memory-adapter.md`
|
||||
- [x] `src/api-keys/api-keys/api-keys.md`
|
||||
- [x] `src/jwt/token-lifecycle/token-lifecycle.md`
|
||||
- [x] `src/middleware/auth-middleware/auth-middleware.md`
|
||||
- [x] `src/schemas/auth.schemas.md`
|
||||
|
||||
Root README:
|
||||
|
||||
- [x] Write `universal/auth/auth/README.md`: purpose, install, quick-start (API key + JWT round-trip), modules table, dependencies, API listing, benchmarks placeholder.
|
||||
|
||||
---
|
||||
|
||||
## 12. Verify and publish
|
||||
|
||||
- [x] Run `bunx turbo lint --filter=@suaveplan/auth` — zero errors.
|
||||
- [x] Run `bunx turbo typecheck --filter=@suaveplan/auth` — zero errors.
|
||||
- [x] Run `bunx turbo test --filter=@suaveplan/auth` — all tests pass.
|
||||
- [x] Run `bunx turbo test:coverage --filter=@suaveplan/auth` — 100/100/100/100.
|
||||
- [x] Run `bun run docs:check --filter=@suaveplan/auth` — zero errors.
|
||||
- [x] Confirm no `TODO`, `FIXME`, `@deprecated`, untyped `any`, `ts-ignore`, or `.skip` in `src/` or `tests/`.
|
||||
- [x] Confirm `"sideEffects": false` in `package.json`.
|
||||
- [x] Run `bun scripts/publish.ts --package @suaveplan/auth`.
|
||||
- [x] Update `CLAUDE.md` package listing to include `@suaveplan/auth`.
|
||||
- [x] Archive: move `openspec/changes/enterprise-auth/` to `openspec/archive/2026-04-24-enterprise-auth/` and fold spec delta into `openspec/specs/auth/spec.md`.
|
||||
Reference in New Issue
Block a user