@enderworld/onlyapi 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +201 -0
  2. package/LICENSE +21 -0
  3. package/README.md +338 -0
  4. package/dist/cli.js +14 -0
  5. package/package.json +69 -0
  6. package/src/application/dtos/admin.dto.ts +25 -0
  7. package/src/application/dtos/auth.dto.ts +97 -0
  8. package/src/application/dtos/index.ts +40 -0
  9. package/src/application/index.ts +2 -0
  10. package/src/application/services/admin.service.ts +150 -0
  11. package/src/application/services/api-key.service.ts +65 -0
  12. package/src/application/services/auth.service.ts +606 -0
  13. package/src/application/services/health.service.ts +97 -0
  14. package/src/application/services/index.ts +10 -0
  15. package/src/application/services/user.service.ts +95 -0
  16. package/src/cli/commands/help.ts +86 -0
  17. package/src/cli/commands/init.ts +301 -0
  18. package/src/cli/commands/upgrade.ts +471 -0
  19. package/src/cli/index.ts +76 -0
  20. package/src/cli/ui.ts +189 -0
  21. package/src/cluster.ts +62 -0
  22. package/src/core/entities/index.ts +1 -0
  23. package/src/core/entities/user.entity.ts +24 -0
  24. package/src/core/errors/app-error.ts +81 -0
  25. package/src/core/errors/index.ts +15 -0
  26. package/src/core/index.ts +7 -0
  27. package/src/core/ports/account-lockout.ts +15 -0
  28. package/src/core/ports/alert-sink.ts +27 -0
  29. package/src/core/ports/api-key.ts +37 -0
  30. package/src/core/ports/audit-log.ts +46 -0
  31. package/src/core/ports/cache.ts +24 -0
  32. package/src/core/ports/circuit-breaker.ts +42 -0
  33. package/src/core/ports/event-bus.ts +78 -0
  34. package/src/core/ports/index.ts +62 -0
  35. package/src/core/ports/job-queue.ts +73 -0
  36. package/src/core/ports/logger.ts +21 -0
  37. package/src/core/ports/metrics.ts +49 -0
  38. package/src/core/ports/oauth.ts +55 -0
  39. package/src/core/ports/password-hasher.ts +10 -0
  40. package/src/core/ports/password-history.ts +23 -0
  41. package/src/core/ports/password-policy.ts +43 -0
  42. package/src/core/ports/refresh-token-store.ts +37 -0
  43. package/src/core/ports/retry.ts +23 -0
  44. package/src/core/ports/token-blacklist.ts +16 -0
  45. package/src/core/ports/token-service.ts +23 -0
  46. package/src/core/ports/totp-service.ts +16 -0
  47. package/src/core/ports/user.repository.ts +40 -0
  48. package/src/core/ports/verification-token.ts +41 -0
  49. package/src/core/ports/webhook.ts +58 -0
  50. package/src/core/types/brand.ts +19 -0
  51. package/src/core/types/index.ts +19 -0
  52. package/src/core/types/pagination.ts +28 -0
  53. package/src/core/types/result.ts +52 -0
  54. package/src/infrastructure/alerting/index.ts +1 -0
  55. package/src/infrastructure/alerting/webhook.ts +100 -0
  56. package/src/infrastructure/cache/in-memory-cache.ts +111 -0
  57. package/src/infrastructure/cache/index.ts +6 -0
  58. package/src/infrastructure/cache/redis-cache.ts +204 -0
  59. package/src/infrastructure/config/config.ts +185 -0
  60. package/src/infrastructure/config/index.ts +1 -0
  61. package/src/infrastructure/database/in-memory-user.repository.ts +134 -0
  62. package/src/infrastructure/database/index.ts +37 -0
  63. package/src/infrastructure/database/migrations/001_create_users.ts +26 -0
  64. package/src/infrastructure/database/migrations/002_create_token_blacklist.ts +21 -0
  65. package/src/infrastructure/database/migrations/003_create_audit_log.ts +31 -0
  66. package/src/infrastructure/database/migrations/004_auth_platform.ts +112 -0
  67. package/src/infrastructure/database/migrations/runner.ts +120 -0
  68. package/src/infrastructure/database/mssql/index.ts +14 -0
  69. package/src/infrastructure/database/mssql/migrations.ts +299 -0
  70. package/src/infrastructure/database/mssql/mssql-account-lockout.ts +95 -0
  71. package/src/infrastructure/database/mssql/mssql-api-keys.ts +146 -0
  72. package/src/infrastructure/database/mssql/mssql-audit-log.ts +86 -0
  73. package/src/infrastructure/database/mssql/mssql-oauth-accounts.ts +118 -0
  74. package/src/infrastructure/database/mssql/mssql-password-history.ts +71 -0
  75. package/src/infrastructure/database/mssql/mssql-refresh-token-store.ts +144 -0
  76. package/src/infrastructure/database/mssql/mssql-token-blacklist.ts +54 -0
  77. package/src/infrastructure/database/mssql/mssql-user.repository.ts +263 -0
  78. package/src/infrastructure/database/mssql/mssql-verification-tokens.ts +120 -0
  79. package/src/infrastructure/database/postgres/index.ts +14 -0
  80. package/src/infrastructure/database/postgres/migrations.ts +235 -0
  81. package/src/infrastructure/database/postgres/pg-account-lockout.ts +75 -0
  82. package/src/infrastructure/database/postgres/pg-api-keys.ts +126 -0
  83. package/src/infrastructure/database/postgres/pg-audit-log.ts +74 -0
  84. package/src/infrastructure/database/postgres/pg-oauth-accounts.ts +101 -0
  85. package/src/infrastructure/database/postgres/pg-password-history.ts +61 -0
  86. package/src/infrastructure/database/postgres/pg-refresh-token-store.ts +117 -0
  87. package/src/infrastructure/database/postgres/pg-token-blacklist.ts +48 -0
  88. package/src/infrastructure/database/postgres/pg-user.repository.ts +237 -0
  89. package/src/infrastructure/database/postgres/pg-verification-tokens.ts +97 -0
  90. package/src/infrastructure/database/sqlite-account-lockout.ts +97 -0
  91. package/src/infrastructure/database/sqlite-api-keys.ts +155 -0
  92. package/src/infrastructure/database/sqlite-audit-log.ts +90 -0
  93. package/src/infrastructure/database/sqlite-oauth-accounts.ts +105 -0
  94. package/src/infrastructure/database/sqlite-password-history.ts +54 -0
  95. package/src/infrastructure/database/sqlite-refresh-token-store.ts +122 -0
  96. package/src/infrastructure/database/sqlite-token-blacklist.ts +47 -0
  97. package/src/infrastructure/database/sqlite-user.repository.ts +260 -0
  98. package/src/infrastructure/database/sqlite-verification-tokens.ts +112 -0
  99. package/src/infrastructure/events/event-bus.ts +105 -0
  100. package/src/infrastructure/events/event-factory.ts +31 -0
  101. package/src/infrastructure/events/in-memory-webhook-registry.ts +67 -0
  102. package/src/infrastructure/events/index.ts +4 -0
  103. package/src/infrastructure/events/webhook-dispatcher.ts +114 -0
  104. package/src/infrastructure/index.ts +58 -0
  105. package/src/infrastructure/jobs/index.ts +1 -0
  106. package/src/infrastructure/jobs/job-queue.ts +185 -0
  107. package/src/infrastructure/logging/index.ts +1 -0
  108. package/src/infrastructure/logging/logger.ts +63 -0
  109. package/src/infrastructure/metrics/index.ts +1 -0
  110. package/src/infrastructure/metrics/prometheus.ts +231 -0
  111. package/src/infrastructure/oauth/github.ts +116 -0
  112. package/src/infrastructure/oauth/google.ts +83 -0
  113. package/src/infrastructure/oauth/index.ts +2 -0
  114. package/src/infrastructure/resilience/circuit-breaker.ts +133 -0
  115. package/src/infrastructure/resilience/index.ts +2 -0
  116. package/src/infrastructure/resilience/retry.ts +50 -0
  117. package/src/infrastructure/security/account-lockout.ts +73 -0
  118. package/src/infrastructure/security/index.ts +6 -0
  119. package/src/infrastructure/security/password-hasher.ts +31 -0
  120. package/src/infrastructure/security/password-policy.ts +77 -0
  121. package/src/infrastructure/security/token-blacklist.ts +45 -0
  122. package/src/infrastructure/security/token-service.ts +144 -0
  123. package/src/infrastructure/security/totp-service.ts +142 -0
  124. package/src/infrastructure/tracing/index.ts +7 -0
  125. package/src/infrastructure/tracing/trace-context.ts +93 -0
  126. package/src/main.ts +479 -0
  127. package/src/presentation/context.ts +26 -0
  128. package/src/presentation/handlers/admin.handler.ts +114 -0
  129. package/src/presentation/handlers/api-key.handler.ts +68 -0
  130. package/src/presentation/handlers/auth.handler.ts +218 -0
  131. package/src/presentation/handlers/health.handler.ts +27 -0
  132. package/src/presentation/handlers/index.ts +15 -0
  133. package/src/presentation/handlers/metrics.handler.ts +21 -0
  134. package/src/presentation/handlers/oauth.handler.ts +61 -0
  135. package/src/presentation/handlers/openapi.handler.ts +543 -0
  136. package/src/presentation/handlers/response.ts +29 -0
  137. package/src/presentation/handlers/sse.handler.ts +165 -0
  138. package/src/presentation/handlers/user.handler.ts +81 -0
  139. package/src/presentation/handlers/webhook.handler.ts +92 -0
  140. package/src/presentation/handlers/websocket.handler.ts +226 -0
  141. package/src/presentation/i18n/index.ts +254 -0
  142. package/src/presentation/index.ts +5 -0
  143. package/src/presentation/middleware/api-key.ts +18 -0
  144. package/src/presentation/middleware/auth.ts +39 -0
  145. package/src/presentation/middleware/cors.ts +41 -0
  146. package/src/presentation/middleware/index.ts +12 -0
  147. package/src/presentation/middleware/rate-limit.ts +65 -0
  148. package/src/presentation/middleware/security-headers.ts +18 -0
  149. package/src/presentation/middleware/validate.ts +16 -0
  150. package/src/presentation/middleware/versioning.ts +69 -0
  151. package/src/presentation/routes/index.ts +1 -0
  152. package/src/presentation/routes/router.ts +272 -0
  153. package/src/presentation/server.ts +381 -0
  154. package/src/shared/cli.ts +294 -0
  155. package/src/shared/container.ts +65 -0
  156. package/src/shared/index.ts +2 -0
  157. package/src/shared/log-format.ts +148 -0
  158. package/src/shared/utils/id.ts +5 -0
  159. package/src/shared/utils/index.ts +2 -0
  160. package/src/shared/utils/timing-safe.ts +20 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,201 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.0] - 2026-02-16
