@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/src/main.ts ADDED
@@ -0,0 +1,479 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import sql from "mssql";
5
+ import { createAdminService } from "./application/services/admin.service.js";
6
+ import { createApiKeyService } from "./application/services/api-key.service.js";
7
+ import { createAuthService } from "./application/services/auth.service.js";
8
+ import { createHealthService } from "./application/services/health.service.js";
9
+ import { createUserService } from "./application/services/user.service.js";
10
+ import { AlertLevel } from "./core/ports/alert-sink.js";
11
+ import type { Cache } from "./core/ports/cache.js";
12
+ import { CircuitState } from "./core/ports/circuit-breaker.js";
13
+ import type { OAuthProvider } from "./core/ports/oauth.js";
14
+ import { createNoopAlertSink, createWebhookAlertSink } from "./infrastructure/alerting/webhook.js";
15
+ import { createInMemoryCache } from "./infrastructure/cache/in-memory-cache.js";
16
+ import { createRedisCache } from "./infrastructure/cache/redis-cache.js";
17
+ import { loadConfig } from "./infrastructure/config/config.js";
18
+ import { migrateUp } from "./infrastructure/database/migrations/runner.js";
19
+ import {
20
+ createMssqlAccountLockout,
21
+ createMssqlApiKeyRepository,
22
+ createMssqlAuditLog,
23
+ createMssqlOAuthAccountRepo,
24
+ createMssqlPasswordHistory,
25
+ createMssqlRefreshTokenStore,
26
+ createMssqlTokenBlacklist,
27
+ createMssqlUserRepository,
28
+ createMssqlVerificationTokenRepo,
29
+ mssqlMigrateUp,
30
+ } from "./infrastructure/database/mssql/index.js";
31
+ import {
32
+ createPgAccountLockout,
33
+ createPgApiKeyRepository,
34
+ createPgAuditLog,
35
+ createPgOAuthAccountRepo,
36
+ createPgPasswordHistory,
37
+ createPgRefreshTokenStore,
38
+ createPgTokenBlacklist,
39
+ createPgUserRepository,
40
+ createPgVerificationTokenRepo,
41
+ pgMigrateUp,
42
+ } from "./infrastructure/database/postgres/index.js";
43
+ import { createSqliteAccountLockout } from "./infrastructure/database/sqlite-account-lockout.js";
44
+ import { createSqliteApiKeyRepository } from "./infrastructure/database/sqlite-api-keys.js";
45
+ import { createSqliteAuditLog } from "./infrastructure/database/sqlite-audit-log.js";
46
+ import { createSqliteOAuthAccountRepo } from "./infrastructure/database/sqlite-oauth-accounts.js";
47
+ import { createSqlitePasswordHistory } from "./infrastructure/database/sqlite-password-history.js";
48
+ import { createSqliteRefreshTokenStore } from "./infrastructure/database/sqlite-refresh-token-store.js";
49
+ import { createSqliteTokenBlacklist } from "./infrastructure/database/sqlite-token-blacklist.js";
50
+ import { createSqliteUserRepository } from "./infrastructure/database/sqlite-user.repository.js";
51
+ import { createSqliteVerificationTokenRepo } from "./infrastructure/database/sqlite-verification-tokens.js";
52
+ import { createEventBus } from "./infrastructure/events/event-bus.js";
53
+ import { createDomainEventFactory } from "./infrastructure/events/event-factory.js";
54
+ import { createInMemoryWebhookRegistry } from "./infrastructure/events/in-memory-webhook-registry.js";
55
+ import { createWebhookDispatcher } from "./infrastructure/events/webhook-dispatcher.js";
56
+ import { createInMemoryJobQueue } from "./infrastructure/jobs/job-queue.js";
57
+ import { createLogger } from "./infrastructure/logging/logger.js";
58
+ import { createMetricsCollector } from "./infrastructure/metrics/prometheus.js";
59
+ import { createGitHubOAuthProvider } from "./infrastructure/oauth/github.js";
60
+ import { createGoogleOAuthProvider } from "./infrastructure/oauth/google.js";
61
+ import { createCircuitBreaker } from "./infrastructure/resilience/circuit-breaker.js";
62
+ import { createPasswordHasher } from "./infrastructure/security/password-hasher.js";
63
+ import { createPasswordPolicy } from "./infrastructure/security/password-policy.js";
64
+ import { createTokenService } from "./infrastructure/security/token-service.js";
65
+ import { createTotpService } from "./infrastructure/security/totp-service.js";
66
+ import { createWebSocketManager } from "./presentation/handlers/websocket.handler.js";
67
+ import { createRouter } from "./presentation/routes/router.js";
68
+ import { createServer } from "./presentation/server.js";
69
+ import { printShutdown, printStartupBanner } from "./shared/cli.js";
70
+ import { Container, Tokens } from "./shared/container.js";
71
+
72
+ /**
73
+ * Bootstrap — compose the entire dependency graph, then start the server.
74
+ * Single entry point, fail-fast on misconfiguration.
75
+ */
76
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: bootstrap wires all drivers/services — inherently branchy
77
+ const bootstrap = async () => {
78
+ const bootStart = performance.now();
79
+
80
+ // 1. Config (validated, fails fast)
81
+ const config = loadConfig();
82
+
83
+ // 2. Infrastructure
84
+ const logger = createLogger(config.log.level, {}, config.log.format);
85
+ const passwordHasher = createPasswordHasher();
86
+ const tokenService = createTokenService(config.jwt);
87
+
88
+ // 3. Database — driver-based adapter selection
89
+ let db: Database | undefined;
90
+ // biome-ignore lint/suspicious/noExplicitAny: Bun.sql tagged template type
91
+ let pgSql: any | undefined;
92
+ let mssqlPool: sql.ConnectionPool | undefined;
93
+ let userRepo: ReturnType<typeof createSqliteUserRepository>;
94
+ let tokenBlacklist: ReturnType<typeof createSqliteTokenBlacklist>;
95
+ let accountLockout: ReturnType<typeof createSqliteAccountLockout>;
96
+ let auditLog: ReturnType<typeof createSqliteAuditLog>;
97
+ let verificationTokens: ReturnType<typeof createSqliteVerificationTokenRepo>;
98
+ let refreshTokenStore: ReturnType<typeof createSqliteRefreshTokenStore>;
99
+ let apiKeyRepo: ReturnType<typeof createSqliteApiKeyRepository>;
100
+ let passwordHistory: ReturnType<typeof createSqlitePasswordHistory>;
101
+ let oauthAccounts: ReturnType<typeof createSqliteOAuthAccountRepo>;
102
+
103
+ if (config.database.driver === "postgres") {
104
+ // Postgres via Bun.sql (zero-dep, native driver)
105
+ const dbUrl = config.database.url;
106
+ if (!dbUrl) {
107
+ logger.fatal("DATABASE_URL is required when DATABASE_DRIVER=postgres");
108
+ process.exit(1);
109
+ }
110
+ // biome-ignore lint/suspicious/noExplicitAny: Bun.sql dynamic URL config
111
+ pgSql = new (Bun as any).SQL({ url: dbUrl });
112
+ logger.info("Postgres database connecting", { url: dbUrl.replace(/:[^:@]+@/, ":***@") });
113
+
114
+ await pgMigrateUp(pgSql, logger);
115
+
116
+ userRepo = createPgUserRepository(pgSql);
117
+ tokenBlacklist = createPgTokenBlacklist(pgSql);
118
+ accountLockout = createPgAccountLockout(pgSql, {
119
+ maxAttempts: config.lockout.maxAttempts,
120
+ lockoutDurationMs: config.lockout.durationMs,
121
+ });
122
+ auditLog = createPgAuditLog(pgSql);
123
+ verificationTokens = createPgVerificationTokenRepo(pgSql);
124
+ refreshTokenStore = createPgRefreshTokenStore(pgSql);
125
+ apiKeyRepo = createPgApiKeyRepository(pgSql);
126
+ passwordHistory = createPgPasswordHistory(pgSql);
127
+ oauthAccounts = createPgOAuthAccountRepo(pgSql);
128
+ logger.info("Postgres adapters initialized");
129
+ } else if (config.database.driver === "mssql") {
130
+ // SQL Server via mssql npm package (tedious TDS protocol)
131
+ const dbUrl = config.database.url;
132
+ if (!dbUrl) {
133
+ logger.fatal("DATABASE_URL is required when DATABASE_DRIVER=mssql");
134
+ process.exit(1);
135
+ }
136
+ mssqlPool = await sql.connect(dbUrl);
137
+ logger.info("SQL Server database connected", {
138
+ url: dbUrl.replace(/:[^:@]+@/, ":***@"),
139
+ });
140
+
141
+ await mssqlMigrateUp(mssqlPool, logger);
142
+
143
+ userRepo = createMssqlUserRepository(mssqlPool);
144
+ tokenBlacklist = createMssqlTokenBlacklist(mssqlPool);
145
+ accountLockout = createMssqlAccountLockout(mssqlPool, {
146
+ maxAttempts: config.lockout.maxAttempts,
147
+ lockoutDurationMs: config.lockout.durationMs,
148
+ });
149
+ auditLog = createMssqlAuditLog(mssqlPool);
150
+ verificationTokens = createMssqlVerificationTokenRepo(mssqlPool);
151
+ refreshTokenStore = createMssqlRefreshTokenStore(mssqlPool);
152
+ apiKeyRepo = createMssqlApiKeyRepository(mssqlPool);
153
+ passwordHistory = createMssqlPasswordHistory(mssqlPool);
154
+ oauthAccounts = createMssqlOAuthAccountRepo(mssqlPool);
155
+ logger.info("SQL Server adapters initialized");
156
+ } else {
157
+ // SQLite (default — zero deps, bun:sqlite built-in)
158
+ const dbPath = config.database.path;
159
+ const dbDir = dirname(dbPath);
160
+ if (!existsSync(dbDir)) {
161
+ mkdirSync(dbDir, { recursive: true });
162
+ }
163
+ db = new Database(dbPath, { create: true });
164
+ db.exec("PRAGMA journal_mode = WAL");
165
+ db.exec("PRAGMA synchronous = NORMAL");
166
+ db.exec("PRAGMA foreign_keys = ON");
167
+ logger.info("SQLite database opened", { path: dbPath });
168
+
169
+ await migrateUp(db, logger);
170
+
171
+ userRepo = createSqliteUserRepository(db);
172
+ tokenBlacklist = createSqliteTokenBlacklist(db);
173
+ accountLockout = createSqliteAccountLockout(db, {
174
+ maxAttempts: config.lockout.maxAttempts,
175
+ lockoutDurationMs: config.lockout.durationMs,
176
+ });
177
+ auditLog = createSqliteAuditLog(db);
178
+ verificationTokens = createSqliteVerificationTokenRepo(db);
179
+ refreshTokenStore = createSqliteRefreshTokenStore(db);
180
+ apiKeyRepo = createSqliteApiKeyRepository(db);
181
+ passwordHistory = createSqlitePasswordHistory(db);
182
+ oauthAccounts = createSqliteOAuthAccountRepo(db);
183
+ }
184
+
185
+ // 4. Cache — driver-based adapter selection
186
+ let cache: Cache;
187
+
188
+ if (config.redis.enabled) {
189
+ cache = await createRedisCache({
190
+ config: {
191
+ host: config.redis.host,
192
+ port: config.redis.port,
193
+ password: config.redis.password,
194
+ db: config.redis.db,
195
+ },
196
+ logger: logger.child({ service: "redis" }),
197
+ });
198
+ logger.info("Redis cache connected", { host: config.redis.host, port: config.redis.port });
199
+ } else {
200
+ cache = createInMemoryCache();
201
+ logger.info("In-memory cache initialized");
202
+ }
203
+
204
+ // 5. Security services
205
+ const totpService = createTotpService();
206
+ const passwordPolicy = createPasswordPolicy(config.passwordPolicy);
207
+
208
+ // 5a. OAuth providers (only registered when configured)
209
+ const oauthProviders = new Map<string, OAuthProvider>();
210
+ if (config.oauth.googleClientId && config.oauth.googleClientSecret) {
211
+ oauthProviders.set(
212
+ "google",
213
+ createGoogleOAuthProvider({
214
+ clientId: config.oauth.googleClientId,
215
+ clientSecret: config.oauth.googleClientSecret,
216
+ }),
217
+ );
218
+ logger.info("OAuth provider registered: google");
219
+ }
220
+ if (config.oauth.githubClientId && config.oauth.githubClientSecret) {
221
+ oauthProviders.set(
222
+ "github",
223
+ createGitHubOAuthProvider({
224
+ clientId: config.oauth.githubClientId,
225
+ clientSecret: config.oauth.githubClientSecret,
226
+ }),
227
+ );
228
+ logger.info("OAuth provider registered: github");
229
+ }
230
+
231
+ // 5b. Event system, webhooks, job queue
232
+ const eventBus = createEventBus({ logger: logger.child({ service: "event-bus" }) });
233
+ const eventFactory = createDomainEventFactory();
234
+ const webhookRegistry = createInMemoryWebhookRegistry();
235
+ const webhookDispatcher = createWebhookDispatcher({
236
+ registry: webhookRegistry,
237
+ logger: logger.child({ service: "webhook" }),
238
+ });
239
+ const jobQueue = createInMemoryJobQueue({
240
+ logger: logger.child({ service: "job-queue" }),
241
+ });
242
+
243
+ // Wire webhook dispatcher as a wildcard event subscriber
244
+ eventBus.subscribeAll((event) => webhookDispatcher.dispatch(event));
245
+
246
+ // 6. Register in DI container
247
+ Container.register(Tokens.Logger, logger);
248
+ Container.register(Tokens.Config, config);
249
+ Container.register(Tokens.Database, db);
250
+ Container.register(Tokens.PasswordHasher, passwordHasher);
251
+ Container.register(Tokens.TokenService, tokenService);
252
+ Container.register(Tokens.TokenBlacklist, tokenBlacklist);
253
+ Container.register(Tokens.AccountLockout, accountLockout);
254
+ Container.register(Tokens.UserRepository, userRepo);
255
+ Container.register(Tokens.AuditLog, auditLog);
256
+ Container.register(Tokens.VerificationTokenRepository, verificationTokens);
257
+ Container.register(Tokens.RefreshTokenStore, refreshTokenStore);
258
+ Container.register(Tokens.ApiKeyRepository, apiKeyRepo);
259
+ Container.register(Tokens.PasswordHistory, passwordHistory);
260
+ Container.register(Tokens.PasswordPolicy, passwordPolicy);
261
+ Container.register(Tokens.TotpService, totpService);
262
+ Container.register(Tokens.OAuthProviders, oauthProviders);
263
+ Container.register(Tokens.OAuthAccountRepository, oauthAccounts);
264
+
265
+ // 6b. Cache
266
+ Container.register(Tokens.Cache, cache);
267
+
268
+ // 6c. Event, webhook, and job queue registrations
269
+ Container.register(Tokens.EventBus, eventBus);
270
+ Container.register(Tokens.EventFactory, eventFactory);
271
+ Container.register(Tokens.WebhookRegistry, webhookRegistry);
272
+ Container.register(Tokens.WebhookDispatcher, webhookDispatcher);
273
+ Container.register(Tokens.JobQueue, jobQueue);
274
+
275
+ // 6d. Observability — metrics collector
276
+ const metricsCollector = createMetricsCollector();
277
+ Container.register(Tokens.MetricsCollector, metricsCollector);
278
+
279
+ // 6e. Alerting — webhook alert sink (or noop if no URL configured)
280
+ const alertSink = config.alerting.webhookUrl
281
+ ? createWebhookAlertSink({
282
+ url: config.alerting.webhookUrl,
283
+ timeoutMs: config.alerting.timeoutMs,
284
+ logger: logger.child({ service: "alerting" }),
285
+ })
286
+ : createNoopAlertSink();
287
+ Container.register(Tokens.AlertSink, alertSink);
288
+
289
+ // 6f. Resilience — circuit breaker for database operations
290
+ const dbCircuitBreaker = createCircuitBreaker({
291
+ name: "database",
292
+ failureThreshold: config.circuitBreaker.failureThreshold,
293
+ resetTimeoutMs: config.circuitBreaker.resetTimeoutMs,
294
+ halfOpenSuccessThreshold: config.circuitBreaker.halfOpenSuccessThreshold,
295
+ onStateChange: (cbName, from, to) => {
296
+ logger.warn("Circuit breaker state changed", {
297
+ circuitBreaker: cbName,
298
+ from,
299
+ to,
300
+ });
301
+
302
+ // Update metrics gauge: 0=closed, 1=half_open, 2=open
303
+ const stateValue = to === CircuitState.CLOSED ? 0 : to === CircuitState.HALF_OPEN ? 1 : 2;
304
+ metricsCollector.circuitBreakerState.set(stateValue, { name: cbName });
305
+
306
+ // Fire alerting hooks on state changes
307
+ if (to === CircuitState.OPEN) {
308
+ alertSink.send({
309
+ level: AlertLevel.CRITICAL,
310
+ title: `Circuit breaker OPEN: ${cbName}`,
311
+ message: `Circuit breaker "${cbName}" has opened after reaching failure threshold. Requests will be short-circuited.`,
312
+ timestamp: new Date().toISOString(),
313
+ source: "onlyApi",
314
+ metadata: { circuitBreaker: cbName, from, to },
315
+ });
316
+ metricsCollector.alertsSentTotal.inc({ level: "critical" });
317
+ } else if (to === CircuitState.CLOSED && from === CircuitState.HALF_OPEN) {
318
+ alertSink.send({
319
+ level: AlertLevel.RESOLVED,
320
+ title: `Circuit breaker CLOSED: ${cbName}`,
321
+ message: `Circuit breaker "${cbName}" has recovered and returned to normal operation.`,
322
+ timestamp: new Date().toISOString(),
323
+ source: "onlyApi",
324
+ metadata: { circuitBreaker: cbName, from, to },
325
+ });
326
+ metricsCollector.alertsSentTotal.inc({ level: "resolved" });
327
+ }
328
+ },
329
+ });
330
+
331
+ // 7. Application services
332
+ const authService = createAuthService({
333
+ userRepo,
334
+ passwordHasher,
335
+ tokenService,
336
+ tokenBlacklist,
337
+ accountLockout,
338
+ verificationTokens,
339
+ refreshTokenStore,
340
+ passwordHistory,
341
+ passwordPolicy,
342
+ totpService,
343
+ oauthProviders,
344
+ oauthAccounts,
345
+ logger: logger.child({ service: "auth" }),
346
+ });
347
+
348
+ const userService = createUserService({
349
+ userRepo,
350
+ passwordHasher,
351
+ logger: logger.child({ service: "user" }),
352
+ });
353
+
354
+ const healthService = createHealthService({
355
+ logger: logger.child({ service: "health" }),
356
+ version: "2.0.0",
357
+ circuitBreakers: [dbCircuitBreaker],
358
+ });
359
+
360
+ const adminService = createAdminService({
361
+ userRepo,
362
+ auditLog,
363
+ logger: logger.child({ service: "admin" }),
364
+ });
365
+
366
+ const apiKeyService = createApiKeyService({
367
+ apiKeyRepo,
368
+ logger: logger.child({ service: "api-key" }),
369
+ });
370
+
371
+ Container.register(Tokens.AuthService, authService);
372
+ Container.register(Tokens.UserService, userService);
373
+ Container.register(Tokens.HealthService, healthService);
374
+ Container.register(Tokens.AdminService, adminService);
375
+ Container.register(Tokens.ApiKeyService, apiKeyService);
376
+
377
+ // 8. Presentation
378
+ const router = createRouter({
379
+ authService,
380
+ userService,
381
+ healthService,
382
+ adminService,
383
+ apiKeyService,
384
+ tokenService,
385
+ metricsCollector,
386
+ oauthProviders,
387
+ eventBus,
388
+ webhookRegistry,
389
+ logger,
390
+ });
391
+
392
+ // 8b. WebSocket manager
393
+ const wsManager = createWebSocketManager({
394
+ tokenService,
395
+ eventBus,
396
+ logger: logger.child({ service: "websocket" }),
397
+ });
398
+ Container.register(Tokens.WebSocketManager, wsManager);
399
+
400
+ // Wire WebSocket manager as event subscriber (broadcast to WS clients)
401
+ eventBus.subscribeAll((event) => wsManager.broadcast(event));
402
+
403
+ const srv = createServer({ config, logger, router, metrics: metricsCollector, wsManager });
404
+
405
+ // 9. Start
406
+ const instance = srv.start();
407
+ jobQueue.start();
408
+
409
+ // 10. Print startup banner
410
+ const isCluster = Bun.env["WORKER_ID"] !== undefined;
411
+ if (!isCluster) {
412
+ printStartupBanner({
413
+ config,
414
+ bootTimeMs: performance.now() - bootStart,
415
+ });
416
+ }
417
+
418
+ // 11. Periodic token pruning (every 10 minutes)
419
+ const pruneInterval = setInterval(
420
+ async () => {
421
+ const blacklistResult = await tokenBlacklist.prune();
422
+ if (blacklistResult.ok && blacklistResult.value > 0) {
423
+ logger.debug("Pruned expired blacklisted tokens", { count: blacklistResult.value });
424
+ }
425
+
426
+ const verifyResult = await verificationTokens.prune();
427
+ if (verifyResult.ok && verifyResult.value > 0) {
428
+ logger.debug("Pruned expired verification tokens", { count: verifyResult.value });
429
+ }
430
+
431
+ const refreshResult = await refreshTokenStore.prune(30 * 24 * 60 * 60 * 1000);
432
+ if (refreshResult.ok && refreshResult.value > 0) {
433
+ logger.debug("Pruned revoked refresh token families", { count: refreshResult.value });
434
+ }
435
+ },
436
+ 10 * 60 * 1000,
437
+ );
438
+
439
+ // 12. Graceful shutdown
440
+ const shutdown = (signal: string) => {
441
+ clearInterval(pruneInterval);
442
+ jobQueue.stop();
443
+ srv.flush(); // flush buffered access logs before exit
444
+ if (db) db.close(); // close SQLite connection
445
+ if (mssqlPool) mssqlPool.close(); // close SQL Server connection pool
446
+ cache.close(); // close cache (no-op for in-memory, QUIT for Redis)
447
+ printShutdown(signal);
448
+ instance.stop();
449
+ process.exit(0);
450
+ };
451
+
452
+ process.on("SIGINT", () => shutdown("SIGINT"));
453
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
454
+
455
+ // 13. Unhandled rejection safety net
456
+ process.on("unhandledRejection", (reason) => {
457
+ logger.fatal("Unhandled promise rejection", {
458
+ error: reason instanceof Error ? reason.message : String(reason),
459
+ stack: reason instanceof Error ? reason.stack : undefined,
460
+ });
461
+
462
+ // Fire alert on fatal errors
463
+ alertSink.send({
464
+ level: AlertLevel.CRITICAL,
465
+ title: "Unhandled promise rejection",
466
+ message: reason instanceof Error ? reason.message : String(reason),
467
+ timestamp: new Date().toISOString(),
468
+ source: "onlyApi",
469
+ metadata: {
470
+ stack: reason instanceof Error ? reason.stack : undefined,
471
+ },
472
+ });
473
+ metricsCollector.alertsSentTotal.inc({ level: "critical" });
474
+ });
475
+
476
+ return instance;
477
+ };
478
+
479
+ bootstrap();
@@ -0,0 +1,26 @@
1
+ import type { Logger } from "../core/ports/logger.js";
2
+ import type { TokenPayload } from "../core/ports/token-service.js";
3
+ import type { RequestId } from "../core/types/brand.js";
4
+ import type { TraceContext } from "../infrastructure/tracing/trace-context.js";
5
+ import type { I18nContext } from "./i18n/index.js";
6
+
7
+ /**
8
+ * Typed request context threaded through the middleware pipeline.
9
+ * Immutable — each middleware returns a new context with added fields.
10
+ */
11
+ export interface RequestContext {
12
+ readonly requestId: RequestId;
13
+ readonly startTime: number;
14
+ readonly ip: string;
15
+ readonly method: string;
16
+ readonly path: string;
17
+ readonly auth?: TokenPayload | undefined;
18
+ /** W3C Trace Context — traceId + spanId for distributed tracing */
19
+ readonly trace: TraceContext;
20
+ /** Request-scoped logger with requestId pre-bound */
21
+ readonly logger: Logger;
22
+ /** API version resolved from URL path or Accept-Version header */
23
+ readonly apiVersion: "v1" | "v2";
24
+ /** i18n context with locale-bound translation function */
25
+ readonly i18n: I18nContext;
26
+ }
@@ -0,0 +1,114 @@
1
+ import { banUserDto, changeRoleDto, listUsersDto } from "../../application/dtos/admin.dto.js";
2
+ import type { AdminService } from "../../application/services/admin.service.js";
3
+ import { UserRole } from "../../core/entities/user.entity.js";
4
+ import type { Logger } from "../../core/ports/logger.js";
5
+ import type { TokenService } from "../../core/ports/token-service.js";
6
+ import type { UserId } from "../../core/types/brand.js";
7
+ import type { RequestContext } from "../context.js";
8
+ import { authenticate, authorise } from "../middleware/auth.js";
9
+ import { validateBody } from "../middleware/validate.js";
10
+ import { errorResponse, jsonResponse, noContentResponse } from "./response.js";
11
+
12
+ /**
13
+ * Extract query parameters from a URL string without allocating a URL object.
14
+ */
15
+ const extractQuery = (url: string): URLSearchParams => {
16
+ const qIdx = url.indexOf("?");
17
+ return new URLSearchParams(qIdx === -1 ? "" : url.substring(qIdx + 1));
18
+ };
19
+
20
+ export const adminHandlers = (
21
+ adminService: AdminService,
22
+ tokenService: TokenService,
23
+ logger: Logger,
24
+ ) => {
25
+ /** Shared admin auth check — requires ADMIN role */
26
+ const requireAdmin = async (req: Request, _ctx: RequestContext) => {
27
+ const authResult = await authenticate(req, tokenService);
28
+ if (!authResult.ok) return authResult;
29
+ return authorise(authResult.value, [UserRole.ADMIN]);
30
+ };
31
+
32
+ return {
33
+ listUsers: async (req: Request, ctx: RequestContext): Promise<Response> => {
34
+ const auth = await requireAdmin(req, ctx);
35
+ if (!auth.ok) {
36
+ logger.warn("Admin auth failed on listUsers", { requestId: ctx.requestId });
37
+ return errorResponse(auth.error, ctx.requestId);
38
+ }
39
+
40
+ const query = extractQuery(req.url);
41
+ const parsed = validateBody(listUsersDto, {
42
+ cursor: query.get("cursor") ?? undefined,
43
+ limit: query.get("limit") ?? "20",
44
+ search: query.get("search") ?? undefined,
45
+ role: query.get("role") ?? undefined,
46
+ });
47
+ if (!parsed.ok) return errorResponse(parsed.error, ctx.requestId);
48
+
49
+ const dto = { ...parsed.value, limit: parsed.value.limit ?? 20 };
50
+ const result = await adminService.listUsers(dto);
51
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
52
+
53
+ return jsonResponse(result.value);
54
+ },
55
+
56
+ getUser: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
57
+ const auth = await requireAdmin(req, ctx);
58
+ if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
59
+
60
+ const result = await adminService.getUser(userId as UserId);
61
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
62
+
63
+ return jsonResponse(result.value);
64
+ },
65
+
66
+ changeRole: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
67
+ const auth = await requireAdmin(req, ctx);
68
+ if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
69
+
70
+ const body = await req.json().catch(() => null);
71
+ const validated = validateBody(changeRoleDto, body);
72
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
73
+
74
+ const result = await adminService.changeRole(
75
+ userId as UserId,
76
+ validated.value,
77
+ auth.value.sub,
78
+ ctx.ip,
79
+ );
80
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
81
+
82
+ return jsonResponse(result.value);
83
+ },
84
+
85
+ banUser: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
86
+ const auth = await requireAdmin(req, ctx);
87
+ if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
88
+
89
+ const body = await req.json().catch(() => null);
90
+ const validated = validateBody(banUserDto, body);
91
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
92
+
93
+ const result = await adminService.banUser(
94
+ userId as UserId,
95
+ validated.value,
96
+ auth.value.sub,
97
+ ctx.ip,
98
+ );
99
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
100
+
101
+ return noContentResponse();
102
+ },
103
+
104
+ unbanUser: async (req: Request, ctx: RequestContext, userId: string): Promise<Response> => {
105
+ const auth = await requireAdmin(req, ctx);
106
+ if (!auth.ok) return errorResponse(auth.error, ctx.requestId);
107
+
108
+ const result = await adminService.unbanUser(userId as UserId, auth.value.sub, ctx.ip);
109
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
110
+
111
+ return noContentResponse();
112
+ },
113
+ };
114
+ };
@@ -0,0 +1,68 @@
1
+ import { createApiKeyDto } from "../../application/dtos/auth.dto.js";
2
+ import type { ApiKeyService } from "../../application/services/api-key.service.js";
3
+ import type { Logger } from "../../core/ports/logger.js";
4
+ import type { TokenService } from "../../core/ports/token-service.js";
5
+ import type { RequestContext } from "../context.js";
6
+ import { authenticate } from "../middleware/auth.js";
7
+ import { validateBody } from "../middleware/validate.js";
8
+ import { createdResponse, errorResponse, jsonResponse, noContentResponse } from "./response.js";
9
+
10
+ export const apiKeyHandlers = (
11
+ apiKeyService: ApiKeyService,
12
+ tokenService: TokenService,
13
+ logger: Logger,
14
+ ) => ({
15
+ create: async (req: Request, ctx: RequestContext): Promise<Response> => {
16
+ const authResult = await authenticate(req, tokenService);
17
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
18
+
19
+ const body = await req.json().catch(() => null);
20
+ const validated = validateBody(createApiKeyDto, body);
21
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
22
+
23
+ const result = await apiKeyService.create(
24
+ authResult.value.sub,
25
+ validated.value.name,
26
+ validated.value.scopes ?? [],
27
+ validated.value.expiresInDays,
28
+ );
29
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
30
+
31
+ logger.info("API key created", {
32
+ requestId: ctx.requestId,
33
+ userId: authResult.value.sub,
34
+ keyId: result.value.key.id,
35
+ });
36
+
37
+ return createdResponse({
38
+ key: result.value.key,
39
+ rawKey: result.value.rawKey,
40
+ });
41
+ },
42
+
43
+ list: async (req: Request, ctx: RequestContext): Promise<Response> => {
44
+ const authResult = await authenticate(req, tokenService);
45
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
46
+
47
+ const result = await apiKeyService.list(authResult.value.sub);
48
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
49
+
50
+ return jsonResponse({ keys: result.value });
51
+ },
52
+
53
+ revoke: async (req: Request, ctx: RequestContext, keyId: string): Promise<Response> => {
54
+ const authResult = await authenticate(req, tokenService);
55
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
56
+
57
+ const result = await apiKeyService.revoke(keyId, authResult.value.sub);
58
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
59
+
60
+ logger.info("API key revoked", {
61
+ requestId: ctx.requestId,
62
+ userId: authResult.value.sub,
63
+ keyId,
64
+ });
65
+
66
+ return noContentResponse();
67
+ },
68
+ });