@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
@@ -0,0 +1,218 @@
1
+ import {
2
+ forgotPasswordDto,
3
+ loginDto,
4
+ logoutDto,
5
+ mfaDisableDto,
6
+ mfaEnableDto,
7
+ mfaVerifyDto,
8
+ refreshDto,
9
+ registerDto,
10
+ resetPasswordDto,
11
+ verifyEmailDto,
12
+ } from "../../application/dtos/auth.dto.js";
13
+ import type { AuthService } from "../../application/services/auth.service.js";
14
+ import type { Logger } from "../../core/ports/logger.js";
15
+ import type { TokenService } from "../../core/ports/token-service.js";
16
+ import type { RequestContext } from "../context.js";
17
+ import { authenticate } from "../middleware/auth.js";
18
+ import { validateBody } from "../middleware/validate.js";
19
+ import { createdResponse, errorResponse, jsonResponse, noContentResponse } from "./response.js";
20
+
21
+ export const authHandlers = (
22
+ authService: AuthService,
23
+ tokenService: TokenService,
24
+ logger: Logger,
25
+ ) => ({
26
+ register: async (req: Request, ctx: RequestContext): Promise<Response> => {
27
+ const body = await req.json().catch(() => null);
28
+ const validated = validateBody(registerDto, body);
29
+ if (!validated.ok) {
30
+ logger.warn("Registration validation failed", {
31
+ requestId: ctx.requestId,
32
+ code: validated.error.code,
33
+ });
34
+ return errorResponse(validated.error, ctx.requestId);
35
+ }
36
+
37
+ const result = await authService.register(validated.value);
38
+ if (!result.ok) {
39
+ logger.warn("Registration failed", { requestId: ctx.requestId, code: result.error.code });
40
+ return errorResponse(result.error, ctx.requestId);
41
+ }
42
+
43
+ return createdResponse(result.value);
44
+ },
45
+
46
+ login: async (req: Request, ctx: RequestContext): Promise<Response> => {
47
+ const body = await req.json().catch(() => null);
48
+ const validated = validateBody(loginDto, body);
49
+ if (!validated.ok) {
50
+ logger.warn("Login validation failed", {
51
+ requestId: ctx.requestId,
52
+ code: validated.error.code,
53
+ });
54
+ return errorResponse(validated.error, ctx.requestId);
55
+ }
56
+
57
+ const result = await authService.login(validated.value);
58
+ if (!result.ok) {
59
+ logger.warn("Login failed", { requestId: ctx.requestId, code: result.error.code });
60
+ return errorResponse(result.error, ctx.requestId);
61
+ }
62
+
63
+ return jsonResponse(result.value);
64
+ },
65
+
66
+ refresh: async (req: Request, ctx: RequestContext): Promise<Response> => {
67
+ const body = await req.json().catch(() => null);
68
+ const validated = validateBody(refreshDto, body);
69
+ if (!validated.ok) {
70
+ logger.warn("Refresh validation failed", {
71
+ requestId: ctx.requestId,
72
+ code: validated.error.code,
73
+ });
74
+ return errorResponse(validated.error, ctx.requestId);
75
+ }
76
+
77
+ const result = await authService.refresh(validated.value);
78
+ if (!result.ok) {
79
+ logger.warn("Token refresh failed", { requestId: ctx.requestId, code: result.error.code });
80
+ return errorResponse(result.error, ctx.requestId);
81
+ }
82
+
83
+ return jsonResponse(result.value);
84
+ },
85
+
86
+ logout: async (req: Request, ctx: RequestContext): Promise<Response> => {
87
+ const authResult = await authenticate(req, tokenService);
88
+ if (!authResult.ok) {
89
+ logger.warn("Auth failed on logout", {
90
+ requestId: ctx.requestId,
91
+ code: authResult.error.code,
92
+ });
93
+ return errorResponse(authResult.error, ctx.requestId);
94
+ }
95
+
96
+ const body = await req.json().catch(() => null);
97
+ const validated = validateBody(logoutDto, body);
98
+ if (!validated.ok) {
99
+ logger.warn("Logout validation failed", {
100
+ requestId: ctx.requestId,
101
+ code: validated.error.code,
102
+ });
103
+ return errorResponse(validated.error, ctx.requestId);
104
+ }
105
+
106
+ const authHeader = req.headers.get("authorization") ?? "";
107
+ const accessToken = authHeader.split(" ")[1] ?? "";
108
+
109
+ const result = await authService.logout({
110
+ ...validated.value,
111
+ accessToken,
112
+ });
113
+ if (!result.ok) {
114
+ logger.warn("Logout failed", { requestId: ctx.requestId, code: result.error.code });
115
+ return errorResponse(result.error, ctx.requestId);
116
+ }
117
+
118
+ return noContentResponse();
119
+ },
120
+
121
+ // ── v1.4 — Auth Platform handlers ──
122
+
123
+ verifyEmail: async (req: Request, ctx: RequestContext): Promise<Response> => {
124
+ const body = await req.json().catch(() => null);
125
+ const validated = validateBody(verifyEmailDto, body);
126
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
127
+
128
+ const result = await authService.verifyEmail(validated.value);
129
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
130
+
131
+ return jsonResponse({ message: "Email verified successfully" });
132
+ },
133
+
134
+ resendVerification: async (req: Request, ctx: RequestContext): Promise<Response> => {
135
+ const authResult = await authenticate(req, tokenService);
136
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
137
+
138
+ const result = await authService.resendVerification(authResult.value.sub);
139
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
140
+
141
+ return jsonResponse({ message: "Verification email sent", token: result.value.token });
142
+ },
143
+
144
+ forgotPassword: async (req: Request, ctx: RequestContext): Promise<Response> => {
145
+ const body = await req.json().catch(() => null);
146
+ const validated = validateBody(forgotPasswordDto, body);
147
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
148
+
149
+ const result = await authService.forgotPassword(validated.value);
150
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
151
+
152
+ // Always return success to prevent email enumeration
153
+ return jsonResponse({
154
+ message: "If this email exists, a reset link has been sent",
155
+ token: result.value.token,
156
+ });
157
+ },
158
+
159
+ resetPassword: async (req: Request, ctx: RequestContext): Promise<Response> => {
160
+ const body = await req.json().catch(() => null);
161
+ const validated = validateBody(resetPasswordDto, body);
162
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
163
+
164
+ const result = await authService.resetPassword(validated.value);
165
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
166
+
167
+ return jsonResponse({ message: "Password reset successfully" });
168
+ },
169
+
170
+ mfaSetup: async (req: Request, ctx: RequestContext): Promise<Response> => {
171
+ const authResult = await authenticate(req, tokenService);
172
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
173
+
174
+ const result = await authService.mfaSetup(authResult.value.sub, "");
175
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
176
+
177
+ return jsonResponse(result.value);
178
+ },
179
+
180
+ mfaEnable: async (req: Request, ctx: RequestContext): Promise<Response> => {
181
+ const authResult = await authenticate(req, tokenService);
182
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
183
+
184
+ const body = await req.json().catch(() => null);
185
+ const validated = validateBody(mfaEnableDto, body);
186
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
187
+
188
+ const result = await authService.mfaEnable(authResult.value.sub, validated.value);
189
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
190
+
191
+ return jsonResponse({ message: "MFA enabled successfully" });
192
+ },
193
+
194
+ mfaDisable: async (req: Request, ctx: RequestContext): Promise<Response> => {
195
+ const authResult = await authenticate(req, tokenService);
196
+ if (!authResult.ok) return errorResponse(authResult.error, ctx.requestId);
197
+
198
+ const body = await req.json().catch(() => null);
199
+ const validated = validateBody(mfaDisableDto, body);
200
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
201
+
202
+ const result = await authService.mfaDisable(authResult.value.sub, validated.value);
203
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
204
+
205
+ return jsonResponse({ message: "MFA disabled successfully" });
206
+ },
207
+
208
+ mfaVerify: async (req: Request, ctx: RequestContext): Promise<Response> => {
209
+ const body = await req.json().catch(() => null);
210
+ const validated = validateBody(mfaVerifyDto, body);
211
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
212
+
213
+ const result = await authService.mfaVerify(validated.value);
214
+ if (!result.ok) return errorResponse(result.error, ctx.requestId);
215
+
216
+ return jsonResponse(result.value);
217
+ },
218
+ });
@@ -0,0 +1,27 @@
1
+ import type { HealthService } from "../../application/services/health.service.js";
2
+
3
+ /**
4
+ * Health handler with two modes:
5
+ * - Deep check: full service health (for /readiness)
6
+ * - Shallow check: pre-serialized instant response (for /health and load balancer probes)
7
+ */
8
+ export const healthHandler = (healthService: HealthService) => {
9
+ /** Pre-serialized shallow health — zero JSON.stringify per request */
10
+ const shallowHeaders = new Headers({
11
+ "Content-Type": "application/json; charset=utf-8",
12
+ });
13
+
14
+ const deepCheck = async (): Promise<Response> => {
15
+ const status = await healthService.check();
16
+ const httpCode = status.status === "ok" ? 200 : 503;
17
+ return Response.json({ data: status }, { status: httpCode });
18
+ };
19
+
20
+ const shallowCheck = (): Response => {
21
+ // Ultra-fast: pre-built body, avoids JSON.stringify + async overhead
22
+ const body = `{"data":{"status":"ok","uptime":${process.uptime()}}}`;
23
+ return new Response(body, { status: 200, headers: shallowHeaders });
24
+ };
25
+
26
+ return { deepCheck, shallowCheck };
27
+ };
@@ -0,0 +1,15 @@
1
+ export { errorResponse, jsonResponse, createdResponse, noContentResponse } from "./response.js";
2
+ export { healthHandler } from "./health.handler.js";
3
+ export { authHandlers } from "./auth.handler.js";
4
+ export { userHandlers } from "./user.handler.js";
5
+ export { adminHandlers } from "./admin.handler.js";
6
+ export { metricsHandler } from "./metrics.handler.js";
7
+ export { apiKeyHandlers } from "./api-key.handler.js";
8
+ export { oauthHandlers } from "./oauth.handler.js";
9
+ export { webhookHandlers } from "./webhook.handler.js";
10
+ export {
11
+ createWebSocketManager,
12
+ type WebSocketManager,
13
+ type WsConnectionData,
14
+ } from "./websocket.handler.js";
15
+ export { createSseHandler, type SseHandler } from "./sse.handler.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Prometheus metrics HTTP handler — serves `GET /metrics` in text exposition format.
3
+ */
4
+
5
+ import type { MetricsCollector } from "../../core/ports/metrics.js";
6
+
7
+ const CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
8
+
9
+ export const metricsHandler = (metrics: MetricsCollector) => {
10
+ const headers = new Headers({
11
+ "Content-Type": CONTENT_TYPE,
12
+ "Cache-Control": "no-store",
13
+ });
14
+
15
+ const serve = (): Response => {
16
+ const body = metrics.serialize();
17
+ return new Response(body, { status: 200, headers });
18
+ };
19
+
20
+ return { serve };
21
+ };
@@ -0,0 +1,61 @@
1
+ import { oauthCallbackDto } from "../../application/dtos/auth.dto.js";
2
+ import type { AuthService } from "../../application/services/auth.service.js";
3
+ import type { Logger } from "../../core/ports/logger.js";
4
+ import type { OAuthProvider } from "../../core/ports/oauth.js";
5
+ import type { RequestContext } from "../context.js";
6
+ import { validateBody } from "../middleware/validate.js";
7
+ import { errorResponse, jsonResponse } from "./response.js";
8
+
9
+ export const oauthHandlers = (
10
+ authService: AuthService,
11
+ oauthProviders: ReadonlyMap<string, OAuthProvider>,
12
+ logger: Logger,
13
+ ) => ({
14
+ /**
15
+ * GET /api/v1/auth/oauth/:provider — redirect to provider's authorization URL
16
+ */
17
+ authorize: async (_req: Request, ctx: RequestContext, provider: string): Promise<Response> => {
18
+ const oauthProvider = oauthProviders.get(provider);
19
+ if (!oauthProvider) {
20
+ const body = `{"error":{"code":"NOT_FOUND","message":"Unknown provider: ${provider}"}}`;
21
+ return new Response(body, {
22
+ status: 404,
23
+ headers: { "Content-Type": "application/json; charset=utf-8" },
24
+ });
25
+ }
26
+
27
+ // Generate a state parameter for CSRF protection
28
+ const state = crypto.randomUUID();
29
+ const redirectUri = `${ctx.path.replace(`/oauth/${provider}`, `/oauth/${provider}/callback`)}`;
30
+
31
+ const url = oauthProvider.getAuthorizationUrl(state, redirectUri);
32
+ logger.info("OAuth redirect", { provider, requestId: ctx.requestId });
33
+
34
+ return new Response(null, {
35
+ status: 302,
36
+ headers: { Location: url },
37
+ });
38
+ },
39
+
40
+ /**
41
+ * POST /api/v1/auth/oauth/:provider/callback — exchange code for tokens
42
+ */
43
+ callback: async (req: Request, ctx: RequestContext, provider: string): Promise<Response> => {
44
+ const body = await req.json().catch(() => null);
45
+ const validated = validateBody(oauthCallbackDto, body);
46
+ if (!validated.ok) return errorResponse(validated.error, ctx.requestId);
47
+
48
+ const redirectUri = `${ctx.path}`;
49
+ const result = await authService.oauthLogin(provider, validated.value.code, redirectUri);
50
+ if (!result.ok) {
51
+ logger.warn("OAuth callback failed", {
52
+ provider,
53
+ requestId: ctx.requestId,
54
+ code: result.error.code,
55
+ });
56
+ return errorResponse(result.error, ctx.requestId);
57
+ }
58
+
59
+ return jsonResponse(result.value);
60
+ },
61
+ });