9
+
10
+ ### Added
11
+
12
+ - **PostgreSQL adapter** — zero-dep Postgres support via `Bun.sql`; 9 repository implementations (user, token blacklist, account lockout, audit log, verification tokens, refresh token store, API keys, password history, OAuth accounts); automatic DDL migration runner (`pgMigrateUp` / `pgMigrateDown`); config-driven adapter selection (`DATABASE_DRIVER=sqlite|postgres`)
13
+ - **Redis cache adapter** — zero-dep Redis client over raw RESP protocol via `Bun.connect()` TCP; `Cache` port with `get`, `set`, `del`, `has`, `incr`, `delPattern`, `close` operations; in-memory fallback cache with TTL and auto-prune; config-driven selection (`REDIS_ENABLED=true|false`)
14
+ - **API versioning** — URL-based version negotiation (`/api/v1/...` and `/api/v2/...`); v2 paths normalized to v1 for shared handler lookup; v1 responses include `Deprecation: true`, `Sunset`, and `Link` headers; v2 responses include clean `API-Version: v2` header; `Accept-Version` header support
15
+ - **Internationalization (i18n)** — 5 language catalogs (en, es, fr, de, ja) with 33 message keys; RFC 7231 `Accept-Language` quality-value parsing; `Content-Language` response header; `resolveLocale`, `t()` translator, `createI18nContext` helpers; config via `I18N_DEFAULT_LOCALE` and `I18N_SUPPORTED_LOCALES`
16
+ - **Kubernetes manifests** — production-ready K8s resources: Namespace, ConfigMap, Secret, Deployment (2 replicas, rolling update, liveness/readiness/startup probes, resource limits), Service (ClusterIP), Ingress (nginx, TLS, cert-manager), HPA (CPU 70% / memory 80%, min 2 max 10), PodDisruptionBudget, NetworkPolicy (DNS, Postgres, Redis egress)
17
+ - **Helm chart** — full `helm/onlyapi/` chart with Chart.yaml (appVersion 2.0.0), values.yaml, and 8 templates (deployment, service, configmap, secret, ingress, HPA, serviceaccount, PDB); configurable replicas, probes, autoscaling, and secrets
18
+ - **CD pipeline** — `.github/workflows/deploy.yml` continuous deployment: Docker Buildx multi-arch builds (amd64 + arm64), push to GHCR, staging deploy with Helm + smoke test, production deploy with Helm + GitHub Release; triggered by `v*` tags or manual dispatch
19
+ - **E2E test suite** — 28 end-to-end tests in `tests/e2e/journey.test.ts` spawning a real server with isolated database; complete user journey (register → login → refresh → update → logout); API versioning headers; i18n Content-Language; security headers; ETag/304 conditional GET; CORS preflight; OpenAPI + Metrics
20
+ - **Load test harness** — `tests/load/harness.ts` automated performance regression detection; CLI-driven (--url, --duration, --concurrency, --max-p99, --min-rps); concurrent worker pool; latency percentile computation (p50/p90/p95/p99); 4 scenarios (health, register, login, metrics); threshold pass/fail
21
+ - **Postman collection** — `postman/onlyapi-v2.postman_collection.json` with all endpoints (health, auth, users, API keys, admin, webhooks, SSE, docs, metrics, v2 endpoints); auto-extraction scripts for tokens; variables for baseUrl, credentials, token management
22
+
23
+ ### Changed
24
+
25
+ - Config schema extended with `database.driver` (sqlite | postgres), `redis.*` (enabled, host, port, password, db), `i18n.*` (defaultLocale, supportedLocales)
26
+ - `main.ts` bootstrap conditionally selects SQLite or Postgres adapters and in-memory or Redis cache based on config
27
+ - DI container expanded with `Cache` token
28
+ - Server context includes `apiVersion` and `i18n` fields; responses include `Content-Language` and version headers
29
+ - Test count: 282 → 351 (267 unit + 56 integration + 28 E2E) across 36 files
30
+ - Version bumped to 2.0.0
31
+
32
+ ---
33
+
34
+ ## [1.5.0] - 2026-02-16
35
+
36
+ ### Added
37
+
38
+ - **WebSocket support** — Bun-native WebSocket upgrade at `/ws` with JSON protocol; authentication via JWT, pub/sub event subscriptions, broadcast domain events to connected clients; `WebSocketManager` with connection tracking, auth state, per-client event filtering
39
+ - **Server-Sent Events (SSE)** — `GET /api/v1/events/stream` streaming endpoint for real-time updates; auth via `?token=` query param or `Authorization` header; event type filtering via `?events=` param; 30s heartbeat keep-alive; `X-Accel-Buffering: no` for Nginx compatibility
40
+ - **Domain events** — 15 typed event types (`USER_REGISTERED`, `USER_DELETED`, `USER_UPDATED`, `LOGIN_SUCCESS`, `LOGIN_FAILED`, `LOGOUT`, `PASSWORD_CHANGED`, `PASSWORD_RESET`, `EMAIL_VERIFIED`, `MFA_ENABLED`, `MFA_DISABLED`, `API_KEY_CREATED`, `API_KEY_REVOKED`, `ACCOUNT_LOCKED`, `ACCOUNT_UNLOCKED`); `DomainEvent<T>` type with id, type, timestamp, optional userId/payload/ip; `DomainEventFactory` for event creation
41
+ - **Event bus** — in-memory pub/sub with fire-and-forget semantics; type-specific and wildcard (`subscribeAll`) subscriptions; error isolation (throwing handlers don't crash publisher); `EventBus` port ready to swap for Redis Pub/Sub or NATS
42
+ - **Webhooks** — admin-only CRUD API (`POST /api/v1/webhooks`, `GET /api/v1/webhooks`, `DELETE /api/v1/webhooks/:id`); HMAC-SHA256 signed delivery with `X-Webhook-Signature` header; event-type filtering per subscription; `WebhookRegistry` port + in-memory adapter; `WebhookDispatcher` with fire-and-forget delivery and AbortController timeout
43
+ - **Background job queue** — polling-based in-memory job processor with configurable interval; exponential backoff retry (`min(1000 * 2^attempts, 60000)` ms); dead letter queue for exhausted retries; job lifecycle (PENDING → RUNNING → COMPLETED / FAILED → DEAD); `JobQueue` port ready to swap for Redis/SQLite-backed adapter
44
+ - 3 new core ports: `EventBus`, `WebhookRegistry`, `JobQueue`
45
+ - 7 new DI container tokens: `EventBus`, `EventFactory`, `WebhookRegistry`, `WebhookDispatcher`, `JobQueue`, `WebSocketManager`, `SseHandler`
46
+ - WebSocket JSON protocol: `auth`, `subscribe`, `unsubscribe`, `ping` client messages; `connected`, `auth_result`, `subscribed`, `event`, `pong`, `error` server messages
47
+ - 36 new tests across 4 new test files + expanded integration tests (246 → 282 total, 32 files, 667 expect() calls)
48
+
49
+ ### Changed
50
+
51
+ - Router extended with webhook routes and SSE endpoint; new parametric matcher for `DELETE /api/v1/webhooks/:id`
52
+ - Server now conditionally enables Bun.serve() WebSocket config when `WebSocketManager` is provided
53
+ - `main.ts` bootstrap wires event bus, webhook dispatcher, job queue; webhook dispatcher auto-subscribed as wildcard event listener; WebSocket broadcast wired as wildcard subscriber; job queue starts after server and stops on graceful shutdown
54
+ - Version bumped to 1.5.0
55
+
56
+ ---
57
+
58
+ ## [1.4.0] - 2026-02-16
59
+
60
+ ### Added
61
+
62
+ - **Email verification** — `POST /api/v1/auth/verify-email` with SHA-256 hashed, time-limited tokens (24h TTL); `VerificationTokenRepository` port + SQLite adapter; supports email-verification and password-reset token types
63
+ - **Password reset** — `POST /api/v1/auth/forgot-password` (non-enumerable, always returns 200) + `POST /api/v1/auth/reset-password` with secure token-based flow (1h TTL); prevents information disclosure about registered emails
64
+ - **Refresh token rotation** — one-time-use refresh tokens with family-based reuse detection; `RefreshTokenStore` port + SQLite adapter; token reuse revokes entire family to mitigate stolen token replay attacks
65
+ - **MFA / 2FA (TOTP)** — RFC 6238 TOTP implementation using `Bun.CryptoHasher` HMAC-SHA1; setup/enable/disable/verify endpoints; Google Authenticator-compatible `otpauth://` URI generation; base32 encoding (zero-dep); time-step window of ±1 for clock drift tolerance
66
+ - **OAuth2 / SSO** — Google and GitHub provider adapters with authorization URL generation and token exchange; `OAuthProvider` port + `OAuthAccountRepository` for provider-account linking; conditional registration based on `OAUTH_GOOGLE_CLIENT_ID` / `OAUTH_GITHUB_CLIENT_ID` environment variables
67
+ - **API key auth** — `POST /api/v1/api-keys` (create), `GET /api/v1/api-keys` (list), `DELETE /api/v1/api-keys/:id` (revoke); `oapi_` prefixed keys with SHA-256 hashed storage; `X-API-Key` header authentication; configurable scopes and expiry; `ApiKeyRepository` port + SQLite adapter
68
+ - **Password policy** — configurable complexity rules (min length, uppercase, lowercase, digit, special character); password history checking to prevent reuse of last N passwords; password expiry detection; `PasswordPolicy` service + `PasswordHistory` port + SQLite adapter
69
+ - Migration 004: `auth_platform` — creates `verification_tokens`, `refresh_tokens`, `api_keys`, `password_history`, `oauth_accounts` tables with appropriate indexes
70
+ - `TotpService` port + infrastructure adapter (RFC 6238, HMAC-SHA1 via Bun.CryptoHasher, base32 codec)
71
+ - 9 new DI container tokens: `VerificationTokenRepository`, `RefreshTokenStore`, `ApiKeyRepository`, `PasswordHistory`, `PasswordPolicy`, `TotpService`, `OAuthProviders`, `OAuthAccountRepository`, `ApiKeyService`
72
+ - Password policy config section: `PASSWORD_MIN_LENGTH`, `PASSWORD_REQUIRE_UPPERCASE`, `PASSWORD_REQUIRE_LOWERCASE`, `PASSWORD_REQUIRE_DIGIT`, `PASSWORD_REQUIRE_SPECIAL`, `PASSWORD_HISTORY_COUNT`, `PASSWORD_MAX_AGE_DAYS`
73
+ - OAuth config section: `OAUTH_GOOGLE_CLIENT_ID`, `OAUTH_GOOGLE_CLIENT_SECRET`, `OAUTH_GITHUB_CLIENT_ID`, `OAUTH_GITHUB_CLIENT_SECRET`
74
+ - 68 new tests across 7 new test files + expanded integration tests (178 → 246 total, 28 files, 589 expect() calls)
75
+
76
+ ### Changed
77
+
78
+ - `AuthService` now accepts 13 dependencies (was 6): adds verification tokens, refresh token store, TOTP service, OAuth providers, OAuth accounts, API key repository, password history, password policy
79
+ - `User` entity extended with `emailVerified`, `mfaEnabled`, `mfaSecret`, `passwordChangedAt` fields
80
+ - `InMemoryUserRepository` updated to initialize and handle all new user fields
81
+ - Router now supports OAuth, MFA, API key, and email verification route groups
82
+ - Startup banner displays v1.4.0; pruning interval covers verification tokens and refresh tokens in addition to blacklisted JWT tokens
83
+ - Version bumped to 1.4.0
84
+
85
+ ---
86
+
87
+ ## [1.3.0] - 2026-02-15
88
+
89
+ ### Added
90
+
91
+ - **Prometheus metrics** — `GET /metrics` endpoint serving Prometheus text exposition format (v0.0.4) with counters, histograms, and gauges: `http_requests_total`, `http_request_duration_ms`, `http_active_connections`, `http_errors_total`, `circuit_breaker_state`, `alerts_sent_total`
92
+ - **OpenTelemetry traces** — W3C Trace Context propagation (`traceparent` header), 128-bit trace IDs + 64-bit span IDs generated per request, trace context included in structured JSON logs and response headers
93
+ - **Circuit breaker** — resilience pattern for external service calls with configurable failure threshold, reset timeout, and half-open success threshold; state machine (CLOSED → OPEN → HALF_OPEN → CLOSED); `CircuitBreaker` port + infrastructure adapter
94
+ - **Retry with backoff** — configurable retry policy with exponential backoff, jitter, max delay cap, retryable predicate, and per-attempt callbacks; `RetryPolicy` port + infrastructure adapter
95
+ - **Graceful degradation** — health service now monitors circuit breaker states; reports `degraded` status when downstream services fail; `GET /readiness` reflects circuit breaker health in component checks
96
+ - **Alerting hooks** — `AlertSink` port with webhook adapter (`ALERT_WEBHOOK_URL`); fires on circuit breaker OPEN/recovery, unhandled rejections; `createNoopAlertSink` when no URL configured; retry with backoff on webhook delivery failures
97
+ - `MetricsCollector` port with zero-dependency Prometheus adapter (counters, histograms with configurable buckets, gauges with label support)
98
+ - `CircuitBreakerOptions` config section (`CB_FAILURE_THRESHOLD`, `CB_RESET_TIMEOUT_MS`, `CB_HALF_OPEN_SUCCESS_THRESHOLD`)
99
+ - `AlertSink` config section (`ALERT_WEBHOOK_URL`, `ALERT_TIMEOUT_MS`)
100
+ - `TraceContext` type with `traceId`, `spanId`, `parentSpanId`, `flags`
101
+ - `RequestContext` extended with `trace: TraceContext` field
102
+ - Child loggers now include `traceId` and `spanId` bindings for structured log correlation
103
+ - New DI tokens: `MetricsCollector`, `AlertSink`
104
+ - 58 new tests (120 → 178 total across 21 files)
105
+
106
+ ### Changed
107
+
108
+ - `ComponentHealth.status` now supports `"degraded"` in addition to `"ok"` | `"down"`
109
+ - `HealthService` accepts optional `circuitBreakers` array for graceful degradation monitoring
110
+ - Server hot path now tracks metrics (request count, duration, active connections, errors) without measurable latency impact
111
+ - All responses now include `traceparent` header for distributed tracing correlation
112
+ - Unhandled promise rejections now fire alerting hooks in addition to logging
113
+
114
+ ---
115
+
116
+ ## [1.2.0] - 2026-02-15
117
+
118
+ ### Added
119
+
120
+ - **Admin endpoints** — `GET /api/v1/admin/users` (list, search, filter by role), `GET /api/v1/admin/users/:id`, `PATCH /api/v1/admin/users/:id/role`, `POST /api/v1/admin/users/:id/ban`, `POST /api/v1/admin/users/:id/unban`
121
+ - **Cursor-based pagination** — opaque base64 cursor, `?cursor=X&limit=20` on list endpoints, `PaginatedResult<T>` type
122
+ - **OpenAPI 3.1 specification** — auto-generated from Zod schemas at `GET /docs` (JSON) and `GET /docs/html` (embedded Swagger UI)
123
+ - **ETag / conditional requests** — MD5-based ETag on GET 200 responses, `If-None-Match` → 304 Not Modified
124
+ - **Request ID tracing** — per-request child logger with `requestId` binding, propagated via `X-Request-Id` header
125
+ - **Structured JSON log mode** — `LOG_FORMAT=json` for production log aggregators (Datadog, ELK, CloudWatch)
126
+ - **Audit log** — append-only ledger of significant system events (who, what, when, IP), SQLite-backed with `AuditLog` port
127
+ - `AdminService` with role-based authorization, self-action prevention
128
+ - `AuditLog` port + SQLite adapter with query filters (userId, action, since, limit)
129
+ - Migration 003: `audit_log` table with indexes
130
+ - Parametric route matching for admin endpoints (prefix-based, O(1) static + O(n) parametric)
131
+ - Admin DTOs: `listUsersDto`, `changeRoleDto`, `banUserDto` validated via Zod
132
+ - `UserRepository` extended with `list(options)` and `count()` methods
133
+ - 30 new tests (90 unit + 30 integration = 120 total)
134
+ - `LOG_FORMAT` config option (`pretty` | `json`)
135
+
136
+ ### Changed
137
+
138
+ - `authorise()` middleware now returns 403 Forbidden (was incorrectly 401) for role mismatches
139
+ - Logger `createLogger()` accepts optional `format` parameter (`"pretty"` | `"json"`)
140
+ - `RequestContext` includes request-scoped `logger` field
141
+ - Router supports both static O(1) map and parametric admin route matching
142
+ - Version bumped to 1.2.0 in package.json and startup banner
143
+ - CLI startup banner displays new admin + docs routes and log format
144
+ - Biome config: `useLiteralKeys` disabled globally (incompatible with TS `noPropertyAccessFromIndexSignature`)
145
+ - Migration tests updated for 3-migration schema
146
+
147
+ ## [1.1.0] - 2026-02-15
148
+
149
+ ### Added
150
+
151
+ - **SQLite persistence** via `bun:sqlite` — zero external dependencies, WAL mode, prepared statements
152
+ - **Database migrations** — versioned TypeScript migrations with up/down, tracked in `_migrations` table
153
+ - **Logout endpoint** — `POST /api/v1/auth/logout` with token blacklist (in-memory + SQLite adapters)
154
+ - **Token blacklist** — SHA-256 hashed tokens, auto-pruning expired entries, Redis-ready port
155
+ - **Account lockout** — configurable max attempts + lockout duration, resets on successful login
156
+ - **Dockerfile** — multi-stage build with `distroless` base, health check, non-root user
157
+ - **docker-compose.yml** — app + SQLite volume, all env vars configurable
158
+ - **Test coverage** — `bun test --coverage` script, 67 tests (up from 41)
159
+ - `AccountLockout` port + in-memory and SQLite adapters
160
+ - `TokenBlacklist` port + in-memory and SQLite adapters
161
+ - `SqliteUserRepository` adapter with prepared statements
162
+ - New unit tests: SQLite user repository, migrations, token blacklist, account lockout
163
+ - New integration tests: logout flow, blacklisted token rejection, account lockout
164
+ - `DATABASE_PATH`, `LOCKOUT_MAX_ATTEMPTS`, `LOCKOUT_DURATION_MS` config options
165
+
166
+ ### Changed
167
+
168
+ - `AuthService` now requires `TokenBlacklist` and `AccountLockout` dependencies
169
+ - `main.ts` bootstrap is now async, initializes SQLite + runs migrations at boot
170
+ - Auth handler receives `TokenService` for logout authentication
171
+ - Version bumped to 1.1.0 in package.json and startup banner
172
+ - Refresh token rotation now blacklists the old refresh token
173
+ - Login flow checks account lockout before credential verification
174
+
175
+ ## [1.0.0] - 2026-02-11
176
+
177
+ ### Added
178
+
179
+ - Clean Architecture project structure (core / application / infrastructure / presentation)
180
+ - Bun.serve() HTTP server with inlined hot-path optimizations
181
+ - O(1) Map-based router — no regex, no radix tree
182
+ - `Result<T, E>` monad for error handling (no throw-based control flow)
183
+ - Branded types: `UserId`, `RequestId`, `Timestamp`
184
+ - Auth endpoints: register, login, refresh
185
+ - User endpoints: get profile, update, delete
186
+ - Health endpoints: shallow (`/health`) and deep (`/readiness`)
187
+ - Argon2id password hashing via Bun.password (native)
188
+ - HMAC-SHA256 JWT via Web Crypto API (zero external deps)
189
+ - Zod-validated environment config with fail-fast on boot
190
+ - Structured JSON logger with batched async writes
191
+ - Per-IP sliding-window rate limiter (inlined on hot path)
192
+ - Security headers middleware (CSP, HSTS, X-Frame-Options, etc.)
193
+ - CORS middleware with configurable origin allowlist
194
+ - Request validation middleware using Zod schemas
195
+ - Bearer token auth middleware
196
+ - Multi-process clustering with SO_REUSEPORT (`src/cluster.ts`)
197
+ - DI container with symbol tokens (no decorators, no reflect-metadata)
198
+ - 41 tests (unit + integration) — all passing
199
+ - Biome linting with strict rules (no `any`, no `!`, cognitive complexity cap)
200
+ - TypeScript strict mode with 22+ compiler flags
201
+ - Benchmarking setup with bombardier
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 onlyApi Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ <div align="center">
2
+
3
+ # onlyApi
4
+
5
+ **Production-ready REST API foundation built on [Bun](https://bun.sh)**
6
+
7
+ Zero unnecessary dependencies. Strictest TypeScript. Clean architecture. Enterprise security.
8
+
9
+ [![CI](https://github.com/lysari/onlyapi/actions/workflows/ci.yml/badge.svg)](https://github.com/lysari/onlyapi/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
+ [![Bun](https://img.shields.io/badge/runtime-Bun_1.3-f472b6)](https://bun.sh)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)](tsconfig.json)
13
+ [![Tests](https://img.shields.io/badge/tests-351_passing-brightgreen)]()
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## Overview
20
+
21
+ onlyApi is a batteries-included API starter that ships everything you need to go from `git clone` to production — authentication, database, caching, observability, deployment — without pulling in a single framework or ORM. One runtime dependency (`zod`). ~78 KB minified.
22
+
23
+ ```bash
24
+ bunx onlyapi init my-api && cd my-api && bun run dev
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ ### Performance
32
+
33
+ - **~30K req/s** single-core on a MacBook; scales linearly via `SO_REUSEPORT` clustering
34
+ - O(1) Map-based routing — no regex matching, no radix tree traversal
35
+ - Pathname extracted with string slicing — no `new URL()` allocation (~12x faster)
36
+ - Pre-serialized static responses — `/health` avoids `JSON.stringify` entirely
37
+ - Batched async access logging — one `write()` syscall per 100ms flush window
38
+ - Inlined rate limiter on the hot path — zero function-call overhead
39
+
40
+ ### Authentication & Authorization
41
+
42
+ - **JWT** — HMAC-SHA256 via Web Crypto API; access + refresh token pair
43
+ - **Refresh token rotation** — one-time-use tokens with family-based reuse detection
44
+ - **MFA / TOTP** — RFC 6238 implementation, Google Authenticator compatible
45
+ - **OAuth2 / SSO** — Google and GitHub provider adapters
46
+ - **API key auth** — `X-API-Key` header for service-to-service communication
47
+ - **Email verification** — SHA-256 hashed, time-limited verification tokens
48
+ - **Password reset** — secure token-based flow with non-enumerable responses
49
+ - **Password policy** — configurable complexity, history tracking, reuse prevention, expiry
50
+ - **Account lockout** — automatic lock after N consecutive failed login attempts
51
+ - **Token blacklist** — logout invalidates both access and refresh tokens
52
+
53
+ ### Database
54
+
55
+ - **SQLite** — `bun:sqlite` with WAL mode, zero-dep, built-in migrations
56
+ - **PostgreSQL** — `Bun.sql` with 9 repository implementations and DDL migration runner
57
+ - **SQL Server** — `mssql` (tedious TDS) with 9 repository adapters, T-SQL migrations, and stored procedure support
58
+ - **In-memory** — testing adapter with full repository interface compliance
59
+ - Config-driven adapter selection via `DATABASE_DRIVER=sqlite|postgres|mssql`
60
+
61
+ ### Caching
62
+
63
+ - **In-memory cache** — Map-based with TTL and automatic prune interval
64
+ - **Redis cache** — zero-dep implementation over raw RESP protocol via `Bun.connect()` TCP
65
+ - Unified `Cache` port — `get`, `set`, `del`, `has`, `incr`, `delPattern`, `close`
66
+
67
+ ### Real-time
68
+
69
+ - **WebSocket** — Bun-native upgrade at `/ws` with JSON protocol, JWT auth, pub/sub subscriptions
70
+ - **Server-Sent Events** — `GET /api/v1/events/stream` with auth, event filtering, 30s heartbeat
71
+ - **Domain events** — 15 typed events (`USER_REGISTERED`, `LOGIN_FAILED`, `MFA_ENABLED`, etc.)
72
+ - **Event bus** — in-memory pub/sub with type-specific and wildcard subscriptions
73
+ - **Webhooks** — outbound HTTP with HMAC-SHA256 signatures, event-type filtering per subscription
74
+ - **Background job queue** — async processing with exponential backoff retry and dead letter queue
75
+
76
+ ### Observability
77
+
78
+ - **Prometheus metrics** — `GET /metrics` with request count, latency histogram, error rate, active connections
79
+ - **OpenTelemetry traces** — distributed tracing with W3C `traceparent` propagation
80
+ - **Structured logging** — JSON mode for Datadog / ELK; batched async writes in production
81
+ - **Audit log** — append-only record of user actions with IP, timestamp, and user ID
82
+ - **Health checks** — shallow `/health` (instant) and deep `/readiness` (service connectivity)
83
+ - **Alerting hooks** — webhook notifications on fatal errors and health degradation
84
+
85
+ ### API Design
86
+
87
+ - **OpenAPI 3.1** — auto-generated spec served at `GET /docs`; Swagger UI at `GET /docs/html`
88
+ - **API versioning** — `/api/v1/` and `/api/v2/` coexist; v1 returns `Deprecation` + `Sunset` headers
89
+ - **ETag / conditional GET** — `If-None-Match` support with `304 Not Modified` responses
90
+ - **Cursor-based pagination** — `?cursor=X&limit=20` on list endpoints
91
+ - **Request ID tracing** — `X-Request-Id` propagated through loggers and echoed in responses
92
+ - **i18n** — 5 languages (en, es, fr, de, ja), 33 message keys, RFC 7231 `Accept-Language` negotiation
93
+
94
+ ### Security
95
+
96
+ - **Argon2id** — Bun-native password hashing, no C bindings
97
+ - **CORS** — configurable origin allowlist with preflight caching
98
+ - **Rate limiting** — per-IP sliding window with `Retry-After` headers
99
+ - **Security headers** — `X-Content-Type-Options`, `X-Frame-Options`, `Strict-Transport-Security`, and more
100
+ - **`Result<T, E>` monad** — no thrown exceptions; errors never leak internal state
101
+
102
+ ### Infrastructure & Deployment
103
+
104
+ - **Dockerfile** — multi-stage build with distroless base, <20 MB image
105
+ - **docker-compose** — app + SQLite volume, ready to run
106
+ - **Kubernetes manifests** — Namespace, ConfigMap, Secret, Deployment, Service, Ingress, HPA, PDB, NetworkPolicy
107
+ - **Helm chart** — parameterized deployment with 8 templates
108
+ - **CI pipeline** — GitHub Actions: lint, type-check, test, build
109
+ - **CD pipeline** — multi-arch Docker build (amd64 + arm64), staging deploy with smoke test, production deploy with GitHub Release
110
+ - **Postman collection** — all endpoints with auto-token extraction scripts
111
+
112
+ ### Developer Experience
113
+
114
+ - **CLI scaffolding** — `bunx onlyapi init my-api` creates a full project in seconds
115
+ - **CLI upgrade** — `onlyapi upgrade` updates framework internals while preserving custom code
116
+ - **Hot reload** — `bun run dev` with `--watch` mode
117
+ - **351 tests** — unit, integration, E2E, and load testing across 36 files
118
+ - **Biome** — fast linter and formatter, zero-config
119
+ - **22+ strict TypeScript flags** — branded types, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`
120
+
121
+ ---
122
+
123
+ ## Architecture
124
+
125
+ ```
126
+ src/
127
+ ├── core/ # Domain — zero dependencies
128
+ │ ├── entities/ # User, UserRole
129
+ │ ├── errors/ # AppError, canonical error codes
130
+ │ ├── ports/ # Interfaces: repos, services, cache, events
131
+ │ └── types/ # Branded types, Result<T,E> monad
132
+ ├── application/ # Use cases
133
+ │ ├── dtos/ # Zod request schemas
134
+ │ └── services/ # Auth, User, Health
135
+ ├── infrastructure/ # Adapters
136
+ │ ├── config/ # Zod-validated env config
137
+ │ ├── database/ # SQLite, PostgreSQL, SQL Server, in-memory
138
+ │ ├── cache/ # In-memory, Redis (raw RESP)
139
+ │ ├── logging/ # Structured JSON logger
140
+ │ └── security/ # Argon2id, HMAC-SHA256 JWT
141
+ ├── presentation/ # HTTP
142
+ │ ├── handlers/ # Route handlers
143
+ │ ├── middleware/ # CORS, auth, rate-limit, versioning
144
+ │ ├── i18n/ # Language catalogs
145
+ │ ├── routes/ # O(1) Map router
146
+ │ └── server.ts # Bun.serve() hot-path
147
+ ├── shared/ # DI container, utilities
148
+ ├── cluster.ts # SO_REUSEPORT multi-process
149
+ └── main.ts # Bootstrap + graceful shutdown
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Quick Start
155
+
156
+ ### Prerequisites
157
+
158
+ - [Bun](https://bun.sh) >= 1.1
159
+
160
+ ### Scaffold a Project
161
+
162
+ ```bash
163
+ bunx onlyapi init my-api
164
+ cd my-api
165
+ bun run dev
166
+ ```
167
+
168
+ ### Or Clone Manually
169
+
170
+ ```bash
171
+ git clone https://github.com/lysari/onlyapi.git
172
+ cd onlyApi
173
+ bun install
174
+ cp .env.example .env
175
+ bun run dev
176
+ ```
177
+
178
+ ### Production
179
+
180
+ ```bash
181
+ # Single process
182
+ bun run start
183
+
184
+ # Multi-process cluster (1 worker per CPU core)
185
+ bun run start:cluster
186
+ ```
187
+
188
+ ---
189
+
190
+ ## API Endpoints
191
+
192
+ | Method | Path | Auth | Description |
193
+ |--------|------|------|-------------|
194
+ | `GET` | `/health` | — | Shallow health check |
195
+ | `GET` | `/readiness` | — | Deep readiness check |
196
+ | `GET` | `/docs` | — | OpenAPI 3.1 JSON |
197
+ | `GET` | `/docs/html` | — | Swagger UI |
198
+ | `GET` | `/metrics` | — | Prometheus metrics |
199
+ | `POST` | `/api/v1/auth/register` | — | Register user |
200
+ | `POST` | `/api/v1/auth/login` | — | Login, returns JWT pair |
201
+ | `POST` | `/api/v1/auth/refresh` | — | Refresh access token |
202
+ | `POST` | `/api/v1/auth/logout` | Bearer | Logout + blacklist tokens |
203
+ | `POST` | `/api/v1/auth/verify-email` | — | Verify email token |
204
+ | `POST` | `/api/v1/auth/forgot-password` | — | Request password reset |
205
+ | `POST` | `/api/v1/auth/reset-password` | — | Reset password with token |
206
+ | `POST` | `/api/v1/auth/mfa/setup` | Bearer | Generate TOTP secret |
207
+ | `POST` | `/api/v1/auth/mfa/enable` | Bearer | Enable MFA |
208
+ | `POST` | `/api/v1/auth/mfa/disable` | Bearer | Disable MFA |
209
+ | `POST` | `/api/v1/auth/mfa/verify` | Bearer | Verify TOTP code |
210
+ | `GET` | `/api/v1/auth/oauth/:provider` | — | OAuth2 redirect |
211
+ | `GET` | `/api/v1/auth/oauth/:provider/callback` | — | OAuth2 callback |
212
+ | `GET` | `/api/v1/users/me` | Bearer | Current user profile |
213
+ | `PATCH` | `/api/v1/users/me` | Bearer | Update profile |
214
+ | `DELETE` | `/api/v1/users/me` | Bearer | Delete account |
215
+ | `POST` | `/api/v1/api-keys` | Bearer | Create API key |
216
+ | `GET` | `/api/v1/api-keys` | Bearer | List API keys |
217
+ | `DELETE` | `/api/v1/api-keys/:id` | Bearer | Revoke API key |
218
+ | `POST` | `/api/v1/webhooks` | Admin | Create webhook |
219
+ | `GET` | `/api/v1/webhooks` | Admin | List webhooks |
220
+ | `DELETE` | `/api/v1/webhooks/:id` | Admin | Remove webhook |
221
+ | `GET` | `/api/v1/events/stream` | Bearer | SSE event stream |
222
+ | `WS` | `/ws` | JWT | WebSocket connection |
223
+
224
+ All v1 endpoints are also available under `/api/v2/` with clean version headers.
225
+
226
+ ### Usage
227
+
228
+ ```bash
229
+ # Register
230
+ curl -s -X POST http://localhost:3000/api/v1/auth/register \
231
+ -H "Content-Type: application/json" \
232
+ -d '{"email":"user@example.com","password":"Str0ngP@ss!"}'
233
+
234
+ # Login
235
+ curl -s -X POST http://localhost:3000/api/v1/auth/login \
236
+ -H "Content-Type: application/json" \
237
+ -d '{"email":"user@example.com","password":"Str0ngP@ss!"}'
238
+
239
+ # Authenticated request
240
+ curl -s http://localhost:3000/api/v1/users/me \
241
+ -H "Authorization: Bearer <token>"
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Configuration
247
+
248
+ Environment variables are validated with Zod at startup. Invalid config crashes immediately — not at 3 AM in production.
249
+
250
+ | Variable | Default | Description |
251
+ |----------|---------|-------------|
252
+ | `PORT` | `3000` | Server port |
253
+ | `HOST` | `0.0.0.0` | Bind address |
254
+ | `NODE_ENV` | `development` | Environment mode |
255
+ | `JWT_SECRET` | — | **Required.** Min 32 characters |
256
+ | `JWT_EXPIRES_IN` | `15m` | Access token TTL |
257
+ | `JWT_REFRESH_EXPIRES_IN` | `7d` | Refresh token TTL |
258
+ | `DATABASE_DRIVER` | `sqlite` | `sqlite`, `postgres`, or `mssql` |
259
+ | `DATABASE_URL` | — | PostgreSQL or SQL Server connection string |
260
+ | `REDIS_ENABLED` | `false` | Enable Redis cache layer |
261
+ | `REDIS_HOST` | `127.0.0.1` | Redis hostname |
262
+ | `REDIS_PORT` | `6379` | Redis port |
263
+ | `REDIS_PASSWORD` | — | Redis password |
264
+ | `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
265
+ | `RATE_LIMIT_WINDOW_MS` | `60000` | Rate limit window (ms) |
266
+ | `RATE_LIMIT_MAX_REQUESTS` | `100` | Max requests per window |
267
+ | `LOG_LEVEL` | `debug` | `debug` \| `info` \| `warn` \| `error` \| `fatal` |
268
+ | `WORKERS` | CPU count | Cluster worker count |
269
+ | `I18N_DEFAULT_LOCALE` | `en` | Default response locale |
270
+ | `I18N_SUPPORTED_LOCALES` | `en` | Comma-separated supported locales |
271
+ | `PASSWORD_MIN_LENGTH` | `8` | Minimum password length |
272
+ | `PASSWORD_HISTORY_COUNT` | `5` | Previous passwords blocked from reuse |
273
+ | `OAUTH_GOOGLE_CLIENT_ID` | — | Google OAuth2 client ID |
274
+ | `OAUTH_GOOGLE_CLIENT_SECRET` | — | Google OAuth2 client secret |
275
+ | `OAUTH_GITHUB_CLIENT_ID` | — | GitHub OAuth2 client ID |
276
+ | `OAUTH_GITHUB_CLIENT_SECRET` | — | GitHub OAuth2 client secret |
277
+
278
+ ---
279
+
280
+ ## Scripts
281
+
282
+ | Script | Description |
283
+ |--------|-------------|
284
+ | `bun run dev` | Development server with hot-reload |
285
+ | `bun run start` | Production single-process |
286
+ | `bun run start:cluster` | Production multi-process |
287
+ | `bun run build` | Bundle and minify to `dist/` |
288
+ | `bun run check` | TypeScript type-check |
289
+ | `bun test` | Run all 351 tests |
290
+ | `bun run lint` | Lint with Biome |
291
+ | `bun run lint:fix` | Auto-fix lint issues |
292
+
293
+ ---
294
+
295
+ ## Performance
296
+
297
+ Benchmarked on MacBook Pro (Intel i7-9750H, 12 threads) with [bombardier](https://github.com/codesenberg/bombardier):
298
+
299
+ | Mode | Connections | Requests/sec | Avg Latency |
300
+ |------|-------------|-------------|-------------|
301
+ | Single process | 512 | **29,525** | 17.3ms |
302
+ | Cluster (12 workers) | 512 | **32,415** | 15.8ms |
303
+
304
+ > Localhost-constrained (server + load generator share CPU). Expect 5–10x on dedicated hardware.
305
+
306
+ ---
307
+
308
+ ## Testing
309
+
310
+ ```bash
311
+ bun test # All 351 tests
312
+ bun test --watch # Watch mode
313
+ bun test tests/unit/ # Unit only
314
+ bun test tests/integration/ # Integration only
315
+ bun test tests/e2e/ # End-to-end only
316
+ bun run tests/load/harness.ts # Load test with threshold checks
317
+ ```
318
+
319
+ | Layer | Coverage |
320
+ |-------|----------|
321
+ | **Unit** | Result monad, AppError, password hashing, JWT, repositories, TOTP, password policy, cache, i18n, versioning, event bus, webhooks, job queue |
322
+ | **Integration** | Full HTTP lifecycle — health, auth flow, email verification, MFA, API keys, password policy, webhooks, SSE, CORS |
323
+ | **E2E** | Complete user journey against a live server — register through logout, versioning headers, i18n, ETag/304, security headers |
324
+ | **Load** | Automated regression detection — p50/p90/p95/p99 latency, throughput thresholds, error rate checks |
325
+
326
+ ---
327
+
328
+ ## Contributing
329
+
330
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
331
+
332
+ ## Security
333
+
334
+ See [SECURITY.md](SECURITY.md) for reporting vulnerabilities.
335
+
336
+ ## License
337
+
338
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var Lz=Object.create;var{getPrototypeOf:Mz,defineProperty:Qz,getOwnPropertyNames:Az}=Object;var vz=Object.prototype.hasOwnProperty;var g=(z,$,H)=>{H=z!=null?Lz(Mz(z)):{};let J=$||!z||!z.__esModule?Qz(H,"default",{value:z,enumerable:!0}):H;for(let T of Az(z))if(!vz.call(J,T))Qz(J,T,{get:()=>z[T],enumerable:!0});return J};var m=import.meta.require;var N=(z)=>`\x1B[${z}m`,w=N("0"),K=(z)=>`${N("1")}${z}${w}`,X=(z)=>`${N("2")}${z}${w}`,q=(z)=>`${N("36")}${z}${w}`,R=(z)=>`${N("32")}${z}${w}`,k=(z)=>`${N("33")}${z}${w}`,Wz=(z)=>`${N("35")}${z}${w}`,Cz=(z)=>`${N("34")}${z}${w}`,Zz=(z)=>`${N("31")}${z}${w}`,v=(z)=>`${N("90")}${z}${w}`,L=(z)=>`${N("97")}${z}${w}`;var _={success:R("\u2714"),error:Zz("\u2717"),warning:k("\u26A0"),info:q("\u2139"),arrow:q("\u2192"),chevron:q("\u203A"),sparkle:Wz("\u2726"),bolt:k("\u26A1"),folder:Cz("\uD83D\uDCC1"),file:v("\uD83D\uDCC4"),gear:v("\u2699"),rocket:Wz("\uD83D\uDE80"),package:q("\uD83D\uDCE6")},d=(z)=>{return[`${K(q(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"))}`,`${K(q(" \u2502"))} ${K(q("\u2502"))}`,`${K(q(" \u2502"))} ${K(L("\u26A1 onlyApi CLI"))} ${X(v(`v${z}`))} ${K(q("\u2502"))}`,`${K(q(" \u2502"))} ${X(v("Zero-dep enterprise REST API on Bun"))} ${K(q("\u2502"))}`,`${K(q(" \u2502"))} ${K(q("\u2502"))}`,`${K(q(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"))}`].join(`
4
+ `)},Q=(z)=>process.stdout.write(`${z}
5
+ `),Z=()=>process.stdout.write(`
6
+ `),M=(z)=>process.stderr.write(` ${_.error} ${Zz(z)}
7
+ `),h=(z)=>process.stdout.write(` ${_.warning} ${k(z)}
8
+ `),C=(z)=>process.stdout.write(` ${_.info} ${z}
9
+ `),e=(z)=>process.stdout.write(` ${_.success} ${R(z)}
10
+ `),D=(z)=>process.stdout.write(` ${_.chevron} ${z}
11
+ `),Xz=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],y=(z)=>{let $=0,H=null,J=z,T=()=>{process.stdout.write("\r\x1B[K")};return{start(){H=setInterval(()=>{T();let W=Xz[$%Xz.length]??"\u280B";process.stdout.write(` ${q(W)} ${J}`),$++},80)},update(W){J=W},stop(W){if(H)clearInterval(H);if(T(),W)process.stdout.write(` ${_.success} ${R(W)}
12
+ `)}}},$z=async(z,$)=>{let H=$?` ${X(`(${$})`)}`:"";process.stdout.write(` ${_.chevron} ${z}${H}: `);let J=Bun.stdin.stream().getReader(),{value:T}=await J.read();return J.releaseLock(),(T?new TextDecoder().decode(T).trim():"")||$||""},c=async(z,$=!0)=>{let H=$?`${K("Y")}/n`:`y/${K("N")}`;process.stdout.write(` ${_.chevron} ${z} ${X(`[${H}]`)}: `);let J=Bun.stdin.stream().getReader(),{value:T}=await J.read();J.releaseLock();let W=T?new TextDecoder().decode(T).trim().toLowerCase():"";if(W==="")return $;return W==="y"||W==="yes"},zz=(z)=>{let $=Math.max(...z.map(([H])=>H.length));for(let[H,J]of z)Q(` ${v("\u2502")} ${X(H.padEnd($))} ${L(J)}`)},b=(z)=>{Z(),Q(` ${K(L(z))}`),Q(` ${v("\u2500".repeat(50))}`)};var s=(z)=>{if(z<1000)return`${Math.round(z)}ms`;if(z<60000)return`${(z/1000).toFixed(1)}s`;return`${Math.floor(z/60000)}m ${Math.round(z%60000/1000)}s`},Hz=(z=64)=>{let H=crypto.getRandomValues(new Uint8Array(z));return Array.from(H,(J)=>"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[J%62]).join("")};var Jz=(z)=>{Z(),Q(d(z)),Z(),Q(` ${K(L("USAGE"))}`),Q(` ${v("\u2500".repeat(50))}`),Q(` ${X("$")} ${q("onlyapi")} ${R("<command>")} ${X("[options]")}`),Z(),Q(` ${K(L("COMMANDS"))}`),Q(` ${v("\u2500".repeat(50))}`);let $=[["init <name>","Create a new onlyApi project"],["upgrade","Upgrade current project to latest version"],["version","Show CLI version"],["help","Show this help message"]],H=Math.max(...$.map(([B])=>B.length));for(let[B,E]of $)Q(` ${R(B.padEnd(H+2))} ${X(E)}`);Z(),Q(` ${K(L("INIT OPTIONS"))}`),Q(` ${v("\u2500".repeat(50))}`);let J=[[".","Initialize in the current directory"],["--cwd","Same as '.' \u2014 use current directory"]],T=Math.max(...J.map(([B])=>B.length));for(let[B,E]of J)Q(` ${k(B.padEnd(T+2))} ${X(E)}`);Z(),Q(` ${K(L("UPGRADE OPTIONS"))}`),Q(` ${v("\u2500".repeat(50))}`);let W=[["--force, -f","Force upgrade even if on latest version"],["--dry-run","Preview changes without applying them"]],P=Math.max(...W.map(([B])=>B.length));for(let[B,E]of W)Q(` ${k(B.padEnd(P+2))} ${X(E)}`);Z(),Q(` ${K(L("EXAMPLES"))}`),Q(` ${v("\u2500".repeat(50))}`);let I=[["onlyapi init my-api","Create project in ./my-api"],["onlyapi init .","Initialize in current directory"],["onlyapi upgrade","Upgrade to latest version"],["onlyapi upgrade --dry-run","Preview upgrade without changes"],["onlyapi upgrade --force","Force re-apply latest version"]];for(let[B,E]of I)Q(` ${X("$")} ${q(B)}`),Q(` ${X(E)}`);Z(),Q(` ${X("Docs:")} ${q("https://github.com/lysari/onlyapi#readme")}`),Q(` ${X("Issues:")} ${q("https://github.com/lysari/onlyapi/issues")}`),Z()};import{existsSync as l,mkdirSync as Nz,rmSync as Kz}from"fs";import{join as o,resolve as Rz}from"path";var qz="https://github.com/lysari/onlyapi.git",Vz="https://github.com/lysari/onlyapi/archive/refs/heads/main.tar.gz",wz=/^[a-zA-Z0-9_-]+$/,Dz=(z)=>{if(!z)return"Project name is required.";if(!wz.test(z))return"Project name can only contain letters, numbers, hyphens, and underscores.";if(z.length>214)return"Project name is too long (max 214 chars).";return null},u=async(z,$=process.cwd())=>{let H=Bun.spawn(z,{cwd:$,stdout:"pipe",stderr:"pipe"}),[J,T]=await Promise.all([new Response(H.stdout).text(),new Response(H.stderr).text()]),W=await H.exited;return{stdout:J.trim(),stderr:T.trim(),exitCode:W}},xz=async(z)=>{try{let{exitCode:$}=await u(["which",z]);return $===0}catch{return!1}},Yz=async(z,$)=>{let H=performance.now();Z(),Q(d($)),Z();let J=z[0]??"",T=z.includes("--cwd")||z.includes(".");if(!J&&!T)J=await $z("Project name","my-api");if(T)J=".";if(J!=="."){let Y=Dz(J);if(Y)M(Y),process.exit(1)}let W=J==="."?process.cwd():Rz(process.cwd(),J);if(J!=="."&&l(W)){if((await Array.fromAsync(new Bun.Glob("*").scan({cwd:W}))).length>0){if(!await c(`Directory ${K(L(J))} already exists and is not empty. Continue?`,!1))C("Aborted."),process.exit(0)}}if(b("Creating project"),J!==".")Nz(W,{recursive:!0}),D(`Created directory ${K(q(J))}`);let P=await xz("git"),I=y("Downloading template...");I.start();let B=!1;if(P){I.update("Cloning from GitHub...");let{exitCode:Y}=await u(["git","clone","--depth=1","--single-branch",qz,J==="."?".":J],J==="."?W:process.cwd());B=Y===0}if(!B){I.update("Downloading release archive...");try{let Y=await fetch(Vz);if(!Y.ok)throw Error(`HTTP ${Y.status}`);let G=o(W,"__onlyapi.tar.gz");await Bun.write(G,Y),I.update("Extracting..."),await u(["tar","xzf",G,"--strip-components=1"],W),Kz(G,{force:!0}),B=!0}catch(Y){I.stop(),M(`Failed to download template: ${Y instanceof Error?Y.message:String(Y)}`),M("Please check your network connection and try again."),Z(),C(`You can also clone manually: ${X(`git clone ${qz} ${J}`)}`),process.exit(1)}}I.stop("Template downloaded");let E=o(W,".git");if(l(E))Kz(E,{recursive:!0,force:!0});if(P)await u(["git","init"],W),D("Initialized fresh git repository");let f=o(W,"package.json");if(l(f))try{let Y=await Bun.file(f).text(),G=JSON.parse(Y);if(J!==".")G.name=J;G.version="0.1.0",G.description="",G.author="",G.repository=void 0,G.bugs=void 0,G.homepage=void 0,await Bun.write(f,`${JSON.stringify(G,null,2)}
13
+ `),D(`Updated ${K(q("package.json"))}`)}catch{h("Could not update package.json \u2014 you can edit it manually")}let O=o(W,".env.example"),S=o(W,".env");if(l(O)&&!l(S))try{let Y=await Bun.file(O).text(),G=Hz(64);Y=Y.replace("change-me-to-a-64-char-random-string",G),await Bun.write(S,Y),D(`Generated ${K(q(".env"))} with secure JWT_SECRET`)}catch{h("Could not generate .env \u2014 copy .env.example manually")}b("Installing dependencies");let p=y("Running bun install...");p.start();let{exitCode:j,stderr:t}=await u(["bun","install"],W);if(j!==0)p.stop(),M("Failed to install dependencies:"),Q(` ${X(t)}`),Z(),C(`Run ${K(q("bun install"))} manually in the project directory.`);else p.stop("Dependencies installed");if(P)await u(["git","add","-A"],W),await u(["git","commit","-m","Initial commit from onlyApi CLI","--no-verify"],W),D("Created initial commit");let r=performance.now()-H;Z(),Q(` ${_.rocket} ${K(R("Project created successfully!"))} ${X(`(${s(r)})`)}`),Z(),b("Next steps"),Z();let F=J!=="."?`cd ${J}`:null,U=[...F?[F]:[],"bun run dev # Start dev server (hot-reload)","bun test # Run tests","bun run check # Type-check"];for(let Y of U)Q(` ${X("$")} ${K(q(Y))}`);Z(),Q(` ${X("Docs:")} ${q("https://github.com/lysari/onlyapi#readme")}`),Q(` ${X("Issues:")} ${q("https://github.com/lysari/onlyapi/issues")}`),Z(),Q(` ${X("Happy hacking!")} ${_.bolt}`),Z()};import{existsSync as V}from"fs";import{join as x,resolve as Pz}from"path";var Bz="https://api.github.com/repos/lysari/onlyapi",fz=(z)=>`https://github.com/lysari/onlyapi/archive/refs/tags/${z}.tar.gz`,Fz="https://github.com/lysari/onlyapi/archive/refs/heads/main.tar.gz",Sz="https://registry.npmjs.org/only-api",jz=["src/core/errors/app-error.ts","src/core/types/brand.ts","src/core/types/result.ts","src/infrastructure/logging/logger.ts","src/infrastructure/security/password-hasher.ts","src/infrastructure/security/token-service.ts","src/presentation/middleware/cors.ts","src/presentation/middleware/rate-limit.ts","src/presentation/middleware/security-headers.ts","src/presentation/server.ts","src/presentation/context.ts","src/shared/cli.ts","src/shared/container.ts","src/shared/utils/id.ts","src/shared/utils/timing-safe.ts","src/shared/log-format.ts","src/cluster.ts","tsconfig.json","biome.json"],i=async(z,$=process.cwd())=>{let H=Bun.spawn(z,{cwd:$,stdout:"pipe",stderr:"pipe"}),[J,T]=await Promise.all([new Response(H.stdout).text(),new Response(H.stderr).text()]),W=await H.exited;return{stdout:J.trim(),stderr:T.trim(),exitCode:W}},Gz=(z)=>{let $=z.replace(/^v/,"").split(".").map(Number);return[$[0]??0,$[1]??0,$[2]??0]},Tz=(z,$)=>{let[H,J,T]=Gz(z),[W,P,I]=Gz($);if(H!==W)return H>W;if(J!==P)return J>P;return T>I},hz=async()=>{try{let z=await fetch(`${Bz}/releases/latest`,{headers:{Accept:"application/vnd.github.v3+json"}});if(z.ok)return(await z.json()).tag_name.replace(/^v/,"")}catch{}try{let z=await fetch(`${Bz}/tags?per_page=1`,{headers:{Accept:"application/vnd.github.v3+json"}});if(z.ok){let $=await z.json();if($.length>0){let H=$[0];return H?H.name.replace(/^v/,""):null}}}catch{}try{let z=await fetch(Sz);if(z.ok)return(await z.json())["dist-tags"].latest}catch{}return null},Uz=async(z,$)=>{let H=performance.now(),J=Pz(process.cwd());Z(),Q(d($)),Z();let T=x(J,"package.json");if(!V(T))M("No package.json found in current directory."),C("Run this command from the root of your onlyApi project."),process.exit(1);let W;try{W=JSON.parse(await Bun.file(T).text()).version??"0.0.0"}catch{M("Could not read package.json."),process.exit(1)}if(!(V(x(J,"src/main.ts"))&&V(x(J,"src/core"))&&V(x(J,"src/presentation"))))M("This doesn't appear to be an onlyApi project."),C("Expected to find src/main.ts, src/core/, and src/presentation/"),process.exit(1);b("Checking for updates");let I=y("Fetching latest version...");I.start();let B=await hz();if(!B){if(I.stop(),h("Could not determine the latest version."),C("This may be due to network issues or API rate limits."),Z(),!(z.includes("--force")||z.includes("-f"))){if(!await c("Continue with upgrade from main branch?",!1))C("Aborted."),process.exit(0)}}else{if(I.stop("Version check complete"),Z(),zz([["Current version",W],["Latest version",B]]),Z(),!Tz(B,W)&&!z.includes("--force")&&!z.includes("-f"))e("You're already on the latest version!"),Z(),process.exit(0);if(Tz(B,W))C(`Update available: ${K(k(W))} ${X("\u2192")} ${K(R(B))}`);else C(`Re-applying latest version ${X("(--force)")}`)}let E=V(x(J,".git"));if(E){let{stdout:F}=await i(["git","status","--porcelain"],J);if(F){if(Z(),h("You have uncommitted changes."),!await c("Continue anyway?",!1))C("Commit your changes first, then retry."),process.exit(0)}}b("Downloading update");let f=y("Downloading latest source...");f.start();let O=x(J,".onlyapi-upgrade-tmp");try{if(V(O)){let{rmSync:Ez}=await import("fs");Ez(O,{recursive:!0,force:!0})}let{mkdirSync:F}=await import("fs");F(O,{recursive:!0});let U=B?fz(`v${B}`):Fz,Y=await fetch(U),G=Y;if(!Y.ok&&B)f.update("Tag not found, trying main branch..."),G=await fetch(Fz);if(!G.ok)throw Error(`HTTP ${G.status}`);let A=x(O,"update.tar.gz");await Bun.write(A,G),f.update("Extracting..."),await i(["tar","xzf",A,"--strip-components=1"],O);let{rmSync:n}=await import("fs");n(A,{force:!0}),f.stop("Download complete")}catch(F){if(f.stop(),M(`Failed to download update: ${F instanceof Error?F.message:String(F)}`),V(O)){let{rmSync:U}=await import("fs");U(O,{recursive:!0,force:!0})}process.exit(1)}b("Applying updates");let S=0,p=0,j=z.includes("--dry-run");for(let F of jz){let U=x(O,F),Y=x(J,F);if(!V(U))continue;try{let G=await Bun.file(U).text();if(V(Y)){if(await Bun.file(Y).text()===G){p++;continue}}if(!j){let A=Y.substring(0,Y.lastIndexOf("/")),{mkdirSync:n}=await import("fs");n(A,{recursive:!0}),await Bun.write(Y,G)}D(`${j?`${X("[dry-run]")} `:""}Updated ${K(q(F))}`),S++}catch{h(`Could not update ${F}`)}}let t=x(O,"package.json");if(V(t))try{let F=JSON.parse(await Bun.file(t).text()),U=JSON.parse(await Bun.file(T).text()),Y=!1;if(F.dependencies){U.dependencies=U.dependencies??{};for(let[G,A]of Object.entries(F.dependencies))if(U.dependencies[G]!==A)U.dependencies[G]=A,Y=!0}if(F.devDependencies){U.devDependencies=U.devDependencies??{};for(let[G,A]of Object.entries(F.devDependencies))if(U.devDependencies[G]!==A)U.devDependencies[G]=A,Y=!0}if(B)U.version=B;if(!j)await Bun.write(T,`${JSON.stringify(U,null,2)}
14
+ `);if(Y)D(`${j?`${X("[dry-run]")} `:""}Updated dependencies in ${K(q("package.json"))}`)}catch{h("Could not merge package.json dependencies")}if(V(O)){let{rmSync:F}=await import("fs");F(O,{recursive:!0,force:!0})}if(!j&&S>0){b("Installing dependencies");let F=y("Running bun install...");F.start();let{exitCode:U}=await i(["bun","install"],J);if(U!==0)F.stop(),h("bun install failed \u2014 run it manually.");else F.stop("Dependencies installed")}if(E&&!j&&S>0){if(await c("Create a git commit for this upgrade?")){let U=B?`chore: upgrade onlyApi to v${B}`:"chore: upgrade onlyApi to latest";await i(["git","add","-A"],J),await i(["git","commit","-m",U,"--no-verify"],J),D("Created upgrade commit")}}let r=performance.now()-H;if(Z(),S>0)Q(` ${_.rocket} ${K(R("Upgrade complete!"))} ${X(`(${s(r)})`)}`),Z(),zz([["Files updated",String(S)],["Files unchanged",String(p)]]);else if(j)Q(` ${_.info} ${K(q("Dry run complete"))} \u2014 no files were modified.`);else e("All files are already up to date!");if(Z(),S>0)Q(` ${X("Note: The following files are NOT auto-upgraded (your custom code):")}`),Q(` ${X(" - src/application/ (your services & DTOs)")}`),Q(` ${X(" - src/core/entities/ (your domain entities)")}`),Q(` ${X(" - src/core/ports/ (your port interfaces)")}`),Q(` ${X(" - src/presentation/handlers/ (your route handlers)")}`),Q(` ${X(" - src/presentation/routes/ (your routes)")}`),Q(` ${X(" - src/main.ts (your bootstrap)")}`),Z(),Q(` ${X("Review the")} ${q("CHANGELOG.md")} ${X("for breaking changes.")}`),Z()};var a="1.5.1",_z=process.argv.slice(2),Iz=_z[0]?.toLowerCase()??"",Oz=_z.slice(1),bz=async()=>{try{switch(Iz){case"init":case"create":case"new":await Yz(Oz,a);break;case"upgrade":case"update":await Uz(Oz,a);break;case"version":case"-v":case"--version":Q(`onlyapi v${a}`);break;case"help":case"-h":case"--help":Jz(a);break;case"":Jz(a);break;default:Z(),M(`Unknown command: ${K(L(Iz))}`),Z(),Q(` ${X("Run")} ${q("onlyapi help")} ${X("to see available commands.")}`),Z(),process.exit(1)}}catch(z){Z(),M(z instanceof Error?z.message:String(z)),Z(),process.exit(1)}};bz();