@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.
- package/CHANGELOG.md +201 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/cli.js +14 -0
- package/package.json +69 -0
- package/src/application/dtos/admin.dto.ts +25 -0
- package/src/application/dtos/auth.dto.ts +97 -0
- package/src/application/dtos/index.ts +40 -0
- package/src/application/index.ts +2 -0
- package/src/application/services/admin.service.ts +150 -0
- package/src/application/services/api-key.service.ts +65 -0
- package/src/application/services/auth.service.ts +606 -0
- package/src/application/services/health.service.ts +97 -0
- package/src/application/services/index.ts +10 -0
- package/src/application/services/user.service.ts +95 -0
- package/src/cli/commands/help.ts +86 -0
- package/src/cli/commands/init.ts +301 -0
- package/src/cli/commands/upgrade.ts +471 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/ui.ts +189 -0
- package/src/cluster.ts +62 -0
- package/src/core/entities/index.ts +1 -0
- package/src/core/entities/user.entity.ts +24 -0
- package/src/core/errors/app-error.ts +81 -0
- package/src/core/errors/index.ts +15 -0
- package/src/core/index.ts +7 -0
- package/src/core/ports/account-lockout.ts +15 -0
- package/src/core/ports/alert-sink.ts +27 -0
- package/src/core/ports/api-key.ts +37 -0
- package/src/core/ports/audit-log.ts +46 -0
- package/src/core/ports/cache.ts +24 -0
- package/src/core/ports/circuit-breaker.ts +42 -0
- package/src/core/ports/event-bus.ts +78 -0
- package/src/core/ports/index.ts +62 -0
- package/src/core/ports/job-queue.ts +73 -0
- package/src/core/ports/logger.ts +21 -0
- package/src/core/ports/metrics.ts +49 -0
- package/src/core/ports/oauth.ts +55 -0
- package/src/core/ports/password-hasher.ts +10 -0
- package/src/core/ports/password-history.ts +23 -0
- package/src/core/ports/password-policy.ts +43 -0
- package/src/core/ports/refresh-token-store.ts +37 -0
- package/src/core/ports/retry.ts +23 -0
- package/src/core/ports/token-blacklist.ts +16 -0
- package/src/core/ports/token-service.ts +23 -0
- package/src/core/ports/totp-service.ts +16 -0
- package/src/core/ports/user.repository.ts +40 -0
- package/src/core/ports/verification-token.ts +41 -0
- package/src/core/ports/webhook.ts +58 -0
- package/src/core/types/brand.ts +19 -0
- package/src/core/types/index.ts +19 -0
- package/src/core/types/pagination.ts +28 -0
- package/src/core/types/result.ts +52 -0
- package/src/infrastructure/alerting/index.ts +1 -0
- package/src/infrastructure/alerting/webhook.ts +100 -0
- package/src/infrastructure/cache/in-memory-cache.ts +111 -0
- package/src/infrastructure/cache/index.ts +6 -0
- package/src/infrastructure/cache/redis-cache.ts +204 -0
- package/src/infrastructure/config/config.ts +185 -0
- package/src/infrastructure/config/index.ts +1 -0
- package/src/infrastructure/database/in-memory-user.repository.ts +134 -0
- package/src/infrastructure/database/index.ts +37 -0
- package/src/infrastructure/database/migrations/001_create_users.ts +26 -0
- package/src/infrastructure/database/migrations/002_create_token_blacklist.ts +21 -0
- package/src/infrastructure/database/migrations/003_create_audit_log.ts +31 -0
- package/src/infrastructure/database/migrations/004_auth_platform.ts +112 -0
- package/src/infrastructure/database/migrations/runner.ts +120 -0
- package/src/infrastructure/database/mssql/index.ts +14 -0
- package/src/infrastructure/database/mssql/migrations.ts +299 -0
- package/src/infrastructure/database/mssql/mssql-account-lockout.ts +95 -0
- package/src/infrastructure/database/mssql/mssql-api-keys.ts +146 -0
- package/src/infrastructure/database/mssql/mssql-audit-log.ts +86 -0
- package/src/infrastructure/database/mssql/mssql-oauth-accounts.ts +118 -0
- package/src/infrastructure/database/mssql/mssql-password-history.ts +71 -0
- package/src/infrastructure/database/mssql/mssql-refresh-token-store.ts +144 -0
- package/src/infrastructure/database/mssql/mssql-token-blacklist.ts +54 -0
- package/src/infrastructure/database/mssql/mssql-user.repository.ts +263 -0
- package/src/infrastructure/database/mssql/mssql-verification-tokens.ts +120 -0
- package/src/infrastructure/database/postgres/index.ts +14 -0
- package/src/infrastructure/database/postgres/migrations.ts +235 -0
- package/src/infrastructure/database/postgres/pg-account-lockout.ts +75 -0
- package/src/infrastructure/database/postgres/pg-api-keys.ts +126 -0
- package/src/infrastructure/database/postgres/pg-audit-log.ts +74 -0
- package/src/infrastructure/database/postgres/pg-oauth-accounts.ts +101 -0
- package/src/infrastructure/database/postgres/pg-password-history.ts +61 -0
- package/src/infrastructure/database/postgres/pg-refresh-token-store.ts +117 -0
- package/src/infrastructure/database/postgres/pg-token-blacklist.ts +48 -0
- package/src/infrastructure/database/postgres/pg-user.repository.ts +237 -0
- package/src/infrastructure/database/postgres/pg-verification-tokens.ts +97 -0
- package/src/infrastructure/database/sqlite-account-lockout.ts +97 -0
- package/src/infrastructure/database/sqlite-api-keys.ts +155 -0
- package/src/infrastructure/database/sqlite-audit-log.ts +90 -0
- package/src/infrastructure/database/sqlite-oauth-accounts.ts +105 -0
- package/src/infrastructure/database/sqlite-password-history.ts +54 -0
- package/src/infrastructure/database/sqlite-refresh-token-store.ts +122 -0
- package/src/infrastructure/database/sqlite-token-blacklist.ts +47 -0
- package/src/infrastructure/database/sqlite-user.repository.ts +260 -0
- package/src/infrastructure/database/sqlite-verification-tokens.ts +112 -0
- package/src/infrastructure/events/event-bus.ts +105 -0
- package/src/infrastructure/events/event-factory.ts +31 -0
- package/src/infrastructure/events/in-memory-webhook-registry.ts +67 -0
- package/src/infrastructure/events/index.ts +4 -0
- package/src/infrastructure/events/webhook-dispatcher.ts +114 -0
- package/src/infrastructure/index.ts +58 -0
- package/src/infrastructure/jobs/index.ts +1 -0
- package/src/infrastructure/jobs/job-queue.ts +185 -0
- package/src/infrastructure/logging/index.ts +1 -0
- package/src/infrastructure/logging/logger.ts +63 -0
- package/src/infrastructure/metrics/index.ts +1 -0
- package/src/infrastructure/metrics/prometheus.ts +231 -0
- package/src/infrastructure/oauth/github.ts +116 -0
- package/src/infrastructure/oauth/google.ts +83 -0
- package/src/infrastructure/oauth/index.ts +2 -0
- package/src/infrastructure/resilience/circuit-breaker.ts +133 -0
- package/src/infrastructure/resilience/index.ts +2 -0
- package/src/infrastructure/resilience/retry.ts +50 -0
- package/src/infrastructure/security/account-lockout.ts +73 -0
- package/src/infrastructure/security/index.ts +6 -0
- package/src/infrastructure/security/password-hasher.ts +31 -0
- package/src/infrastructure/security/password-policy.ts +77 -0
- package/src/infrastructure/security/token-blacklist.ts +45 -0
- package/src/infrastructure/security/token-service.ts +144 -0
- package/src/infrastructure/security/totp-service.ts +142 -0
- package/src/infrastructure/tracing/index.ts +7 -0
- package/src/infrastructure/tracing/trace-context.ts +93 -0
- package/src/main.ts +479 -0
- package/src/presentation/context.ts +26 -0
- package/src/presentation/handlers/admin.handler.ts +114 -0
- package/src/presentation/handlers/api-key.handler.ts +68 -0
- package/src/presentation/handlers/auth.handler.ts +218 -0
- package/src/presentation/handlers/health.handler.ts +27 -0
- package/src/presentation/handlers/index.ts +15 -0
- package/src/presentation/handlers/metrics.handler.ts +21 -0
- package/src/presentation/handlers/oauth.handler.ts +61 -0
- package/src/presentation/handlers/openapi.handler.ts +543 -0
- package/src/presentation/handlers/response.ts +29 -0
- package/src/presentation/handlers/sse.handler.ts +165 -0
- package/src/presentation/handlers/user.handler.ts +81 -0
- package/src/presentation/handlers/webhook.handler.ts +92 -0
- package/src/presentation/handlers/websocket.handler.ts +226 -0
- package/src/presentation/i18n/index.ts +254 -0
- package/src/presentation/index.ts +5 -0
- package/src/presentation/middleware/api-key.ts +18 -0
- package/src/presentation/middleware/auth.ts +39 -0
- package/src/presentation/middleware/cors.ts +41 -0
- package/src/presentation/middleware/index.ts +12 -0
- package/src/presentation/middleware/rate-limit.ts +65 -0
- package/src/presentation/middleware/security-headers.ts +18 -0
- package/src/presentation/middleware/validate.ts +16 -0
- package/src/presentation/middleware/versioning.ts +69 -0
- package/src/presentation/routes/index.ts +1 -0
- package/src/presentation/routes/router.ts +272 -0
- package/src/presentation/server.ts +381 -0
- package/src/shared/cli.ts +294 -0
- package/src/shared/container.ts +65 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/log-format.ts +148 -0
- package/src/shared/utils/id.ts +5 -0
- package/src/shared/utils/index.ts +2 -0
- 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
|
+
[](https://github.com/lysari/onlyapi/actions/workflows/ci.yml)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://bun.sh)
|
|
12
|
+
[](tsconfig.json)
|
|
13
|
+
[]()
|
|
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();
|